Log each access denied decision as its own event with the actor, the resource, the action attempted, the rule that said no, and the request context. Avoid logging the sensitive content of the resource. Then alert on patterns, such as one user probing many records, not on single denials.
Why denials are worth recording
Most application users never see a 403. When they do, it is usually a mistake, such as a stale bookmark. A run of denials against many different object IDs is different. It often means someone is testing whether they can read other people's data by changing an identifier in the URL.
The OWASP Logging Cheat Sheet lists authorization (access control) failures among the events to log wherever possible. OWASP Top 10 item A09 repeats the point: log login, access control and server side input validation failures with enough user context to identify suspicious accounts.
One caution from the OWASP Authorization Cheat Sheet: too much logging and too little logging can both be weaknesses. It says volume should match the application's environment and requirements. Denials are high value. Logging every successful check is rarely needed.
Fields for a denial event
| Field | Example | Why |
|---|---|---|
| Timestamp in UTC | 2026-10-19T09:14:02Z | Ordering during an investigation |
| Event name | authz_fail | Easy to query and alert on |
| Actor ID | user 4821 | Who tried |
| Tenant or account | tenant 77 | Shows cross tenant attempts |
| Resource type and ID | invoice 90311 | What they reached for |
| Action | read, update, delete | What they tried to do |
| Policy or rule | owner_only | Helps tune false positives |
| Request ID | a trace or correlation ID | Joins the denial to other logs |
| Source IP | verified client address | Context for blocking |
The OWASP Logging Vocabulary gives the example name authz_fail with the user and the resource. Consistent names matter more than the exact spelling.
Steps to add it
- Find the single place that makes the allow or deny decision. A central middleware or policy function beats scattered checks.
- Emit one structured event when the answer is deny, before returning the error.
- Return a generic response to the client. Do not echo the rule name or the resource details back to the user.
- Make sure the logging call cannot throw and break the request. See logging failures must not crash your app.
- Check that deny by default is real. The OWASP cheat sheet says an application should be configured to deny access by default, so unexpected states end up in your denial logs rather than being allowed silently.
Alert on patterns
A single denial is noise. Useful patterns include:
- One actor with many denials across different resource IDs in a short window.
- A normal user hitting admin functions.
- Denials across tenants, meaning a user touching another tenant's identifiers.
- A sudden rise in denials after a release, which may point to a broken permission rule rather than an attack.
Start with one rule, check how often it fires, and tune it. An alert nobody acts on is not a control.
Quick check
Should denied requests return a different message to the user?
Return a generic message to the client and keep the detail in the server side log.
Do I log successful authorization checks too?
Not usually. Log successes for sensitive actions, such as admin changes and data export, where an audit trail is needed.
Safeguard's policies and gates apply the same idea to your pipeline: a failed check is recorded and acted on. Try the free scan at gold.safeguard.sh/scan.
Sources
- OWASP Logging Cheat Sheet
- OWASP Top 10 A09:2021
- OWASP Authorization Cheat Sheet
- OWASP Logging Vocabulary Cheat Sheet
Last reviewed: 19 October 2026.