feat(container): manifest-declared generated secrets + companion/quadlet hardening

Generated-secrets system: apps declare `generated_secrets` in their manifest
(kinds hex16/hex32/bcrypt); `container::secrets::ensure_generated_secrets`
materialises them 0600/rootless in resolve_dynamic_env — idempotent and
self-healing (recovers wrongly root-owned secrets with no privilege). Replaces
per-app Rust (deletes ensure_fmcd_password). fedimint-clientd/gateway manifests
now declare fmcd-password / fedimint-gateway-hash.

companion.rs: rebuild the auto-built :latest image when its build context changes
(staleness check) so baked-in fixes (e.g. guardian-UI CSS) actually reach nodes.

quadlet.rs: skip PublishPort under Network=host (podman rejects the combo, exit
125) + regression tests.

UI: "Fedimint Guardian" rename, fedimint-clientd/nostr-rs-relay/meshtastic tagged
as Services (headless backends), gateway icon fallback.

Deployed + verified on .228 (generated-secrets fixed fedimint-gateway start;
grafana/strfry orphan crash-loop units removed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-21 05:11:07 -04:00
co-authored by Claude Opus 4.8
parent db7d424bff
commit 03a4ee1b30
16 changed files with 521 additions and 52 deletions
+4 -30
View File
@@ -50,38 +50,12 @@ pub struct FederationRegistry {
const REGISTRY_FILE: &str = "wallet/fedimint_federations.json";
/// Shared HTTP-Basic password between the fmcd container and this bridge. The
/// fedimint-clientd manifest reads it via `secret_env: fmcd-password`, resolved
/// from `<data_dir>/secrets/`; the bridge reads the same file in `from_node`.
/// fedimint-clientd manifest generates it via `generated_secrets: [fmcd-password]`
/// and injects it through `secret_env`; the bridge reads the same file in
/// `from_node`. (Generation lives in `container::secrets`, not here — it's a
/// generic, manifest-declared concern, not fedimint-specific.)
const FMCD_PASSWORD_SECRET: &str = "fmcd-password";
/// Generate the fmcd Basic-auth password once, so the fmcd container
/// (`secret_env: fmcd-password`) and this bridge (`from_node`) agree on it.
/// Idempotent: a non-empty existing secret is left untouched. Mirrors the
/// bitcoin-rpc secret pattern (random hex, 0600). Called from the orchestrator's
/// `ensure_app_secrets` before the container's `secret_env` is resolved.
pub async fn ensure_fmcd_password(secrets_dir: &Path) -> Result<()> {
let path = secrets_dir.join(FMCD_PASSWORD_SECRET);
if let Ok(existing) = fs::read_to_string(&path).await {
if !existing.trim().is_empty() {
return Ok(());
}
}
fs::create_dir_all(secrets_dir)
.await
.context("creating secrets dir for fmcd password")?;
let bytes: [u8; 16] = rand::random();
let password = hex::encode(bytes);
fs::write(&path, &password)
.await
.context("writing fmcd password secret")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).await;
}
Ok(())
}
pub async fn load_registry(data_dir: &Path) -> Result<FederationRegistry> {
let path = data_dir.join(REGISTRY_FILE);
if !path.exists() {