Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/v/0.38.0/llms.txt
Full corpus: https://docs.registrystack.org/v/0.38.0/llms-full.txt

# Read and ship the Evidence Gateway audit log

> Configure where Evidence Gateway writes its audit entries, ship them to append-only storage, understand what each phase establishes, and keep the subject-correlation boundary explicit.

Evidence Gateway writes one audit entry before every source read and one with every outcome, and
refuses the request when an entry cannot be written. The log is useful for showing which governed
operation was admitted and what concept was released. It does not repeat source values,
automatically provide a citizen-facing access history, or prove on its own host that nobody edited
it: tamper evidence comes from the append-only storage you ship it to.

{/* Evidence: products/evidence/OPERATOR-CONTRACT.md, "Audit destinations, rotation, and retention";
    crates/registry-platform-audit/src/writer.rs, AuditWriter and AuditEntry. */}

## Read one entry

Every entry is one JSON line with six members:

| Member | Meaning |
|---|---|
| `schema` | `registry.evidence.audit/v2`, `registry.evidence.audit.request-batch/v2`, or `registry.evidence.audit.authorization-refusal/v2`. |
| `eventId` | A random identifier the writer gives this entry. |
| `time` | The writer's UTC timestamp for the append. |
| `phase` | `request` for an access attempt, written before the source read it authorizes; `response` for a release, denial, or failure. |
| `correlation` | The server-minted operation identifier, shared by every entry of one operation. |
| `record` | The closed, minimized Evidence record. |

Entries are not chained, so any line can be read on its own. To list the entries in a file with
their phases and decisions, for example:

```sh
jq -c '{correlation, phase, event: .record.phase, decision: .record.decision}' \
  /var/lib/registry-evidence/audit/evidence.jsonl
```

Readers written for the chained `/v1` records do not read these entries; update them before routing
traffic to a runtime that writes `/v2`.

## Interpret one operation

For an admitted request, the record's own `phase` uses closed values:

| Record phase | What it establishes | What it does not establish |
|---|---|---|
| `access-attempt` with `authorized` | Authentication and the complete authority path were accepted before source credentials or source data were touched. | The source returned a record or the assertion was released. |
| `disclosure-release` with `released` | The named concepts were serialized and durably recorded before the exact response bytes were released. | The source fact was true or the caller used the answer correctly. |
| `denial` with `not-authorized` | An authenticated authorization refusal was durably recorded before its generic `403` response. | The rejected requirement, purpose, subjects, unmatched authority, selector information, or response format. |
| `denial` or `transient-failure` after authorization | An admitted operation later failed in one closed, value-free class. | The protected selector, source value, or credential that triggered it. |

Authentication, malformed-request, and invalid-selector failures happen before Evidence Gateway has
enough privacy-safe context and do not fabricate an audit entry. Do not expect every HTTP request
to produce two entries.

Authorized-material records can carry the governed requirement, purpose, package digest
(in the frozen `bundleRevision` field),
requester pseudonym, subject role and selector-profile pseudonym, source and adapter identifiers,
response protection, decision, safe failure class, disclosed concept IDs, evidence ID, and signing
key ID. A standalone authorization-refusal record instead carries a requester pseudonym and closed
denial reason without the rejected requirement, purpose, subjects, unmatched authority, selector
information, or response format. Records never carry the disclosed value, raw selector,
source response, access token, or credential.

## Choose a destination

The runtime file's `audit` block picks one of two destinations:

```yaml
audit:
  destination: file
  path: /var/lib/registry-evidence/audit/evidence.jsonl
  rotateBytes: 104857600
  retainDays: 90
```

- **`file`**, the default, returns from each append only after the entry is synced to disk. When
  the active file would pass `rotateBytes` (100 MiB by default), the writer renames it to
  `<path>.<sequence>` with an eight-digit sequence and opens a fresh file at `path`. When it opens
  and each time it rotates, it deletes sealed files last modified more than `retainDays` ago (90
  by default); it never deletes the active file.
