Skip to main content
Version: Latest

Audit

Every policy decision in PBAC is recorded — who requested access, what they requested, what OPA decided, and why. The audit log is append-only and immutable. It answers the question every regulator asks: who accessed what, when, and under what authority?


What gets logged​

PBAC logs every policy evaluation at every OAuth decision point. Both allow and deny outcomes are recorded.

Decision pointEndpointWhen
Token issuancePOST /tokenEvery token request
AuthorizationGET /authorizeEvery authorize redirect
IntrospectionPOST /introspectEvery token validation
RegistrationPOST /registerEvery DCR request
CallbackGET /callbackEvery IdP callback

Each audit entry captures three layers of context:

FieldWhat it contains
endpointThe request path (/token, /introspect, etc.)
outcomeallow, deny, or error
clientIdThe OAuth client_id of the requester
subjectIdThe user's sub claim (empty for machine-to-machine flows)
flowIdOpaque, non-PII id shared by every request in one journey (/authorize → callback → /token → /introspect) — see Correlating a full journey
requestSummaryKey request fields — grant type, auth method, token presence
policyInputThe JSON input that OPA evaluated, without the user's IdP claim values (subject.claims)
policyDecisionThe JSON output from OPA — scopes, obligations, every deny reason in reasons. Values policy authored for a client (subject_claims, token_response_extra) appear by name only
ipClient IP address
userAgentUser-Agent header
createdAtTimestamp (ISO 8601)

The policyInput and policyDecision fields are the critical pair: they record what OPA saw and what OPA decided. IdP claim values live only in the user's stored claims (subject_userinfo), not in the audit row, so replaying a decision that read a claim (an identity segment, a groups rule) needs the user's claims from subject_userinfo, which holds their current values.


Correlating a full journey​

A single user action is rarely one request. An authorization-code login spans /authorize, one or more IdP callbacks, /token, and later /introspect calls from the resource server or gateway — each landing its own audit row. The flowId ties them together.

PBAC mints an opaque, non-PII flowId at the start of /authorize and carries it across every downstream request in that journey:

The same id surfaces in three places, so one journey can be reconstructed end to end across the audit log, your traces, and your own logs:

  • Audit log — every row in the journey shares the flowId, so filtering by it returns the whole /authorize → … → /introspect trail.
  • Introspection response — the flow_id field is returned to the resource server, letting an RS stitch its logs to the PBAC journey.
  • Traces — the pbac.flow.id span attribute carries the same value, so a journey lines up with distributed traces exported over OTLP.

In the admin UI, each Activity/Audit row shows a flow chip; clicking it (or the Flow ID link on an entry's detail) filters the view to that single journey. Multi-hop obligation re-evaluations inherit the id automatically.


Immutability​

The audit log is append-only. Entries cannot be modified or deleted through the Admin API or admin UI. This is a design constraint, not a limitation — compliance frameworks (HIPAA, SOC 2, FedRAMP) require that audit trails be tamper-evident.


Bundle audit​

Separate from the request audit log, PBAC records every change to the policy bundle it serves to OPA: when the content changed, what it changed to, and which write changed it. This answers a different compliance question: what policy was in effect at the time of a given decision?

Each entry records:

FieldPurpose
etagContent hash of the bundle after the change
previousEtagContent hash of the bundle before it
ruleCount, rulePaths, dataKeysWhat the bundle carries
bundleSizeBytesSize of the bundle
triggerSourceWhat made the change (see below)
triggeredByRequest ID of the write, to join with the request audit log
createdAtWhen the change was committed

triggerSource is one of:

ValueThe change came from
startupThe first bundle a new database serves
apiAn admin API or admin UI write, or a change the AS makes itself, such as a connector sync
batch_deployPOST /admin/api/policy-bundle/deploy
deploy_rollbackA deploy that OPA refused to activate, undone by restoring the earlier rules and data
rule_save_rollbackA single rule save that OPA refused to activate, undone

Exactly once. A write records its entry in the same database transaction as the change itself. A change that commits has exactly one entry, with that write's trigger source, and a change that rolls back has none. A write that changes nothing the bundle carries adds no entry. This holds with any number of AS replicas.

A failed entry fails the write. If the entry cannot be written, the policy change is not saved either. The admin API answers 503 with {"error": "policy_change_not_recorded"}, and nothing has changed. Retry once the database is healthy.

At most once, outside a transaction. A few changes are made without a surrounding transaction: a connector sync, and the confirm step of a rule save or deploy, which rebuilds the bundle to read its revision. The change is already saved when the entry is written, so if the entry fails, the AS logs an error and keeps the change. The confirm step's rebuild finds the entry its save already wrote and adds nothing.

Combined with the request audit log, you can reconstruct the full picture: this decision was made at this time, under this bundle, with this input, producing this output.


Querying​

The audit log is queryable via the Admin API with filters for any combination of:

  • Endpoint — show only /token or /introspect entries
  • Client — show decisions for a specific client_id
  • Subject — show decisions for a specific user
  • Flow — show every row in one journey, by flowId
  • Outcome — show only denials, or only allows
  • Date range — entries between two timestamps

Results are paginated and sorted by timestamp (most recent first). Individual entries can be fetched by ID to retrieve the full policyInput and policyDecision JSON.

The admin UI provides the same filters with a visual interface, including formatted JSON views of the policy input and decision for each entry.


Compliance use cases​

FrameworkWhat audit provides
HIPAAWho accessed patient data, under what authority, with full policy context
SOC 2Continuous evidence of access control enforcement and change tracking
FedRAMPDecision-level logging with immutable trails for federal workloads
GDPRRecord of lawful basis for data access (consent, legitimate interest) via policy data
PIPEDACanadian privacy law compliance — consent-based access logged with full decision context

The audit log is not just for compliance reviews. It's operational: when a user reports they can't access a resource, the audit log shows the exact OPA input and output for their request — including which rule denied them and why.


Next steps​

  • Policy Model — How the policy decisions that get audited are made
  • Architecture — Where audit fits in the overall system