From 409543c41e78025354acbdde5ffc6445895d4508 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 20 Jun 2026 13:08:13 -0400 Subject: [PATCH] fix(fedimint): run fmcd with seccomp=unconfined so its DHT can start (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fmcd crash-looped "Operation not permitted (os error 1)" on .116 (kernel 6.12.74): the default rootless seccomp profile blocks a syscall its Mainline-DHT / iroh transport needs, so the REST API never came up (:8178 → HTTP 000) and federations couldn't be joined. Verified: with seccomp=unconfined fmcd boots and answers /v2/* (HTTP 401 instead of dead). fmcd works on other nodes, so this is kernel/seccomp-specific — but the relaxation is safe for an outbound-networking daemon and harmless where not needed. - new `security.seccomp_unconfined` manifest flag (SecurityPolicy); - libpod backend sets `seccomp_profile_path: "unconfined"` (== --security-opt seccomp=unconfined); quadlet backend emits `SeccompProfile=unconfined`; - enabled in apps/fedimint-clientd/manifest.yml. NOTE: manifests live on-disk at /opt/archipelago/apps//manifest.yml, so the node needs the updated manifest deployed + the fmcd container recreated to apply. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/fedimint-clientd/manifest.yml | 6 ++++++ core/archipelago/src/container/quadlet.rs | 7 +++++++ core/container/src/manifest.rs | 7 +++++++ core/container/src/podman_client.rs | 11 +++++++++++ 4 files changed, 31 insertions(+) diff --git a/apps/fedimint-clientd/manifest.yml b/apps/fedimint-clientd/manifest.yml index aba52999..73378311 100644 --- a/apps/fedimint-clientd/manifest.yml +++ b/apps/fedimint-clientd/manifest.yml @@ -42,6 +42,12 @@ app: # skip this whole manifest, so fmcd never ran and federations never joined.) # Lock down once the default federation's reachability model is finalized. network_policy: bridge + # fmcd's Mainline-DHT / iroh transport uses syscalls the default rootless + # seccomp profile blocks on some kernels (e.g. .116, kernel 6.12.74), where + # fmcd crash-loops "Operation not permitted (os error 1)" and never serves + # its REST API — so federations can't be joined (#7). Verified: with + # seccomp=unconfined fmcd boots and answers /v2/* (HTTP 401 vs dead 000). + seccomp_unconfined: true ports: # fmcd REST bound to 8080 in-container; 8080 collides with LND REST on the diff --git a/core/archipelago/src/container/quadlet.rs b/core/archipelago/src/container/quadlet.rs index 45ecfde7..0fc05c22 100644 --- a/core/archipelago/src/container/quadlet.rs +++ b/core/archipelago/src/container/quadlet.rs @@ -149,6 +149,9 @@ pub struct QuadletUnit { pub command: Vec, pub read_only_root: bool, pub no_new_privileges: bool, + /// Render `SeccompProfile=unconfined` — for daemons whose networking needs + /// syscalls the default rootless seccomp profile blocks (e.g. fmcd, #7). + pub seccomp_unconfined: bool, pub cpu_quota: Option, pub restart_policy: RestartPolicy, } @@ -252,6 +255,9 @@ impl QuadletUnit { if self.no_new_privileges { let _ = writeln!(s, "NoNewPrivileges=true"); } + if self.seccomp_unconfined { + let _ = writeln!(s, "SeccompProfile=unconfined"); + } if let Some(cpus) = self.cpu_quota { let _ = writeln!(s, "PodmanArgs=--cpus={cpus}"); } @@ -408,6 +414,7 @@ impl QuadletUnit { command: app.container.custom_args.clone(), read_only_root: app.security.readonly_root, no_new_privileges: app.security.no_new_privileges, + seccomp_unconfined: app.security.seccomp_unconfined, cpu_quota: app.resources.cpu_limit, restart_policy: RestartPolicy::OnFailure, } diff --git a/core/container/src/manifest.rs b/core/container/src/manifest.rs index 4b35e80f..4924cf8b 100644 --- a/core/container/src/manifest.rs +++ b/core/container/src/manifest.rs @@ -228,6 +228,13 @@ pub struct SecurityPolicy { pub network_policy: String, #[serde(default)] pub apparmor_profile: Option, + /// Run the container with `seccomp=unconfined`. Needed by daemons whose + /// networking uses syscalls blocked by the default rootless seccomp profile + /// on some kernels — e.g. fmcd's Mainline-DHT/iroh transport, which otherwise + /// crash-loops with "Operation not permitted (os error 1)" (#7). Opt-in only; + /// a mild relaxation, so reserve it for apps that genuinely need it. + #[serde(default)] + pub seccomp_unconfined: bool, } fn default_true() -> bool { diff --git a/core/container/src/podman_client.rs b/core/container/src/podman_client.rs index 35fcad36..8c75db86 100644 --- a/core/container/src/podman_client.rs +++ b/core/container/src/podman_client.rs @@ -384,6 +384,17 @@ impl PodmanClient { "nsmode": net_mode }, }); + // seccomp=unconfined for apps that need syscalls the default rootless + // profile blocks (e.g. fmcd's DHT) — libpod takes the literal "unconfined" + // as the profile path, mirroring `--security-opt seccomp=unconfined` (#7). + if manifest.app.security.seccomp_unconfined { + body.as_object_mut() + .expect("container create body is a JSON object") + .insert( + "seccomp_profile_path".to_string(), + serde_json::json!("unconfined"), + ); + } if let Some(network) = custom_network { body.as_object_mut() .expect("container create body is a JSON object")