Audit trail reference
This page lists every event the trail records, every field a record carries and every attribute a collector can filter on without parsing a body: 22 events, 7 outcomes, 46 reasons and 23 record fields. The page is generated from the package that emits them, so a vocabulary it does not list is one no record can carry.
This is the shape of the trail. The audit trail is why it has this shape: why delivery is synchronous, why a gap is evidence rather than noise, and what is deliberately not recorded. The short version of the last one is that no record carries content. Guardrail records name entity types and count them; the matched text is not in the record and cannot be recovered from it.
Contents
Section titled “Contents”Events
Section titled “Events”An event says what happened, and it is the first thing a reader filters on. Reads and admitted requests are absent by design. A trail of every request is the traffic log, and burying the refusals in it is how an audit trail stops being read.
key.mint
A virtual key created through the admin API.
key.revoke
A virtual key revoked through the admin API.
config.reload
A configuration reload, successful or not.
config.source.write
A configuration source written or removed through the admin API, successful or not. It is separate from config.reload because the two answer different questions: a reload says an operator pushed the file this node started with, and this says somebody changed the deployment from outside it.
secret.write
A secret stored or removed through the admin API, successful or not. The record names the secret and never the value, which is the point of the store it went into.
suspension.change
Something taken out of service, or put back, through the admin API. The operator’s stated reason is not in the record, because reason here is the bounded vocabulary. It is on the suspension itself and in the log line beside this.
request.suspended
A data-plane request refused because something it needed is out of service. Separate from request.auth because nothing was wrong with the caller: the deployment was deliberately not serving, and an incident review asking “what did we break while we had it out” needs to be able to count exactly this.
cluster.member.remove
A node dropped from the raft configuration through the admin API. Joins are not recorded here: a node joins by starting up, so the record that matters is the leader’s, and it belongs to the node that admitted it.
cluster.snapshot.save
The whole replicated state leaving the cluster as a backup archive. Recorded because the archive holds every key hash, every sealed secret and every document. Who took a copy, and when, is a question an incident asks.
cluster.snapshot.restore
The replicated state being replaced from an archive: every key, budget, source and secret at once.
admin.auth
An admin request refused before it reached an operation. Successful authentication is not its own record: it is implied by the record of what the request then did.
admin.policy.load
An admin authorization policy becoming effective. It is the only record of what the gateway is enforcing rather than of a decision it made: every admin.authz names the policy that decided, but a name means nothing without knowing which document it came from, and the document is startup-owned so nothing else in the trail marks when it changed.
admin.authz
An admin request refused by policy. It is a separate event from admin.auth because the interesting fact is the opposite one: the caller is known, and a trail that filed “we know who you are and you may not” under the same name as “we have no idea who you are” would hide the more urgent of the two.
admin.probe
A run the admin API made for an operator that reads nothing stored: the MCP server/discover check, and the guardrails/inspect dry run. It is one of two reads that record what they allowed. The probe because its effect lands on somebody else: the bytes arrive at their server carrying the deployment’s credential, in their logs and against their rate limits, and a trail without this record cannot say who caused a request the peer saw. The inspection because it runs every configured detector, a remote analyzer included, over text the operator chose. What the record asserts is that an operator asked for the run reason names against target, and status is what they were told. Neither carries content.
admin.query
One SQL statement run over the list views. The other read that records itself, for the opposite reason: its effect lands on nobody, but the statement is a sentence somebody wrote, and the trail is the only place “who asked what” survives. A list endpoint answers the same question every time; a statement answers the one its author chose. target is the statement whole, and status is what SQLite made of it: 200 for an answer, 400 for a statement it refused, 422 for one that ran past the deadline.
request.auth
A data-plane request refused by authentication or by an access rule. Like admin.auth it records only refusals: a trail of every admitted request is the traffic log.
request.budget
A request refused because a budget could not cover it. Spend itself is metered, not audited. The trail carries the moment a limit stopped someone working, which is the event they will come asking about.
guardrail.decision
One guardrail inspection that decided something. An allow is not recorded, because the trail would then be the traffic log.
guardrail.uninspected
Content policy was configured to read and did not: too large to buffer, or in a shape it could not parse. It is recorded because the alternative is an audit trail whose silence reads as “nothing was found”.
guardrail.restore
A format-preserved value turned back into the original on its way to a destination declared to be inside the boundary. Every other guardrail event records something being hidden; this one records something being revealed, which is why it is its own event rather than a redaction with the sign flipped. The target is the server that received the real values.
audit.chain.start
The first record a node writes after it comes up: the public key every record after it is signed with, and in its reason whether the chain continues one this node kept on disk (“resumed”), starts one it will keep (“genesis”), or starts one that dies with the process (“ephemeral”). It is a record rather than a log line because a restart is a boundary a verifier has to account for, and the only way to account for it is to have it in the chain.
audit.chain.close
The last record a node writes when it stops on purpose. A chain that ends without one was cut short, by a crash or by whoever holds the file, and the reader is owed the difference.
Outcomes
Section titled “Outcomes”An outcome is what was decided. It is paired with enforced on a guardrail record, which says whether the decision was acted on. An async scan’s verdict is what policy would have done, and reads identically otherwise.
| Value | Meaning |
|---|---|
allowed |
The request or response proceeding untouched. |
denied |
A refusal. |
redacted |
Content rewritten before it travelled on. |
annotated |
A finding recorded and forwarded untouched: shadow mode. |
error |
The gateway failing to reach a decision. |
forwarded |
Content that travelled on unexamined. |
restored |
Content put back: the one outcome that hands out more than arrived. |
Reasons
Section titled “Reasons”A reason says why, in one token. This is the field a reader groups on (how many refusals last week were an expired key, how many were a budget), so it is closed, and closed the hard way. Every emitter names a constant, and a test refuses a literal written at a call site. It is never a sentence, never a client’s string and never a provider’s error, because a field that can hold prose is a field nobody can count.
What varies within a reason gets its own field rather than being spelled into the token. A write refused part-way names the write here and the half it fell at in stage, so every refused write of a source counts as one population and the ones that died in composition are a slice of it.
| Value | Meaning |
|---|---|
missing_key |
A request that presented no credential at all. |
unknown_key |
A credential no key matches. The token itself is never recorded: a mistyped one is very often a real one belonging somewhere else. |
revoked_key |
A key somebody revoked. The client is told only “invalid API key”. The difference from an unknown key lives here, where a prober cannot read it. |
expired_key |
A key past its expires_at. |
unknown_profile |
A key naming a profile the configuration no longer defines. It is the operator’s mistake rather than the caller’s, which is why it is a 403 naming the profile and not one of the 401s above. |
policy_denied |
An access rule that denied. The rule’s name is in rule: “denied by policy” without saying which policy sends the reader back to the config file to guess. |
policy_error |
An access rule that could not be evaluated, so the gateway refused rather than guess. Kept apart from policy_denied because a reader counting policy denials must not count the days a rule was broken; it carries error. |
budget_exceeded |
A limit that stopped somebody working. |
budget_error |
A ledger that could not answer. Separate because an outage is not a spending pattern. |
provider_suspended |
A provider out of service. |
profile_suspended |
Every key on a profile refused. |
key_suspended |
One key refused. |
mcp_suspended |
An MCP server out of service. |
a2a_suspended |
A peer agent out of service. |
principal_suspended |
A token whose caller (issuer and subject) is out of service. |
detector_failed |
An inspection that never reached a verdict. There is no result to record, which is the point of recording it. |
oversize |
Content policy was configured to read and did not, because it was too large to buffer. |
unparseable |
Content policy was configured to read and did not, because it was in a shape it could not walk. |
redact_after_forward |
A redact rule that fired on an overlap pass, after the request had already gone. Non-zero means a rule is not doing what its author wrote. |
invalid_token |
A bearer no trusted issuer verified. |
not_exchangeable |
A caller refused at an MCP server that acts as the caller: the credential presented was not a token the server’s issuer minted, so there was nothing to exchange. |
profile_claim |
A verified data-plane token whose profile could not be chosen: the claim the binding selects profiles from was withheld by the issuer or marked unverified. |
no_peer_credentials |
A local-socket caller the kernel would not name. |
loopback_only |
A remote caller on a deployment that trusts no issuer, and so can authenticate nobody who is not local. |
no_policy_permits |
The deny-by-default answer: no policy permitted the action. rule is empty, because nothing decided. |
forbidden_by_policy |
A forbid that matched, named in rule. |
no_policy |
A remote caller on a deployment that has loaded no admin policy at all. Distinct from no_policy_permits, which is a policy set that permitted nothing: here there is nothing to permit with, and the fix is to install a policy rather than to grant somebody a role. |
put |
A write that replaced or created. |
post |
A write that appended. |
delete |
A removal. |
keyring_add |
A wrapping added to the keyring. |
keyring_rotate |
A new data key generation. |
keyring_drop |
A wrapping removed. |
suspend |
Something taken out of service. |
unsuspend |
Something put back by hand, whether or not its lease had already lapsed. |
inspect |
A guardrails/inspect: an operator ran the compiled detectors over text of their own, under the profile the target names. The record says a run happened and what it answered; the text never enters the trail. |
discover |
An MCP server/discover: is this server reachable with the credential configured for it, and does it speak a protocol version this gateway accepts. |
revision_conflict |
A reload of a file the stored configuration has moved past. |
invalid_config |
A reload that would not build. The previous configuration is still serving. |
no_leader |
A write that met an election: there was nobody to accept it, nothing was written, and the same request works once a leader is elected. Kept apart from the refusals because it says nothing about what was asked. |
snapshot_saved |
A copy of everything leaving the cluster. |
snapshot_restored |
The replicated state replaced wholesale. |
restore_failed |
An archive that was refused. The error is in the log, not here. |
genesis |
A chain with no predecessor: this node has not written one before. |
resumed |
A chain continuing from a head this node kept. |
ephemeral |
A chain with nowhere to keep a head, so it can never be resumed and every restart begins again. |
stage is on a write that was refused, and empty everywhere else:
| Value | Meaning |
|---|---|
parse |
The document itself: malformed, or a key nothing reads. |
compose |
The document against the sources around it. The collision named sources exist to make loud. |
build |
The composed deployment against this node. |
store |
The write itself: a revision that moved, a document past the size limit, one source too many. |
unknown |
A refusal that named no stage. |
Actors
Section titled “Actors”An actor is who caused it. actor.kind says which of these a record’s actor is, and it decides how much the other actor fields can be trusted to mean.
| Value | Meaning |
|---|---|
oidc |
A caller who presented a JWT from a configured issuer. A person from a directory, or a workload from its platform: on either plane. Which one it was is in the actor’s issuer. |
loopback |
An unauthenticated caller on an issuerless loopback admin listener. It is a real actor kind rather than an absent one: “anyone with a local socket” is the honest name for that principal. |
local |
A caller on the local admin socket, named by the peer credentials the kernel recorded. It is distinct from loopback because it is the opposite in the one way that matters: this one has a uid, so the break-glass door is attributable to somebody. |
virtual_key |
Data-plane traffic presenting a virtual key. |
anonymous |
Data-plane traffic that presented no key. The inspector role sees this constantly: traffic on someone else’s API is not pistra’s to authenticate. |
system |
The gateway acting on its own behalf. A reload driven by SIGHUP, a scheduled scan reporting after the fact. |
A guardrail record also carries the schedule the inspection ran on:
| Value | Meaning |
|---|---|
sync |
The inspection the request waited for. |
overlap |
Ran concurrently with the upstream call and joined before the response was released. |
async |
Ran after the exchange was already answered. Its outcome is what policy would have done, never what it did: see enforced. |
The record
Section titled “The record”The canonical record is the audit trail’s wire format: the bytes that are hashed, signed, written to the local copy and carried as the OTLP body. The field order below is the order they are serialised in, and it is load-bearing. A hash is computed over these bytes, so renaming or reordering a field changes what every existing hash was computed over. Always marks the fields present on every record even when empty, because a reader distinguishing “no rule” from “field missing” cannot.
| Field | Type | Always | Meaning |
|---|---|---|---|
v |
number | yes | The record format version. It exists because the field order below is what every hash was computed over |
time |
string | yes | When the event happened; Auditor.Emit stamps it. |
event |
string | yes | What happened; see Events |
outcome |
string | yes | What was decided; see Outcomes |
actor |
object | yes | Who caused the event; see Actors |
target |
string | What the event acted on: the key name for key events, the route label for guardrail events, empty when the event has no object. | |
path |
string | The request path, on events that had a request. | |
rule |
string | Names the policy that decided, empty when nothing did and empty when several guardrail rules acted on one redaction. Naming one of several read as a complete attribution and was not, so the field answers “which rule” exactly when there is one and rules answers it the rest of the time. |
|
controls |
list of string | The governance control identifiers the deciding rule declared, in the rule’s order, and on a redaction that several rules acted on, the deduplicated union of all of theirs. They are what a governance platform maps a decision to; the gateway carries them and never reads them. Present exactly when an acting rule declared any. See rules for which rule cited what. |
|
policy_hash |
string | The hex SHA-256 of the whole policy document, on records about the document rather than about one decision. Paired with rule on the decision records around it, it says which text a named policy was at the time. |
|
reason |
object | Why, from the closed vocabulary above. It is never free text from a client, and never a provider’s error string. | |
stage |
object | How far a write got before it was refused, on the write events that have stages. Empty everywhere else, and on a write that worked. | |
side |
string | Which half of the exchange this judged: “request” or “response”. Guardrail events only. | |
status |
number | The HTTP status the caller received, when there was one. | |
spans |
number | How many spans a redaction rewrote, across every rule that acted. | |
rules |
list of object | One entry per guardrail rule that rewrote something, in rule order, with the spans it rewrote and the controls it cited. Redact rules accumulate, so one request carrying two entity classes is rewritten by two rules and has no single decider. Present on any redaction, so a reader never has to infer attribution from whether rule happens to be set. |
|
revision |
number | The configuration revision now serving, on reload. | |
source_revision |
number | The revision a configuration source write produced, on a successful config_source_write: the number the store’s history and the sources listing report for the same write, and the one thing that joins this record to them. revision cannot serve (it counts every rebuild, a minted key included) so a trail without this field can be joined to the store only by target and time. |
|
entities |
object | Counts findings by entity type. Types and counts only: the matched text is never recorded. | |
schedule |
string | When the inspection ran. Guardrail events only. | |
enforced |
boolean | States whether the outcome was acted on. It is false for every async record (nothing waited for that scan, so its verdict is what policy would have done) and it is a field rather than something derived from schedule because a reader who has to reconstruct “was this actually blocked?” will eventually get it wrong in the direction that flatters the trail. |
|
deferred |
list of string | Names detectors that contributed only what they already knew about this text from an earlier turn. Their findings are real; their silence is not evidence, because anything new in this body has not been seen by them yet. Carrying it is what keeps a record from implying a detector ran when it did not. | |
public_key |
string | The node’s audit signing key, standard base64. Chain start records only: it is what a verifier checks every following signature against, which is why it travels inside the signed chain rather than beside it. |
The actor object:
| Field | Meaning |
|---|---|
kind |
How the actor was identified. |
name |
The principal: the admin credential’s name, the virtual key’s name, or empty for kinds that have no name to give. |
addr |
The remote address the request arrived from, empty when the event had no request behind it. |
issuer |
Issuer names the identity provider that vouched for an OIDC caller, and is empty for every other kind. |
subject |
The claim that identified an OIDC caller, which Name does not repeat: a subject is stable and often unreadable (a directory’s opaque id, a SPIFFE ID) while Name is what a person reading the trail recognizes. Recording only the readable one would leave a record that cannot be tied back to the principal a policy names, and only the stable one leaves a trail nobody checks. |
The chain
Section titled “The chain”Each record is one link of a per-node hash chain, and the link is what pistra audit verify reads. The local copy writes an entry per line; an OTLP record carries the record as its body and these as attributes, so a verifier reads one format wherever the trail was captured.
| Field | Type | Meaning |
|---|---|---|
v |
number | The entry format version. |
node |
string | The chain’s node. |
kid |
string | Identifies the signing key: the first sixteen hex characters of the SHA-256 of the public key. Together with node it names one chain, which is what lets an ephemeral node restart at sequence one without colliding with its own previous life. |
seq |
number | This entry’s position in the chain, from one. |
prev |
string | The hex hash of the entry before this one. |
hash |
string | This entry’s hex hash. |
sig |
string | The standard-base64 signature over hash. |
record |
object | The canonical record, verbatim. |
OTLP attributes
Section titled “OTLP attributes”Audit records travel on their own instrumentation scope, pistra/audit, so a collector can route them somewhere a dropped record is noticed. The body is the canonical record; these attributes are duplicated out of it on purpose, because a collector filters on attributes and the body is opaque to it.
| Attribute | From |
|---|---|
audit.node |
the chain entry’s node |
audit.kid |
the chain entry’s kid |
audit.seq |
the chain entry’s seq |
audit.prev |
the chain entry’s prev |
audit.hash |
the chain entry’s hash |
audit.sig |
the chain entry’s sig |
event |
the record’s event; it is also the OTLP event name |
outcome |
the record’s outcome |
actor.kind |
the record’s actor.kind |
actor.name |
the record’s actor.name |
controls |
the deciding rule’s controls, present only when it declared any, so a governance platform can ask for every decision under a control without parsing bodies |
Regenerate with make gen-docs.