feat(security): move secret env out of podman inspect and Quadlet unit files
Secret env used to merge into manifest.app.environment, landing in
'podman inspect' Config.Env on the API backend and — worse — as
plaintext Environment= lines in Quadlet unit files on disk. Now:
- expand_and_partition_env (container crate, pure + tested) expands
${KEY} placeholders and splits env into plain entries and
secret-bearing pairs. Plain entries that interpolate a secret
(btcpay's Password=${BTCPAY_DB_PASS} connection strings) are
tainted and travel as secrets too. Secret values themselves are
never expanded (a generated value containing '${' passes verbatim).
- values register as podman secrets: stdin (never argv/tempfile),
--replace, content-hash label to skip no-op rewrites; a per-app hash
cache in the orchestrator makes steady-state reconciles free of
podman secret calls. Registration goes through the runtime trait
(default no-op keeps mocks/docker inert).
- containers reference secrets by name: secret_env map in the libpod
create spec, Secret=<name>,type=env,target=<KEY> in Quadlet units.
Verified empirically on fleet podman 5.4.2: value absent from
inspect Config.Env, runtime injection works rootless.
- rotation detection: io.archipelago.secret-env-hash container label
(API) / the changed unit bytes (Quadlet). Pre-upgrade containers
lack the label, so every secret-bearing app recreates ONCE on the
first reconcile after deploy — deliberate, it scrubs the plaintext
secrets out of existing container configs. Data dirs untouched.
- docker dev fallback keeps plain -e injection (no secret store);
podman secrets persist across uninstall, matching the
preserve-credentials invariant (reinstall re-registers by hash).
In-container /proc/<pid>/environ is unchanged — env remains the
app-compat contract; the closed leaks are inspect output and unit
files on disk.
Tests: archipelago-container 61/61 (3 new: taint partition, verbatim
secrets, hash order-independence), archipelago container:: 160/160
(fedimint install test now asserts the secret arrives as a ref, not
env; quadlet render test asserts Secret=/Label= lines). NEEDS the
on-node gate re-run before the item counts as verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
eed830e1ee
commit
4665e497d7
@@ -149,9 +149,21 @@ modules; production request/boot paths are essentially panic-free. The real risk
|
||||
public; zmq 28332/28333 should get the same look — unauthenticated). ⚠ May break
|
||||
external wallets pointed straight at nodeIP:8332 — needs a user call + on-node gate
|
||||
re-run, so NOT changed drive-by from the dev box.
|
||||
- [ ] 🟡 **Move generated secrets from env to file mounts.** `manifest.rs:1208-1226`
|
||||
injects secrets as `-e KEY=value`, readable via `podman inspect` / `/proc/<pid>/environ`.
|
||||
Prefer bind-mounting the existing `0600` secret file or `podman --secret`.
|
||||
- [x] 🟡 **Move secret env out of plaintext channels → podman secrets.** DONE 2026-07-05
|
||||
(code + unit tests; needs the on-node gate re-run before it counts as verified):
|
||||
secret env no longer merges into `environment` — it would land in `podman inspect`
|
||||
AND as plaintext `Environment=` lines in Quadlet unit files on disk (the worse leak).
|
||||
New pipeline: `expand_and_partition_env` taints plain entries that interpolate
|
||||
secrets (btcpay's `Password=${BTCPAY_DB_PASS}` connection strings travel as secrets
|
||||
too), values register as podman secrets (stdin, `--replace`, content-hash label,
|
||||
per-app cache so steady-state reconciles are podman-free), containers reference
|
||||
them via `secret_env` (API) / `Secret=…,type=env` (Quadlet). Verified empirically
|
||||
on fleet podman 5.4.2: value absent from inspect, runtime injection works. Rotation
|
||||
drift via `io.archipelago.secret-env-hash` container label; pre-upgrade containers
|
||||
lack the label → ONE-TIME recreate wave on first reconcile after deploy (by design —
|
||||
scrubs plaintext secrets from existing container configs). Docker dev fallback keeps
|
||||
plain env (no secret store). `/proc/<pid>/environ` inside the container is unchanged
|
||||
(env is the app-compat contract); the closed leaks are inspect output + unit files.
|
||||
- [x] 🟡 **Harden rate-limit IP extraction.** DONE 2026-07-03: the accept loop injects the
|
||||
TCP `PeerAddr` into request extensions; `extract_client_ip` honors
|
||||
`X-Real-IP`/`X-Forwarded-For` ONLY when the connection is from loopback (our nginx,
|
||||
|
||||
Reference in New Issue
Block a user