The life of a credential¶
Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: Credential classes and the redaction filter.
How a credential is born, filed, delivered, proved, rotated and retired in this estate, and what checks each of those. Rotate secrets is the procedure for one rotation; this is the model behind it, and the page to read before minting anything new.
Read at nerthus.core.infra.common 7cb02cb, sejf/inventory.yaml at 18b0057 (2026-08-28).
The vault is a map, not a keyring¶
sejf/ is a committed, encrypted tree plus one inventory that is deliberately cleartext:
sejf/inventory.yaml, 1 118 lines, declaring 52 credentials and 131 holder rows. A holder
row says where one copy of one credential lives.
| Holder domain | Rows | What it means |
|---|---|---|
laptop |
44 | a file on a contributor's machine |
sejf |
27 | an entry in the encrypted tree itself |
gitlab_ci |
26 | a CI/CD variable on a project |
host_file |
16 | a file on a fleet host |
age_git |
13 | an age-encrypted file committed to a fleet repository |
cf_worker |
4 | a Cloudflare Worker secret |
git_history |
1 | a value that was committed once and is therefore permanent |
17 rows say THE ONLY COPY. Losing that machine loses that credential, and the recovery for
each is minting a new one rather than reading it back from somewhere.
The names are cleartext by design. That is what lets a check run on every pipeline without holding a key, and it is the whole reason the map is worth keeping.
What checks what, and where each stops¶
Nine sejf:* jobs run in Nerthus.Core.Infra.Common's pipeline. Four of them are the lifecycle:
| Job | The question it answers |
|---|---|
sejf:verify |
does the tree agree with the estate - is every credential the map declares actually there, and is anything there that the map does not declare |
sejf:liveness |
does the credential still authenticate |
sejf:expiry |
when does it stop working, and who says so |
sejf:apply |
make the estate match the tree |
sejf:verify cannot see two holder domains, and the estate closed that with an offline check
rather than by widening it. It compares gitlab_ci and cf_worker against the live APIs and
reports host_file and laptop as unknown. sejf and age_git rows appeared in neither branch -
not compared, and not counted among the unknowns either, so they produced no row at all. Two offline
checks in Invoke-SejfSelftest.ps1 now assert that every such holder names a file that exists.
Neither needs a key.
sejf:liveness asks the credential, not the calendar, and it runs two controls every time or
exits without a verdict: its own token against a real endpoint, and a synthetic never-issued token
against the same path. The negative control is synthetic on purpose - it is the only proof that
dead is a reachable answer, it needs nothing from the estate, and it cannot rot on the day somebody
deletes the real dead credential.
A password is unprobeable by construction. A wrong password does not fail, it hangs: this
estate ran an SSH negative control that timed out instead of being refused, and the timeout read as
the answer. Anything that cannot be probed is reported as unprobed rather than assumed alive.
sejf:expiry separates a date somebody typed from a date somebody read. A stated expiry with no
issuer record behind it is policy-only - a number nothing has ever compared to anything - and it
is reported apart from a measured one. That distinction is not decoration: this estate has had a
credential whose policy date said it was good for another year and which was already dead.
sejf:apply never creates. It writes a new value over a name the estate already carries and
skips any name it does not, because creating one means minting or inventing a value, and that is a
person's act. So filing something new in the tree reddens the checker; it does not conjure a live
credential.
Its journal records the digest of what was written and never the value, so a reader can tell a retried rotation from a second, different one without the journal itself becoming a copy.
What they say today, and why the pipeline is green anyway¶
Measured on the scheduled run of 2026-08-28 at 17:08 UTC, pipeline 2800504765 on
Nerthus.Core.Infra.Common@main:
| Job | Verdict | Exit |
|---|---|---|
sejf:verify |
the tree and the estate agree | 0 |
sejf:liveness |
blind - 1 row unread. This is NOT a live estate | 3 |
sejf:expiry |
0 expired, 3 awaiting an operator act, 0 expiring | 2 |
sejf:keyring · sejf:keyring-epoch |
red | non-zero |
Four of those jobs are allow_failure: true, so the pipeline is green while they are red. That
is worth knowing before you read a green badge as a healthy vault. Only sejf and sejf:verdict
can colour it.
Two of the reds are the instruments working rather than failing:
sejf:expiryis red because three credentials are waiting on a person. The stateawaiting-operator-actwas carved out ofexpireddeliberately, and it stays red, because a state that went green on a ruling would be the map answering for the territory.sejf:livenessreportsblindrather than a pass when a row could not be read at all. Nine rows authenticating and one unread is not nine-tenths of an answer; it is an answer with a hole in it, and the job says so and exits non-zero.
And every alive means authenticates, not works. An API answering 200 and a 403 from the one
repository a write-scoped token exists to push to are both consistent with a live token. The job
prints that sentence in its own log rather than leaving a reader to infer it.
What it costs¶
Nothing per run: every check above is text and API calls on a pipeline that already runs. The cost is at the other end. A credential in this estate is minted by a person, filed by a person, and its holder rows are maintained by a person - and a holder row that goes stale is invisible until something fails.
The blast radius of a lost laptop is 44 holder rows, 17 of which are the only copy.
The gap this page will not paper over¶
fleet:roster-audit exists and does not run. It is in Nerthus.Core.Infra.Common@main at
ci/fleet.gitlab-ci.yml:104, and it is not in v0.9.7, which is the newest tag and the ref
Nerthus.Core.Infra.Dev pins. main is 113 commits ahead of that tag, measured 2026-08-28.
A job that exists in a submodule nobody has pinned yet is not a check that runs. It starts running when the contract pin advances through a new release tag, and not before - The fleet owns how a contract release reaches a host.
See also¶
- Rotate secrets - the procedure for one rotation
- The secret tree - the vault's layout and how to read an entry
- Publish an age key - adding a recipient
- Set up credentials - the
.envevery script reads