- **`stdout`** writes each entry as one flushed line on standard output. The process writes its
  operational records to standard error, so the stream carries audit entries alone. Durability, rotation, and
  retention belong to your log collector, and `path`, `rotateBytes`, and `retainDays` are refused.
  `evidence check --require-audit-under` refuses this destination, because it has no local file to
  contain.

One process writes one file. The writer holds a lock on `<path>.lock` for its whole life, and a
second process pointed at the same path fails at startup with a sink-locked error. To run more
than one replica, give each its own `path`, for example on a per-replica volume, or use `stdout`
and let the platform's collector gather every stream.

The file's directory must belong to the service user and must not be writable by group or others.
Never rename, edit, or truncate the active file while the service runs: the writer treats that as
tampering, and readiness and every later audited request fail closed until the process restarts.
Copying a sealed file is safe at any time.

{/* Evidence: products/evidence/contracts/runtime.schema.yaml, audit;
    crates/registry-platform-audit/src/writer.rs, DEFAULT_AUDIT_ROTATE_BYTES,
    DEFAULT_AUDIT_RETAIN_DAYS, and AuditDestination. */}

## Ship audit to append-only storage

Ship sealed files, or the `stdout` stream, to storage the Evidence Gateway host cannot rewrite:
object storage with an object lock, a write-once archive, or a log pipeline whose retention the
Evidence operator account cannot change. Ship each sealed file before `retainDays` deletes it.
The writer records the next sequence in `<path>.seq` at every start and before every rotation, so
numbering continues after retention or a shipper removed every sealed file. It restarts only in a
directory that lost `<path>.seq`, such as a fresh or restored one, and every replica's stream uses
the same names, so key archived copies on more than the sealed file name.

If the process stopped mid-write, the active file ends in part of an entry. The next start copies
those bytes to the owner-only side file `<path>.torn`, truncates the active file to its last
complete line, and logs the side file's path and byte count at error level. Entries are
acknowledged only after they are synced, so no acknowledged entry is lost. Archive the side file
with the sealed files and remove it; the writer never overwrites it, so a second torn line refuses
to start until the first side file is moved.

{/* Evidence: crates/registry-platform-audit/src/writer.rs, write_next_sequence,
    AuditSegments::next_sequence, and recover_torn_tail. */}

Confirm that entries reached the store as part of backup, restore, and incident procedures.
Restore audit history into that store, never back into the live audit directory.

:::caution[Archive chained logs before switching]
A runtime that wrote the older chained audit format left `<path>.<sequence>` segments behind.
Retention deletes sealed files under the same name once they are older than `retainDays`. Before
the first start with the `audit` block, archive every old segment and the old active file to
append-only storage, and point `path` at a fresh file in a directory that holds no old segments.
:::

## Inspect the last local tutorial operation

The local tutorials expose one intentionally small view after the services stop:

```sh
evidencectl audit show --last-operation
```

```text
ACCESS AUTHORIZED age-bracket service-path-selection requester=<pseudonym>
DISCLOSURE RELEASED age_bracket
```

A standalone authorization refusal produces a one-event view:

```text
ACCESS REFUSED requester=<pseudonym> reason=not_authorized
```

An operation that ended after its access without a release prints the decision instead of a
release line: `DISCLOSURE DENIED reason=no_match` (or `ambiguous`, `unresolved`, `fact_missing`)
for a denial the evaluation decided, and `TRANSIENT FAILURE reason=dependency_failure` (or
`evaluation_failure`, `signing_failure`) for a failure a retry may clear.

The command takes the writer's lock, so it refuses while Evidence Gateway still runs. It reads the
retained local audit files in order, checks that every entry is well formed and that each
operation's access attempts, one per source call, share one context and pair with a coherent
terminal record, then renders the operation of the physically last entry. A multi-stage acquisition,
such as `search-then-fetch`, prints one `ACCESS AUTHORIZED` line per source call. Concurrent requests can complete in a different order from their admission order, so "last"
does not mean the most recently admitted request. The command is not a production audit browser
and has no `--all`, subject filter, or export mode. An access-only operation prints only the
authorized line rather than claiming a release that did not occur.

