diff --git a/src/storage.rs b/src/storage.rs
index 6eac40cadda57e103591b410b6c52513c2d8c16c..6330ce6237c4663bb1080961c78781b291c7a4f5 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -2986,7 +2986,7 @@ pub mod acl {
             ("execute", "role") => {
                 let object = object.expect("should be set");
                 crate::audit!(
-                    message: "revoke role `{object}` from {grantee_type} `{grantee}`",
+                    message: "revoked role `{object}` from {grantee_type} `{grantee}`",
                     title: "revoke_role",
                     severity: High,
                     role: object,
diff --git a/src/traft/node.rs b/src/traft/node.rs
index f0e46c53a215e300193900e6cab4341cde73e40e..621d4abb42403e3defd049883a9fe0b9a465a970 100644
--- a/src/traft/node.rs
+++ b/src/traft/node.rs
@@ -779,6 +779,7 @@ impl NodeImpl {
                                 message: "property `{key}` was deleted",
                                 title: "change_config",
                                 severity: High,
+                                key: %key,
                             );
                         }
                     }
@@ -803,6 +804,8 @@ impl NodeImpl {
                                 message: "property `{key}` was changed to {value}",
                                 title: "change_config",
                                 severity: High,
+                                key: %key,
+                                value: &value,
                             );
                         }
                     }
diff --git a/test/conftest.py b/test/conftest.py
index d37e65f22cf4fbe6d2e927151d38079d6b269995..6fd0c643828b04046cf76ff3e0e25754b0d8df9a 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -426,6 +426,7 @@ class Instance:
 
     color: Callable[[str], str]
 
+    audit: str | bool = True
     tier: str | None = None
     init_replication_factor: int | None = None
     init_cfg_path: str | None = None
@@ -452,8 +453,24 @@ class Instance:
     def replicaset_uuid(self):
         return self.eval("return box.info.cluster.uuid")
 
+    @property
+    def audit_flag_value(self):
+        """
+        This property abstracts away peculiarities of the audit config.
+        This is the value we're going to pass via `--audit`, or `None`
+        if audit is disabled for this instance.
+        """
+        if self.audit:
+            if isinstance(self.audit, bool):
+                return os.path.join(self.data_dir, "audit.log")
+            if isinstance(self.audit, str):
+                return self.audit
+        return None
+
     @property
     def command(self):
+        audit = self.audit_flag_value
+
         # fmt: off
         return [
             self.binary_path, "run",
@@ -469,6 +486,7 @@ class Instance:
             *(["--init-cfg", self.init_cfg_path]
               if self.init_cfg_path is not None else []),
             *(["--tier", self.tier] if self.tier is not None else []),
+            *(["--audit", audit] if audit else []),
         ]
         # fmt: on
 
@@ -1179,6 +1197,7 @@ class Cluster:
             init_replication_factor=init_replication_factor,
             tier=tier,
             init_cfg_path=self.cfg_path,
+            audit=True,
         )
 
         self.instances.append(instance)
