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
@@ -382,12 +382,32 @@ impl PodmanClient {
|
||||
manifest.app.security.network_policy.as_str(),
|
||||
);
|
||||
|
||||
// Secret env travels by reference (podman injects the value at
|
||||
// start), so it never shows up in `podman inspect` output. The
|
||||
// combined content hash rides as a label for rotation-drift checks.
|
||||
let mut secret_env_map = serde_json::Map::new();
|
||||
for r in &manifest.app.container.secret_env_refs {
|
||||
secret_env_map.insert(
|
||||
r.env_key.clone(),
|
||||
serde_json::Value::String(r.secret_name.clone()),
|
||||
);
|
||||
}
|
||||
let mut labels_map = serde_json::Map::new();
|
||||
if let Some(hash) = &manifest.app.container.secret_env_hash {
|
||||
labels_map.insert(
|
||||
crate::manifest::SECRET_ENV_HASH_LABEL.to_string(),
|
||||
serde_json::Value::String(hash.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
let mut body = serde_json::json!({
|
||||
"name": name,
|
||||
"image": image_ref,
|
||||
"portmappings": port_mappings,
|
||||
"mounts": mounts,
|
||||
"env": env_map,
|
||||
"secret_env": secret_env_map,
|
||||
"labels": labels_map,
|
||||
"entrypoint": manifest.app.container.entrypoint.clone(),
|
||||
"command": manifest.app.container.custom_args.clone(),
|
||||
"hostadd": [
|
||||
|
||||
Reference in New Issue
Block a user