Skip to content

fix(kryphos): stop the audit log recording credential names in cleartext - #457

Merged
forkwright merged 1 commit into
mainfrom
fix/378-audit-log-credential-refs
Aug 21, 2026
Merged

fix(kryphos): stop the audit log recording credential names in cleartext#457
forkwright merged 1 commit into
mainfrom
fix/378-audit-log-credential-refs

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Closes #378

Post-#215, the audit log was the weaker link for the exact threat #215 named.

That issue stopped the fjall store revealing credential names. tamper.log kept writing them
verbatim, so an adversary with filesystem access and no passphrase learned the operator's credential
inventory — and from the operation sequence, their rotation and revocation cadence — without ever
touching the ChaCha20-Poly1305 AEAD. Two stores, one threat model, and only one of them defended.

One detail shows the protection was already at the wrong layer: LogEntryKind's manual Debug impl
has always printed <redacted> for this field. The type knew it was sensitive and kept it out of
logs, while writing it to disk in the clear.

The fix

The entry now carries a reference derived from the name under the vault key — the same treatment
Vault::lookup_key already gives names in the keyspace. Deterministic, so every entry touching one
credential shares a reference and its history stays followable; keyed, so a reader holding the file
and not the passphrase learns neither the name nor whether two vaults hold the same one.

A separate domain from lookup_key, deliberately. The two identifiers answer different questions
— one addresses a fjall record, the other groups audit entries — and a shared domain would make an
audit reference usable as a store key. That is a capability the audit log has no reason to hand
anyone.

The field is renamed credential_namecredential_ref. A field called credential_name
holding a digest is precisely the class of lie this repo's audits keep finding. Its doc also states
that koinon does not derive the reference: what the derivation is, and therefore who can reverse
it, belongs to the writer. koinon carries it and nothing more.

The test that documented the leak

vault_mutations_append_intact_tamper_log asserted the plaintext name round-tripped through the log.
It was, in effect, the leak's specification — and it would have gone on passing forever.

It now asserts the four properties that actually matter: the reference is not the name, is the
fixed-width derived form, is stable across one credential's three entries, and differs
between credentials. Stability and distinctness are the point of deriving rather than randomising —
without them an operator could not follow one credential's history.

the_audit_log_does_not_reveal_credential_names is the sibling of #215's
on_disk_fjall_contents_do_not_reveal_credential_name, scanning the raw log bytes for the name. Its
acceptance partner asserts the operation string is still present, so a pass means a protected
name rather than a log that recorded nothing.

Mutation-checked rather than assumed: reverting the writer to CompactString::from(name) fails both
tests.

What this does not do

#378 offered two routes — protect the name, or document the exception. This takes the first, so no
trust-model carve-out is needed and the README needs no change.

It also does not encrypt the operation (add/rotate/revoke/remove), which stays cleartext by
design: it carries no inventory information on its own, and an audit log that cannot be read at all
by its own operator is not an audit log. The cadence of operations remains visible; what is now
hidden is which credential each one concerns.

Verification

utilities/pre-push-verify.sh, CI-exact across all seven affected packages:

FMT_EXIT=0
CLIPPY_EXIT=0
TEST_EXIT=0
NESTED_WORKSPACE_EXIT=0
POLICY_SCRIPTS_EXIT=0
LOCKFILE_LOCKED_EXIT=0
Summary [255.878s] 1145 tests run: 1145 passed, 0 skipped

#215 stopped the fjall store revealing credential names. The tamper log kept
writing them verbatim, so post-#215 the audit log was the weaker link for the
exact threat that issue named: filesystem access without the passphrase. An
adversary with the log learned the operator's credential inventory, and from
the operation sequence their rotation and revocation cadence, without touching
the AEAD.

The entry now carries a reference derived from the name under the vault key,
the same treatment `lookup_key` gives names in the keyspace. Deterministic, so
every entry touching one credential shares a reference and its history stays
followable; keyed, so a reader with the file and not the passphrase learns
neither the name nor whether two vaults hold the same one.

A separate domain from `lookup_key`, deliberately. The two identifiers answer
different questions -- one addresses a fjall record, the other groups audit
entries -- and a shared domain would make an audit reference usable as a store
key, a capability the audit log has no reason to hand anyone.

The koinon field is renamed `credential_name` to `credential_ref`, because a
field called `credential_name` holding a digest is exactly the kind of lie this
repo's audits keep finding. Its doc says what it is and, more usefully, says
that koinon does not derive it: what the derivation is, and therefore who can
reverse it, belongs to the writer.

`vault_mutations_append_intact_tamper_log` asserted the plaintext name
round-tripped -- it documented the leak. It now asserts the reference is not
the name, is the fixed-width derived form, is stable across one credential's
three entries, and differs between credentials. `the_audit_log_does_not_reveal_credential_names`
is the sibling of #215's `on_disk_fjall_contents_do_not_reveal_credential_name`,
scanning the raw log bytes, with an acceptance partner asserting the operation
IS still recorded so the absence is a protected name rather than an empty log.

Mutation-checked: reverting the writer to the plaintext name fails both.

Closes #378
@forkwright
forkwright merged commit 84055c0 into main Aug 21, 2026
8 checks passed
@forkwright
forkwright deleted the fix/378-audit-log-credential-refs branch August 21, 2026 10:41
forkwright pushed a commit that referenced this pull request Aug 21, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.6.1](v0.6.0...v0.6.1)
(2026-08-21)


### Bug Fixes

* **kerykeion:** surface AES init failures during trial decryption
([#459](#459))
([6a0e514](6a0e514))
* **kryphos:** stop the audit log recording credential names in
cleartext ([#457](#457))
([84055c0](84055c0))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tamper-evident audit log records credential names in plaintext

1 participant