Read and retain the audit log

Query the VTA audit log by time, action, actor, and outcome, page through large result sets, and set how long entries are kept.

By the end of this guide, you can answer “who did what on this VTA, and when” from the audit log, and set how long those records are kept.

A VTA writes an audit entry for most privileged operations, such as ACL, key, vault, and backup changes. Each entry holds the actor’s DID, the action, the context, and the outcome. Entries are written on a best-effort basis, and some operations, such as renaming or deleting a context, are not recorded. This guide covers querying that record with pnm audit and setting its retention period. For what the VTA considers a privileged operation in the first place, see Request model.

Without a way to read it, the audit trail only helps after someone has already exported it somewhere else. Reading it directly is what turns questions like “which DID released that secret last Tuesday” into a single query, and it is usually the first thing asked after a credential is suspected of leaking.

Use this guide when:

  • You need to establish what a particular DID did, and when.
  • A credential may have leaked and you want to see what was done with it.
  • An operation failed and you want the recorded outcome rather than a client-side guess.
  • You need to set or confirm how long this VTA keeps audit records.

Prerequisites

  • A running VTA. See Quickstart.

  • pnm installed and connected to the VTA.

    cargo install pnm-cli@0.23.1 --locked --registry crates-io
  • Admin access. A context-scoped admin can read their own contexts; an unrestricted admin can read across all of them. Changing the retention period needs an unrestricted admin.

Read the log

pnm --vta <vta-slug> audit list --context-id my-app

--context-id is required unless you are an unrestricted admin, which is the same scoping that applies everywhere else on a VTA. An empty result reports No audit log entries found.

Narrow the query

Each filter is an exact match, and they combine:

# What one DID did in a context (ACL changes carry no context, so a super-admin reads them without --context-id)
pnm --vta <vta-slug> audit list --context-id my-app --actor did:key:z6Mk...

# One kind of action, in a time window
pnm --vta <vta-slug> audit list --context-id my-app \
    --action key.create \
    --from   2026-09-01T00:00:00Z \
    --to     2026-10-01T00:00:00Z

# Only the operations refused with a given outcome
pnm --vta <vta-slug> audit list --context-id my-app --outcome denied:permissionDenied
FilterWhat it takes
--from / --toRFC 3339 timestamps. --from is inclusive, --to is exclusive.
--actionAn exact action name, such as acl.create or key.create.
--actorThe DID that performed the operation.
--outcomeAn exact outcome, such as success or denied:permissionDenied.
--context-idThe context to read. Required unless you are an unrestricted admin. audit list names this flag --context-id, while commands such as keys create use --context.

Filters match exactly. A refused operation is usually recorded with an outcome of the form denied:<code>, such as denied:permissionDenied, so list the entries without --outcome first and copy the exact outcome you want to filter on. Filtering on refusals is the fastest way to find access that was attempted and refused, which is what you usually want after a credential is suspected of leaking.

Page through a large result

The default page size is 50, and a larger --page-size is capped at 200. When more entries match than fit on a page, the command prints a continuation cursor:

pnm --vta <vta-slug> audit list --context-id my-app --page-size 200
pnm --vta <vta-slug> audit list --context-id my-app --page-size 200 --cursor <cursor-from-previous-page>

Pass the cursor back verbatim and keep every other filter identical. The cursor is bound to the filters and to your DID, so changing a filter mid-sweep, or reusing the cursor as a different DID, makes the VTA reject it with invalid pagination cursor.

Set how long entries are kept

# See the current period
pnm --vta <vta-slug> audit retention get

# Set it
pnm --vta <vta-slug> audit retention set --days 90

audit retention get prints the current period:

  Audit Retention
  Retention period: 90 days

audit retention set confirms the change:

  ✓ Audit retention updated to 90 days

Retention accepts 1 to 365 days. Choose it against whatever obligation you actually have to meet, and remember that the retention sweep picks up a new period after the VTA restarts. From then on it discards entries older than the new window.

Confirm

Test 1: a known action appears in the log

Mint a key, then look for it:

pnm --vta <vta-slug> keys create --key-type ed25519 --context my-app --label audit-check
pnm --vta <vta-slug> audit list --context-id my-app --action key.create

The entry for the key you just minted appears, naming your DID as the actor.

Test 2: a refused operation is recorded with a denied outcome

pnm --vta <vta-slug> audit list --context-id my-app --outcome denied:permissionDenied

Refused operations that change state, and refused vault operations, are recorded with a denied: outcome. A refusal appears under --context-id only when the request named that context, so as an unrestricted admin also run the query without --context-id. An empty result means nothing has been refused with that outcome in that context. List the context without --outcome to see every outcome recorded there.

Test 3: the retention period is what you set

pnm --vta <vta-slug> audit retention get

The period matches the value from “Set how long entries are kept”.

Troubleshooting

SymptomLikely causeFix
audit list refuses without a context--context-id is required for anyone who is not an unrestricted admin.Pass --context-id <id>, or run as a super-admin to read across contexts.
A time filter returns nothing--from and --to need RFC 3339 timestamps, and --to is exclusive.Use the full form, 2026-09-01T00:00:00Z, and set --to past the last moment you want included.
audit list --cursor fails with invalid pagination cursorA filter changed between pages, or a different DID reused the cursor.Restart from the first page with the filters you want, and keep them identical while paging.
An action you expected is missingThe action name is an exact match, or the entry predates the retention window.Confirm the exact action name from an unfiltered listing, then check the retention period.
Audit entries missing after a restoreThe backup was taken without --include-audit.Re-export with --include-audit for future backups. Entries not captured cannot be recovered.

Next steps

  Grant and revoke access: act on what the log shows by changing or removing a DID’s access.

  Back up and restore VTA state: include audit entries in an export, and what a restore leaves behind.

  Roles and access: why a denied entry happened, in terms of role and context.