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 records every privileged operation as it happens: the actor’s DID, the action, the context, and the outcome. 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.16.4 --locked --registry crates-io
  • Admin access. A context-scoped admin can read their own contexts; an unrestricted admin can read across all of them.

Read the log

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

--context 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:

# Everything one DID did in a context
pnm --vta <vta-slug> audit list --context my-app --actor did:key:z6Mk...

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

# Only the operations that were refused
pnm --vta <vta-slug> audit list --context my-app --outcome denied
FilterWhat it takes
--from / --toRFC 3339 timestamps. --from is inclusive, --to is exclusive.
--actionAn exact action name, such as auth.challenge or key.create.
--actorThe DID that performed the operation.
--outcomeAn exact outcome, such as success or denied.
--contextThe context to read. Required unless you are an unrestricted admin.

Filtering on --outcome denied 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 the maximum is 200. When more entries match than fit on a page, the command prints a continuation cursor:

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

Pass the cursor back verbatim and keep every other filter identical. Changing a filter mid-sweep invalidates the cursor, and the results after that point no longer answer the question you started with.

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 90
  Audit Retention
  Retention period: 90 days

Retention accepts 1 to 365 days. Choose it against whatever obligation you actually have to meet, and remember that shortening the period 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 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 as denied

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

Operations the VTA refused appear here rather than being silently dropped. An empty result means nothing has been denied in that context, which is itself worth knowing.

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 is required for anyone who is not an unrestricted admin.Pass --context <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.
Paging returns unexpected entries partway throughA filter changed between pages, which invalidates the cursor.Restart the sweep, keeping every filter identical and passing the cursor back verbatim.
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.