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 point | Endpoint | When |
|---|---|---|
| Token issuance | POST /token | Every token request |
| Authorization | GET /authorize | Every authorize redirect |
| Introspection | POST /introspect | Every token validation |
| Registration | POST /register | Every DCR request |
| Callback | GET /callback | Every IdP callback |
Each audit entry captures three layers of context:
| Field | What it contains |
|---|---|
endpoint | The request path (/token, /introspect, etc.) |
outcome | allow, deny, or error |
clientId | The OAuth client_id of the requester |
subjectId | The user's sub claim (empty for machine-to-machine flows) |
requestSummary | Key request fields — grant type, auth method, token presence |
policyInput | The full JSON input that OPA evaluated |
policyDecision | The full JSON output from OPA — scopes, obligations, deny reasons |
ip | Client IP address |
userAgent | User-Agent header |
createdAt | Timestamp (ISO 8601) |
The policyInput and policyDecision fields are the critical pair: they record exactly what OPA saw and exactly what OPA decided, making every decision fully reproducible.
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 tracks policy bundle deployments — when the OPA bundle changed, what version was deployed, and what triggered the change. This answers a different compliance question: what policy was in effect at the time of a given decision?
Bundle audit entries record:
| Field | Purpose |
|---|---|
version | Bundle version identifier |
digest | Content hash of the bundle |
source | How the change was deployed (API, git, manual) |
timestamp | When the bundle was built and served |
Combined with the request audit log, you can reconstruct the full picture: this decision was made at this time, under this policy version, 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
/tokenor/introspectentries - Client — show decisions for a specific
client_id - Subject — show decisions for a specific user
- 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
| Framework | What audit provides |
|---|---|
| HIPAA | Who accessed patient data, under what authority, with full policy context |
| SOC 2 | Continuous evidence of access control enforcement and change tracking |
| FedRAMP | Decision-level logging with immutable trails for federal workloads |
| GDPR | Record of lawful basis for data access (consent, legitimate interest) via policy data |
| PIPEDA | Canadian 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