morzer secret¶
Secrets live encrypted in secrets.sops.yaml and are rendered to tmpfs for the
product to read.
Values are never printed, never passed in argv, and never written to the
journal. domain.Secret renders as [redacted] through both String and
LogValue; a redacting log handler and the subprocess output scrubber are the
second and third lines of defence.
/etc/<product>/secrets.sops.yaml SOPS + age, encrypted at rest
↓
/run/<product>/secrets/* tmpfs, 0700 directory, 0400 files
↓
/run/secrets/* inside the container
Rendered configuration in /etc contains paths to secrets, never values.
secret list¶
Lists secret names, fingerprints and metadata — never values.
The fingerprint is what lets an operator confirm two machines hold the same value without either of them revealing it.
secret set¶
morzer secret set <name>
The value is read without echo from the terminal, or from stdin when it is
piped. There is no flag for the value: argv is world-readable through
/proc, so a credential passed that way is a credential published.
secret generate¶
morzer secret generate <name>
Generates a value using the generator the release declares for that secret in its secret schema.
| Flag | Meaning |
|---|---|
--kind |
Override the generator: password, hex, base64, uuid, age-key. |
--length |
Override the declared length. |
--alphabet |
Override the password alphabet. |
secret rotate¶
morzer secret rotate <name>
Generates a new value of the same shape and restarts only the services the release declares as depending on it — the difference between a blip and a full outage.
secret edit¶
morzer secret edit # every secret
morzer secret edit db_password session_key
Decrypts into a temporary file on tmpfs, opens $VISUAL or $EDITOR, and
re-encrypts what changed. Rotating a related group of credentials is one logical
change; doing it as several secret set calls is several decrypt-modify-encrypt
cycles and several chances to stop halfway.
The editor sees a plain mapping and nothing else:
db_password: the-current-value
session_key: the-current-value
Change a value to change it, delete a line to remove the secret, add a line to add one. Leaving without changes changes nothing, and only the services that declare a dependency on a secret you changed are restarted.
The encryption metadata is never in the file, so the envelope cannot be corrupted by editing it.
Where the plaintext lives, and for how long¶
This is the one place in the manager where a decrypted secret is written to a filesystem. What bounds it:
- The session lives in a
0700directory inside the tmpfs render directory, not/tmp— which is frequently not tmpfs, and where a crash would leave plaintext on a disk you believe is clean. - The whole directory is overwritten and removed when the editor exits, however it exits: a clean save, a non-zero exit, a signal, a panic. The directory rather than just the file, because editors leave swap and backup files beside the one they were handed.
- On tmpfs, overwriting is as final as it sounds — those bytes are pages of RAM.
On a disk-backed filesystem it is not, which is why
doctorreports a render directory that is not tmpfs.
Refusals¶
- No terminal. There is no sensible non-interactive editor session; use
secret set, which reads from stdin. - A file that does not parse. Nothing is written, and the message says so — an operator who has just lost an edit needs to know whether they also broke something.
- An empty value. Almost always a half-finished edit. Delete the line to remove a secret.
- Removing a secret the release declares
required, without--force.
$VISUAL is preferred over $EDITOR, the order git, crontab and sudoedit
use. A non-zero exit from the editor — :cq in vim — abandons the edit.
secret remove¶
morzer secret remove <name>
Deletes a secret. A secret the release declares as required will then be
reported missing by doctor.
secret render¶
Renders secrets to the tmpfs directory the product reads. apply does this as
one of its steps; running it directly is for recovering a /run that was
cleared by a reboot without a full converge.
secret recipients¶
Manages who can decrypt the secret state.
The state is always encrypted for at least two recipients: this machine, and an offline recovery key. Removing the last recipient, or the machine's own, is refused — either would produce a state nothing on the machine could read.
secret recipients list¶
Lists recipients, with their kind and comment.
secret recipients add¶
morzer secret recipients add <age-public-key>
Adds a recipient and re-encrypts the state for it.
| Flag | Meaning |
|---|---|
--kind |
Recipient kind: recovery or operator. Default operator. |
--comment |
Note recorded alongside the key. |
secret recipients remove¶
morzer secret recipients remove <age-public-key>
Removes a recipient and re-encrypts without it.
secret recipients generate-recovery-key¶
morzer secret recipients generate-recovery-key <path>
Generates an offline recovery identity, writes the private half to path at
mode 0400, and prints its public key on stdout — so it can be fed straight to
init --recovery-recipient.
Move it off the machine
A recovery key kept on the machine it is meant to recover protects nothing. The private half exists so a rebuilt machine can read the old state.
Why sops is a subprocess¶
sops is executed rather than imported. The library pulls in the AWS, GCP and
Azure KMS SDKs for a deployment that only ever uses age. It sits behind a port,
so the decision is reversible: replacing it is a new adapter, not a change to
the lifecycle layer.
Values reach it over stdin, never as arguments.