Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
4.6 KiB
M4a task 04: secrets, and the runbook entries for gatewayd
Branch: m4a (run git switch m4a; git status --short must be empty, otherwise stop)
Commit subject: gatewayd: secrets from a credential, the environment or a file; runbook entries
Goal
gatewayd's one secret is the Mattermost token. It can come from three places, chosen per secret
in gatewayd.toml (spec section 4, P15 in the brief):
| Form | Reads | Refused when |
|---|---|---|
credential = "<name>" |
$CREDENTIALS_DIRECTORY/<name>, which systemd fills for a service with LoadCredentialEncrypted= |
the variable is unset, or the file cannot be read |
env = "<VAR>" |
the environment variable | unset, or not UTF-8 |
file = "<absolute path>" |
the file | not absolute; a symbolic link; not a regular file; not owned by the user gatewayd runs as; any group or other permission bit set |
In every form one trailing newline is removed, and an empty value is refused. A secret read from a
file is in plaintext on disk: gatewayd still starts, but load returns this warning for it,
exactly (<name> and <path> filled in, path.display()):
gatewayd: warning: secret <name> is read in plaintext from <path>; a systemd credential keeps it encrypted at rest (see docs/runbook.md#secret-in-a-file)
The value must never reach a log: Secret has no Display, its Debug prints Secret(…), it is
wiped when dropped (zeroize::Zeroizing), and expose() is the only way to the text.
This task also adds the runbook entries for every fail-closed state of gatewayd, all seven at
once, so that later tasks can point at them.
Files
- Copy:
crates/gatewayd/tests/secrets.rs, and the skeletoncrates/gatewayd/src/secrets.rs - Append:
docs/plans/M4a/files/runbook-gatewayd.mdto the end ofdocs/runbook.md - Modify:
crates/gatewayd/src/lib.rs(pub mod secrets;),docs/implementer-log.md
The skeleton
Written: Secret (with new, expose and its Debug), Loaded { secret, warning },
SecretError { name, why } and its Display (secret <name>: <why> then a newline and
see docs/runbook.md#secret-unavailable), and the constants RUNBOOK and RUNBOOK_FILE. To fill:
load, check_file and value.
load takes the environment as a function, env: &dyn Fn(&str) -> Option<OsString>, so that tests
can pass their own: read CREDENTIALS_DIRECTORY and env variables through it, never through
std::env. The uid of the user gatewayd runs as is the owner of /proc/self
(std::fs::metadata("/proc/self")?.uid(), from std::os::unix::fs::MetadataExt); no unsafe, no
libc.
The why of each error, for the reader: say what is wrong and never the value. The reference used
these, and you may too:
| Case | why |
|---|---|
| credential, variable unset | CREDENTIALS_DIRECTORY is not set: gatewayd was not started by systemd with LoadCredentialEncrypted= |
| credential, unreadable | cannot read the credential <path>: <error> |
| env unset | the environment variable <VAR> is not set |
| env not UTF-8 | the environment variable <VAR> is not UTF-8 |
| file checks | <path> is not an absolute path, cannot read <path>: <error>, <path> is a symbolic link, <path> is not a regular file, <path> is not owned by the user gatewayd runs as, <path> has mode <mode as {:03o}>; only the owner may read it (0600 or 0400) |
| the value | the value is not UTF-8, the value is empty |
Steps
- 1. Copy.
git switch m4a, thencp docs/plans/M4a/files/crates/gatewayd/tests/secrets.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/src/secrets.rs crates/gatewayd/src/andcat docs/plans/M4a/files/runbook-gatewayd.md >> docs/runbook.md. Addpub mod secrets;tocrates/gatewayd/src/lib.rs. - 2. See it fail.
cargo test -p gatewayd --test secrets. Expected: it compiles and 8 tests fail. - 3. Fill
value, thencheck_file, thenload.cargo check -p gatewaydafter each. - 4. See it pass.
cargo test -p gatewayd --test secrets. Expected: 8 passed. - 5. Check the runbook.
grep -c '^## ' docs/runbook.mdis 7 more than before step 1, andsh scripts/check-runbook.shprints nothing and exits 0. - 6. Run the gate.
cargo fmt --all, thenmake gate. Expected last line:gate: ok. - 7. Log and commit.
git add crates/gatewayd docs/runbook.md docs/implementer-log.md Cargo.lock && git commit
Done when
cargo test -p gatewayd --test secretspasses; the seven entries are indocs/runbook.md;make gateprintsgate: ok.
Stop and report if
- A test needs the secret's value in an error message, or
unsafelooks necessary.