fix(fedimint): run fmcd with seccomp=unconfined so its DHT can start (#7)

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/<id>/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) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-20 13:08:13 -04:00
parent d59cf6d299
commit 409543c41e
4 changed files with 31 additions and 0 deletions

View File

@ -42,6 +42,12 @@ app:
# skip this whole manifest, so fmcd never ran and federations never joined.) # skip this whole manifest, so fmcd never ran and federations never joined.)
# Lock down once the default federation's reachability model is finalized. # Lock down once the default federation's reachability model is finalized.
network_policy: bridge 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: ports:
# fmcd REST bound to 8080 in-container; 8080 collides with LND REST on the # fmcd REST bound to 8080 in-container; 8080 collides with LND REST on the

View File

@ -149,6 +149,9 @@ pub struct QuadletUnit {
pub command: Vec<String>, pub command: Vec<String>,
pub read_only_root: bool, pub read_only_root: bool,
pub no_new_privileges: 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<u32>, pub cpu_quota: Option<u32>,
pub restart_policy: RestartPolicy, pub restart_policy: RestartPolicy,
} }
@ -252,6 +255,9 @@ impl QuadletUnit {
if self.no_new_privileges { if self.no_new_privileges {
let _ = writeln!(s, "NoNewPrivileges=true"); let _ = writeln!(s, "NoNewPrivileges=true");
} }
if self.seccomp_unconfined {
let _ = writeln!(s, "SeccompProfile=unconfined");
}
if let Some(cpus) = self.cpu_quota { if let Some(cpus) = self.cpu_quota {
let _ = writeln!(s, "PodmanArgs=--cpus={cpus}"); let _ = writeln!(s, "PodmanArgs=--cpus={cpus}");
} }
@ -408,6 +414,7 @@ impl QuadletUnit {
command: app.container.custom_args.clone(), command: app.container.custom_args.clone(),
read_only_root: app.security.readonly_root, read_only_root: app.security.readonly_root,
no_new_privileges: app.security.no_new_privileges, no_new_privileges: app.security.no_new_privileges,
seccomp_unconfined: app.security.seccomp_unconfined,
cpu_quota: app.resources.cpu_limit, cpu_quota: app.resources.cpu_limit,
restart_policy: RestartPolicy::OnFailure, restart_policy: RestartPolicy::OnFailure,
} }

View File

@ -228,6 +228,13 @@ pub struct SecurityPolicy {
pub network_policy: String, pub network_policy: String,
#[serde(default)] #[serde(default)]
pub apparmor_profile: Option<String>, pub apparmor_profile: Option<String>,
/// 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 { fn default_true() -> bool {

View File

@ -384,6 +384,17 @@ impl PodmanClient {
"nsmode": net_mode "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 { if let Some(network) = custom_network {
body.as_object_mut() body.as_object_mut()
.expect("container create body is a JSON object") .expect("container create body is a JSON object")