Skip to content

Audit Trail

Every modification to a vault appends a new signed entry rather than mutating an existing one. The vault file itself is the audit log. This page describes what gets recorded, how to query it via the CLI, and the limits of what append-only can prove.

Each entry in a vault is a JSONL line carrying signed metadata. The full schema is documented in Vault Format. The audit-relevant fields are:

FieldWhere it appearsMeaning
added_atidentity, secret, valueRFC 3339 timestamp asserted by the writer
signed_byidentity, secret, valueFingerprint of the GPG key that signed this entry
available_tovalue entriesFingerprints whose keys can decrypt this specific value
signatureidentity, secret, valueDetached GPG signature over the entry hash
hashidentity, secret, valueSHA-256/SHA-512 of the entry’s canonical form

Three operations write entries: identity add writes an identity entry; secret store writes a secret-definition entry the first time and a value entry every time; secret share and secret revoke write a new value entry with an updated available_to. secret forget writes a value entry with empty available_to and deleted: true.

Each entry’s signature covers added_at, signed_by, available_to, and the value, binding them together cryptographically. The timestamp is therefore signed and non-repudiable — a key holder cannot later deny writing an entry with that particular added_at. What the signature does not establish is when the entry actually landed in the shared vault: a key holder could sign with any added_at they choose.

Concretely:

  • Detected: an attacker who edits or deletes an existing entry in the vault file. Hash and signature verification fails on read, and dotsecenv validate flags it.
  • Detected: an attacker without a recipient’s private key adding entries. They can’t produce a valid signature.
  • Bounded: a legitimate key holder writing a fresh entry with a chosen added_at. The signature commits the signer to that timestamp but doesn’t prove it matches when the entry actually landed in the shared repo. Cross-reference the git commit that introduced the line.

There are three CLI/git surfaces for querying the history. Pick the one that matches your question.

To see who is currently authorized to decrypt each secret in each vault:

Terminal window
dotsecenv vault describe --json

Each secret entry includes an available_to array reflecting the most-recent value’s recipient list. Deleted secrets and secrets without values omit available_to.

[
{
"position": 1,
"vault": "~/.config/dotsecenv/vault",
"identities": [
{ "uid": "Alice <alice@example.com>", "fingerprint": "ALICE_FP", "algorithm": "RSA", "algorithm_bits": 4096, "created_at": "2026-01-04T08:14:32Z" }
],
"secrets": [
{ "key": "DATABASE_PASSWORD", "available_to": ["ALICE_FP", "BOB_FP"] },
{ "key": "OLD_TOKEN", "deleted": true }
]
}
]

To see every version of a single secret you can decrypt, with timestamps and authorship:

Terminal window
dotsecenv secret get DATABASE_PASSWORD --all --json

The output is an array of value entries, ordered newest-first, each with:

[
{
"added_at": "2026-04-12T10:22:01Z",
"value": "the-current-password",
"vault": "~/.config/dotsecenv/vault",
"available_to": ["ALICE_FP", "BOB_FP"],
"signed_by": "ALICE_FP"
},
{
"added_at": "2026-02-03T14:08:55Z",
"value": "the-previous-password",
"vault": "~/.config/dotsecenv/vault",
"available_to": ["ALICE_FP"],
"signed_by": "ALICE_FP"
}
]

The signed_by and available_to fields are returned only with --all. Plain secret get NAME --json returns just the current value, vault path, and timestamp. Note that --all only surfaces values your key can decrypt; rotated secrets that were never encrypted to your fingerprint are skipped with a warning.

signed_by tells you which key signed an entry; it does not tell you when the entry was committed to the shared repo or which git author landed it. For that, use git directly:

Terminal window
git log -p -- path/to/vault

Each commit shows the diff against the previous vault state. Combined with branch protection, signed commits, or required reviewers, this gives you a tamper-evident timeline that the in-vault added_at cannot.

Who can decrypt each non-deleted secret right now?

Section titled “Who can decrypt each non-deleted secret right now?”
Terminal window
dotsecenv vault describe --json \
| jq '.[].secrets[] | select(.deleted | not) | {key, available_to}'
Terminal window
dotsecenv secret get DATABASE_PASSWORD --all --json \
| jq '.[] | {added_at, signed_by, available_to}'

Who could decrypt a secret as of a past commit?

Section titled “Who could decrypt a secret as of a past commit?”
Terminal window
git checkout <past-commit> -- path/to/vault
dotsecenv vault describe --json | jq '.[].secrets[] | select(.key=="DATABASE_PASSWORD")'
git checkout HEAD -- path/to/vault
Terminal window
git log --format='%h %ai %an %s' -- path/to/vault

Find every commit that changed who can decrypt a secret

Section titled “Find every commit that changed who can decrypt a secret”
Terminal window
git log -p -- path/to/vault \
| grep -E '^\+\{"type":"value"' \
| jq -s '.[] | {added_at: .data.added_at, available_to: .data.available_to}'

This requires git’s diff output to include the JSONL change line. Vault files are designed to produce stable per-line diffs.

For continuous monitoring, the typical pattern is:

  1. Run dotsecenv vault describe --json and dotsecenv secret get NAME --all --json on a schedule (e.g., GitHub Actions on push).
  2. Diff the result against the previous run.
  3. Forward changes to your SIEM as structured events.

The GitHub Action guide shows the CI integration pattern; adapt the same pattern to your audit pipeline.