# 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 = ""` | `$CREDENTIALS_DIRECTORY/`, which systemd fills for a service with `LoadCredentialEncrypted=` | the variable is unset, or the file cannot be read | | `env = ""` | the environment variable | unset, or not UTF-8 | | `file = ""` | 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 (`` and `` filled in, `path.display()`): ```text gatewayd: warning: secret is read in plaintext from ; 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 : ` 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`, 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 : ` | | env unset | `the environment variable is not set` | | env not UTF-8 | `the environment variable is not UTF-8` | | file checks | ` is not an absolute path`, `cannot read : `, ` is a symbolic link`, ` is not a regular file`, ` is not owned by the user gatewayd runs as`, ` has mode ; 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.