diff --git a/test/int/test_audit.py b/test/int/test_audit.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9b8d4f9deb181a942904463faea3a59758137aa6 100644
--- a/test/int/test_audit.py
+++ b/test/int/test_audit.py
@@ -0,0 +1,501 @@
+import pytest
+import json
+
+from dataclasses import dataclass
+from typing import Optional, ClassVar
+from enum import Enum
+
+from tarantool.error import (  # type: ignore
+    NetworkError,
+)
+
+from conftest import (
+    Instance,
+    Cluster,
+    retrying,
+)
+
+
+class Severity(str, Enum):
+    Low = "low"
+    Medium = "medium"
+    High = "high"
+
+
+@dataclass
+class Event:
+    id: str
+    time: str
+    title: str
+    message: str
+    severity: Severity
+
+    @staticmethod
+    def parse(s):
+        match s["title"]:
+            case EventLocalStartup.TITLE:
+                return EventLocalStartup(**s)
+            case EventLocalShutdown.TITLE:
+                return EventLocalShutdown(**s)
+            case EventInitAudit.TITLE:
+                return EventInitAudit(**s)
+            case EventAuthOk.TITLE:
+                return EventAuthOk(**s)
+            case EventAuthFail.TITLE:
+                return EventAuthFail(**s)
+            case EventChangeConfig.TITLE:
+                return EventChangeConfig(**s)
+            case EventChangePassword.TITLE:
+                return EventChangePassword(**s)
+            case EventChangeTargetGrade.TITLE:
+                return EventChangeTargetGrade(**s)
+            case EventChangeCurrentGrade.TITLE:
+                return EventChangeCurrentGrade(**s)
+            case EventJoinInstance.TITLE:
+                return EventJoinInstance(**s)
+            case EventExpelInstance.TITLE:
+                return EventExpelInstance(**s)
+            case EventGrantPrivilege.TITLE:
+                return EventGrantPrivilege(**s)
+            case EventRevokePrivilege.TITLE:
+                return EventRevokePrivilege(**s)
+            case EventGrantRole.TITLE:
+                return EventGrantRole(**s)
+            case EventRevokeRole.TITLE:
+                return EventRevokeRole(**s)
+            case EventCreateRole.TITLE:
+                return EventCreateRole(**s)
+            case EventDropRole.TITLE:
+                return EventDropRole(**s)
+            case EventCreateUser.TITLE:
+                return EventCreateUser(**s)
+            case EventDropUser.TITLE:
+                return EventDropUser(**s)
+            case EventCreateTable.TITLE:
+                return EventCreateTable(**s)
+            case EventDropTable.TITLE:
+                return EventDropTable(**s)
+
+
+@dataclass
+class EventLocalStartup(Event):
+    TITLE: ClassVar[str] = "local_startup"
+
+
+@dataclass
+class EventLocalShutdown(Event):
+    TITLE: ClassVar[str] = "local_shutdown"
+
+
+@dataclass
+class EventInitAudit(Event):
+    TITLE: ClassVar[str] = "init_audit"
+
+
+@dataclass
+class EventAuthOk(Event):
+    TITLE: ClassVar[str] = "auth_ok"
+    user: str
+
+
+@dataclass
+class EventAuthFail(Event):
+    TITLE: ClassVar[str] = "auth_fail"
+    user: str
+    verdict: Optional[str] = None
+
+
+@dataclass
+class EventChangeConfig(Event):
+    TITLE: ClassVar[str] = "change_config"
+    key: str
+    value: Optional[str] = None
+
+
+@dataclass
+class EventChangePassword(Event):
+    TITLE: ClassVar[str] = "change_password"
+    user: str
+    auth_type: str
+
+
+@dataclass
+class EventChangeTargetGrade(Event):
+    TITLE: ClassVar[str] = "change_target_grade"
+    instance_id: str
+    raft_id: str
+    new_grade: str
+
+
+@dataclass
+class EventChangeCurrentGrade(Event):
+    TITLE: ClassVar[str] = "change_current_grade"
+    instance_id: str
+    raft_id: str
+    new_grade: str
+
+
+@dataclass
+class EventJoinInstance(Event):
+    TITLE: ClassVar[str] = "join_instance"
+    instance_id: str
+    raft_id: str
+
+
+@dataclass
+class EventExpelInstance(Event):
+    TITLE: ClassVar[str] = "expel_instance"
+    instance_id: str
+    raft_id: str
+
+
+@dataclass
+class EventGrantPrivilege(Event):
+    TITLE: ClassVar[str] = "grant_privilege"
+    privilege: str
+    object: str
+    object_type: str
+    grantee: str
+    grantee_type: str
+
+
+@dataclass
+class EventRevokePrivilege(Event):
+    TITLE: ClassVar[str] = "revoke_privilege"
+    privilege: str
+    object: str
+    object_type: str
+    grantee: str
+    grantee_type: str
+
+
+@dataclass
+class EventGrantRole(Event):
+    TITLE: ClassVar[str] = "grant_role"
+    role: str
+    grantee: str
+    grantee_type: str
+
+
+@dataclass
+class EventRevokeRole(Event):
+    TITLE: ClassVar[str] = "revoke_role"
+    role: str
+    grantee: str
+    grantee_type: str
+
+
+@dataclass
+class EventCreateRole(Event):
+    TITLE: ClassVar[str] = "create_role"
+    role: str
+
+
+@dataclass
+class EventDropRole(Event):
+    TITLE: ClassVar[str] = "drop_role"
+    role: str
+
+
+@dataclass
+class EventCreateUser(Event):
+    TITLE: ClassVar[str] = "create_user"
+    user: str
+    auth_type: str
+
+
+@dataclass
+class EventDropUser(Event):
+    TITLE: ClassVar[str] = "drop_user"
+    user: str
+
+
+@dataclass
+class EventCreateTable(Event):
+    TITLE: ClassVar[str] = "create_table"
+    name: str
+
+
+@dataclass
+class EventDropTable(Event):
+    TITLE: ClassVar[str] = "drop_table"
+    name: str
+
+
+class AuditFile:
+    def __init__(self, path):
+        self._f = open(path)
+
+    def events(self):
+        for line in self._f:
+            yield Event.parse(json.loads(line))
+
+
+def take_until_type(events, event_class: type):
+    for event in events:
+        if isinstance(event, event_class):
+            return event
+    return None
+
+
+def test_startup(instance: Instance):
+    instance.start()
+    instance.terminate()
+
+    events = list(AuditFile(instance.audit_flag_value).events())
+    assert len(events) > 0
+
+    # Check identifiers
+    i = 1
+    for event in events:
+        assert event.id == f"1.0.{i}"
+        i += 1
+
+    # These should be the first two events
+    assert isinstance(events[0], EventInitAudit)
+    assert events[0].message == "audit log is ready"
+    assert events[0].severity == Severity.Low
+    assert isinstance(events[1], EventLocalStartup)
+    assert events[1].message == "instance is starting"
+    assert events[1].severity == Severity.Low
+
+    event = take_until_type(iter(events), EventJoinInstance)
+    assert event is not None
+    assert event.instance_id == "i1"
+    assert event.raft_id == "1"
+
+    event = take_until_type(iter(events), EventChangeTargetGrade)
+    assert event is not None
+    assert event.new_grade == "Offline(0)"
+    assert event.instance_id == "i1"
+    assert event.raft_id == "1"
+    assert (
+        event.message
+        == f"target grade of instance `{event.instance_id}` changed to {event.new_grade}"
+    )
+    assert event.severity == Severity.Low
+
+    event = take_until_type(iter(events), EventChangeCurrentGrade)
+    assert event is not None
+    assert event.new_grade == "Offline(0)"
+    assert event.instance_id == "i1"
+    assert event.raft_id == "1"
+    assert (
+        event.message
+        == f"current grade of instance `{event.instance_id}` changed to {event.new_grade}"
+    )
+    assert event.severity == Severity.Medium
+
+    event = take_until_type(iter(events), EventChangeConfig)
+    assert event is not None
+
+
+def test_create_drop_table(instance: Instance):
+    instance.start()
+    instance.sql(
+        """
+        create table "foo" ("val" int not null, primary key ("val"))
+        distributed by ("val")
+        """
+    )
+    instance.sql(
+        """
+        drop table "foo"
+        """
+    )
+    instance.terminate()
+
+    events = AuditFile(instance.audit_flag_value).events()
+
+    create_table = take_until_type(events, EventCreateTable)
+    assert create_table is not None
+    assert create_table.name == "foo"
+    assert create_table.message == "created table `foo`"
+    assert create_table.severity == Severity.Medium
+
+    drop_table = take_until_type(events, EventDropTable)
+    assert drop_table is not None
+    assert drop_table.name == "foo"
+    assert drop_table.message == "dropped table `foo`"
+    assert drop_table.severity == Severity.Medium
+
+
+def test_user(instance: Instance):
+    instance.start()
+    instance.sql(
+        """
+        create user "ymir" with password '0123456789' using chap-sha1
+        """
+    )
+    instance.sql(
+        """
+        alter user "ymir" password '9876543210'
+        """
+    )
+    instance.sql(
+        """
+        drop user "ymir"
+        """
+    )
+    instance.terminate()
+
+    events = AuditFile(instance.audit_flag_value).events()
+
+    create_user = take_until_type(events, EventCreateUser)
+    assert create_user is not None
+    assert create_user.user == "ymir"
+    assert create_user.auth_type == "chap-sha1"
+    assert create_user.message == f"created user `{create_user.user}`"
+    assert create_user.severity == Severity.High
+
+    change_password = take_until_type(events, EventChangePassword)
+    assert change_password is not None
+    assert change_password.user == "ymir"
+    assert change_password.auth_type == "chap-sha1"
+    assert (
+        change_password.message
+        == f"password of user `{change_password.user}` was changed"
+    )
+    assert change_password.severity == Severity.High
+
+    drop_user = take_until_type(events, EventDropUser)
+    assert drop_user is not None
+    assert drop_user.user == "ymir"
+    assert drop_user.message == f"dropped user `{drop_user.user}`"
+    assert drop_user.severity == Severity.Medium
+
+
+def test_role(instance: Instance):
+    instance.start()
+    instance.sql(
+        """
+        create role "skibidi"
+        """
+    )
+    instance.sql(
+        """
+        create role "dummy"
+        """
+    )
+    instance.sudo_sql(
+        """
+        grant "dummy" to "skibidi"
+        """
+    )
+    instance.sudo_sql(
+        """
+        revoke "dummy" from "skibidi"
+        """
+    )
+    instance.sql(
+        """
+        drop role "skibidi"
+        """
+    )
+    instance.terminate()
+
+    events = AuditFile(instance.audit_flag_value).events()
+
+    create_role = take_until_type(events, EventCreateRole)
+    assert create_role is not None
+    assert create_role.role == "skibidi"
+    assert create_role.message == f"created role `{create_role.role}`"
+    assert create_role.severity == Severity.High
+
+    grant_role = take_until_type(events, EventGrantRole)
+    assert grant_role is not None
+    # assert grant_role.role == "dummy"  # FIXME: currently it's u32
+    assert grant_role.grantee == "skibidi"
+    assert grant_role.grantee_type == "role"
+    assert (
+        grant_role.message
+        == f"granted role `{grant_role.role}` to role `{grant_role.grantee}`"
+    )
+    assert grant_role.severity == Severity.High
+
+    revoke_role = take_until_type(events, EventRevokeRole)
+    assert revoke_role is not None
+    # assert revoke_role.role == "dummy"  # FIXME: currently it's u32
+    assert revoke_role.grantee == "skibidi"
+    assert revoke_role.grantee_type == "role"
+    assert (
+        revoke_role.message
+        == f"revoked role `{grant_role.role}` from role `{revoke_role.grantee}`"
+    )
+    assert revoke_role.severity == Severity.High
+
+    drop_role = take_until_type(events, EventDropRole)
+    assert drop_role is not None
+    assert drop_role.role == "skibidi"
+    assert drop_role.message == f"dropped role `{drop_role.role}`"
+    assert drop_role.severity == Severity.Medium
+
+
+def assert_instance_expelled(expelled_instance: Instance, instance: Instance):
+    info = instance.call("pico.instance_info", expelled_instance.instance_id)
+    grades = (info["current_grade"]["variant"], info["target_grade"]["variant"])
+    assert grades == ("Expelled", "Expelled")
+
+
+def test_join_expel_instance(cluster: Cluster):
+    cluster.deploy(instance_count=1)
+    i1 = cluster.instances[0]
+
+    audit_i1 = AuditFile(i1.audit_flag_value)
+    for _ in audit_i1.events():
+        pass
+    events = audit_i1.events()
+
+    i2 = cluster.add_instance(instance_id="i2")
+
+    join_instance = take_until_type(events, EventJoinInstance)
+    assert join_instance is not None
+    assert join_instance.instance_id == "i2"
+    assert join_instance.raft_id == str(i2.raft_id)
+    assert join_instance.severity == Severity.Low
+
+    cluster.expel(i2)
+    retrying(lambda: assert_instance_expelled(i2, i1))
+
+    expel_instance = take_until_type(events, EventExpelInstance)
+    assert expel_instance is not None
+    assert expel_instance.instance_id == "i2"
+    assert expel_instance.raft_id == str(i2.raft_id)
+    assert expel_instance.severity == Severity.Low
+
+
+def test_auth(instance: Instance):
+    instance.start()
+
+    instance.sudo_sql(
+        """
+        create user "ymir" with password '0123456789' using chap-sha1
+        """
+    )
+    instance.sudo_sql(
+        """
+        alter user "ymir" login
+        """
+    )
+
+    audit = AuditFile(instance.audit_flag_value)
+    for _ in audit.events():
+        pass
+    events = audit.events()
+
+    with instance.connect(4, user="ymir", password="0123456789") as _:
+        pass
+
+    auth_ok = take_until_type(events, EventAuthOk)
+    assert auth_ok is not None
+    assert auth_ok.user == "ymir"
+    assert auth_ok.severity == Severity.High
+
+    with pytest.raises(NetworkError):
+        with instance.connect(4, user="ymir", password="wrong_pwd") as _:
+            pass
+
+    auth_fail = take_until_type(events, EventAuthFail)
+    assert auth_fail is not None
+    assert auth_fail.user == "ymir"
+    assert auth_fail.severity == Severity.High