Files
boxmaker/docs/plans/M4a/04-gatewayd-secrets.md
T
kyleandClaude Opus 5.5 0339dc13b2 Plan M4a: gatewayd in 15 tasks, with skeletons and given tests
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>
2026-09-23 19:05:44 -07:00

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 skeleton crates/gatewayd/src/secrets.rs
  • Append: docs/plans/M4a/files/runbook-gatewayd.md to the end of docs/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, then cp 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/ and cat docs/plans/M4a/files/runbook-gatewayd.md >> docs/runbook.md. Add pub mod secrets; to crates/gatewayd/src/lib.rs.
  • 2. See it fail. cargo test -p gatewayd --test secrets. Expected: it compiles and 8 tests fail.
  • 3. Fill value, then check_file, then load. cargo check -p gatewayd after each.
  • 4. See it pass. cargo test -p gatewayd --test secrets. Expected: 8 passed.
  • 5. Check the runbook. grep -c '^## ' docs/runbook.md is 7 more than before step 1, and sh scripts/check-runbook.sh prints nothing and exits 0.
  • 6. Run the gate. cargo fmt --all, then make 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 secrets passes; the seven entries are in docs/runbook.md; make gate prints gate: ok.

Stop and report if

  • A test needs the secret's value in an error message, or unsafe looks necessary.