diff --git a/src/lib.rs b/src/lib.rs
index 68180f720612effb424308edf35ea49b761ec9d8..6c1706dc23e68b8926c4b3072aaca0ad1d2c378e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,11 +5,11 @@ use serde::{Deserialize, Serialize};
 
 use ::raft::prelude as raft;
 use ::tarantool::error::Error as TntError;
-use ::tarantool::fiber;
 use ::tarantool::fiber::r#async::timeout::{self, IntoTimeout};
 use ::tarantool::time::Instant;
 use ::tarantool::tlua;
 use ::tarantool::transaction::transaction;
+use ::tarantool::{fiber, session};
 use rpc::{join, update_instance};
 use std::cell::OnceCell;
 use std::collections::HashMap;
@@ -18,6 +18,7 @@ use std::time::Duration;
 use storage::Clusterwide;
 use traft::RaftSpaceAccess;
 
+use crate::access_control::user_by_id;
 use crate::cli::args;
 use crate::cli::args::Address;
 use crate::cli::init_cfg::InitCfg;
@@ -25,9 +26,10 @@ use crate::instance::Grade;
 use crate::instance::GradeVariant::*;
 use crate::instance::Instance;
 use crate::plugin::*;
+use crate::schema::ADMIN_ID;
 use crate::tier::{Tier, DEFAULT_TIER};
 use crate::traft::op;
-use crate::util::{unwrap_or_terminate, validate_and_complete_unix_socket_path};
+use crate::util::{effective_user_id, unwrap_or_terminate, validate_and_complete_unix_socket_path};
 
 mod access_control;
 pub mod audit;
@@ -404,6 +406,41 @@ fn set_login_attempts_check(storage: Clusterwide) {
     .expect("setting on auth trigger should not fail")
 }
 
+fn set_on_access_denied_audit_trigger() {
+    let lua = ::tarantool::lua_state();
+
+    lua.exec_with(
+        "box.session.on_access_denied(...)",
+        tlua::function4(
+            move |privilege_type: String,
+                  object_type: String,
+                  object_name: String,
+                  _lua: tlua::LuaState| {
+                let effective_user = effective_user_id();
+
+                // we do not have box.session.user() equivalent that returns an id straight away
+                // so we look up the user by id.
+                // Note: since we're in a user context we may not have access to use space.
+                let user = session::with_su(ADMIN_ID, || {
+                    user_by_id(effective_user).expect("user exists").name
+                })
+                .expect("must be able to su into admin");
+
+                crate::audit!(
+                    message: "{privilege_type} access to {object_type} `{object_name}` is denied for user `{user}`",
+                    title: "access_denied",
+                    severity: Medium,
+                    privilege_type: &privilege_type,
+                    object_type: &object_type,
+                    object_name: &object_name,
+                    initiator: &user,
+                );
+            },
+        ),
+    )
+    .expect("setting on auth trigger should not fail")
+}
+
 #[allow(clippy::enum_variant_names)]
 #[derive(Debug, Serialize, Deserialize)]
 pub enum Entrypoint {
@@ -451,6 +488,7 @@ fn init_common(args: &args::Run, cfg: &tarantool::Cfg) -> (Clusterwide, RaftSpac
 
     let storage = Clusterwide::try_get(true).expect("storage initialization should never fail");
     set_login_attempts_check(storage.clone());
+    set_on_access_denied_audit_trigger();
     let raft_storage =
         RaftSpaceAccess::new().expect("raft storage initialization should never fail");
     (storage.clone(), raft_storage)
diff --git a/test/int/test_audit.py b/test/int/test_audit.py
index b799bc1deb7d00188c414d57cfa3f4d215d3208d..fd51283f8fc8e442d6ecc5c72b3854114caecbe0 100644
--- a/test/int/test_audit.py
+++ b/test/int/test_audit.py
@@ -75,6 +75,10 @@ class Event:
                 return EventCreateTable(**s)
             case EventDropTable.TITLE:
                 return EventDropTable(**s)
+            case EventAccessDenied.TITLE:
+                return EventAccessDenied(**s)
+            case _:
+                raise ValueError(f"Unknown event type for event: '{s}'")
 
 
 @dataclass
@@ -240,6 +244,15 @@ class EventDropTable(Event):
     initiator: str
 
 
+@dataclass
+class EventAccessDenied(Event):
+    TITLE: ClassVar[str] = "access_denied"
+    privilege_type: str
+    object_type: str
+    object_name: str
+    initiator: str
+
+
 class AuditFile:
     def __init__(self, path):
         self._f = open(path)
@@ -549,3 +562,33 @@ def test_auth(instance: Instance):
     assert auth_fail.user == "ymir"
     assert auth_fail.severity == Severity.High
     assert auth_fail.initiator == "ymir"
+
+
+def test_access_denied(instance: Instance):
+    instance.start()
+
+    instance.create_user(with_name="ymir", with_password="12341234")
+
+    audit = AuditFile(instance.audit_flag_value)
+    for _ in audit.events():
+        pass
+
+    events = audit.events()
+
+    expected_error = "Create access to role 'R' is denied for user 'ymir'"
+    expected_audit = "Create access to role `R` is denied for user `ymir`"
+
+    with pytest.raises(
+        Exception,
+        match=expected_error,
+    ):
+        instance.sql('CREATE ROLE "R"', user="ymir", password="12341234")
+
+    access_denied = take_until_type(events, EventAccessDenied)
+    assert access_denied is not None
+    assert access_denied.message == expected_audit
+    assert access_denied.severity == Severity.Medium
+    assert access_denied.privilege_type == "Create"
+    assert access_denied.object_type == "role"
+    assert access_denied.object_name == "R"
+    assert access_denied.initiator == "ymir"