Request-batch entries are checked like every other entry but never shown. When the last operation
is a request batch, the command refuses with `evidence.audit.request-batch` rather than showing an
earlier operation as the last one. When other operations hold an access entry and no outcome, as a
request the process stopped during does, the view ends with a count and never their identities:

```text
EARLIER OPERATIONS WITHOUT AN OUTCOME count=1
```

A refusal names its cause where the cause changes what to do: `evidence.audit.writer-running`
while a process still holds the audit file, `evidence.audit.history-invalid` for an entry that is
not well formed, and `evidence.audit.unrecognized-outcome` for an outcome a mismatched
`evidencectl` version does not know. Every other failure is `evidence.audit.inspection-failed`.
The command never repeats what the `evidence` process wrote on standard error.

{/* Evidence: crates/registry-evidencectl/src/audit_view.rs, ShowArgs, render_unreleased, and
    CORE_NAMED_FAILURES; crates/registry-evidence/src/audit.rs, last_local_audit_operation and
    LocalAuditCollector::collect_request_batch; crates/registry-evidence/src/main.rs,
    local_audit_failure. */}

## Understand pseudonymous correlation

Requester and subject handles are deterministic keyed pseudonyms. They are protected correlators,
not anonymous values and not encrypted copies of identifiers.

The subject handle binds the complete selector tuple together with the trust domain, purpose,
audience, role, selector profile, and key version. The same tuple in the same scope can be
correlated by protected tooling. A different purpose, audience, or key version produces a
different handle. A composite selector cannot be reproduced from one field such as `person_id`
alone.

Registry Stack V1 does not expose supported tooling to answer:

```text
Show every access for person_id=person-123.
```

There is no subject-history query, citizen portal, pseudonym derivation command, requester-name
resolver, or cross-system history assembly. Do not work around this by exposing the audit hash key
or raw JSONL records. The log contains the privacy-preserving correlation primitive that future
protected tooling would need, but a citizen access request currently requires a separately
governed operator process.

## Protect the log

- Allow one Evidence Gateway writer per audit path.
- Put the path on durable storage and monitor capacity before a fail-closed write stops release.
- Ship sealed files, or the `stdout` stream, to append-only storage before retention deletes them.
- Retain each audit hash key for as long as pseudonyms from its version must stay recomputable.
- Restrict event access separately from ordinary service operations.
- Record retention, deletion, recovery, and requester-identity mapping responsibilities outside the
  Evidence Gateway bundle.

## Rotate the audit master

Every pseudonym carries the bundle's `audit.hashKeyVersion` in its `hmac-sha256:v<version>:`
prefix, so entries written under different key material stay distinguishable in one log and
rotation needs no fresh audit path. The runtime cannot tell a replacement master from the one it
replaces: new bytes under an unchanged `hashKeyVersion` silently change every pseudonym under the
same prefix. Rotate only this way:

1. Generate a fresh independent audit master into a new secret file.
2. Build a governed bundle revision whose `audit.hashKeyRef` names it and whose
   `audit.hashKeyVersion` is one higher, and record the change, the bundle revision, and both
   versions in the change record.
3. Run `evidence check --require-runtime-dependencies` and the full handoff checks, restart the
   service, and route traffic only after readiness succeeds.
4. Keep the previous master under its governed secret controls for as long as pseudonyms from its
   version must be recomputable for an investigation.

{/* Evidence: products/evidence/OPERATOR-CONTRACT.md, "Audit key rotation";
    products/evidence/contracts/bundle.schema.yaml, hashKeyVersion. */}

## Next

- [Retention and persistent state](../retention-and-persistent-state/) for the surrounding
  operator procedures.
- [Rotate credentials, keys, certificates, and trust](../advanced/rotate-credentials-and-trust/)
  to rotate the audit key alongside a deployment's other credentials.
- [Inspect and diagnose a running deployment](../advanced/inspect-and-diagnose/) to use this
  audit log while diagnosing a failing deployment.