feat: add container security hardening and Fedimint setup wizard

Add --cap-drop=ALL, --security-opt=no-new-privileges:true to all
non-privileged containers. Per-app capability grants for apps needing
CHOWN/SETUID/SETGID. Read-only root filesystem with tmpfs for
compatible apps (searxng, grafana, uptime-kuma, filebrowser,
photoprism, vaultwarden). Add Fedimint "Create a Community" goal
with 4-step wizard. Fix deploy script cp -rf for audio directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-05 08:24:56 +00:00
parent da3bf44cdb
commit 0bc7251e22
14 changed files with 186 additions and 50 deletions

View File

@@ -122,6 +122,27 @@ impl RpcHandler {
run_args.push("--network=archy-net");
}
// Security hardening (skip for privileged containers like Tailscale)
let security_caps: Vec<String> = if !is_tailscale {
get_app_capabilities(package_id)
} else {
vec![]
};
let readonly_compatible = !is_tailscale && is_readonly_compatible(package_id);
if !is_tailscale {
run_args.push("--cap-drop=ALL");
run_args.push("--security-opt=no-new-privileges:true");
for cap in &security_caps {
run_args.push(cap);
}
if readonly_compatible {
run_args.push("--read-only");
run_args.push("--tmpfs=/tmp:rw,noexec,nosuid,size=256m");
run_args.push("--tmpfs=/run:rw,noexec,nosuid,size=64m");
}
}
// Create data directories if they don't exist
for volume in &volumes {
if let Some(host_path) = volume.split(':').next() {
@@ -776,6 +797,51 @@ fn is_valid_docker_image(image: &str) -> bool {
true
}
/// Per-app Linux capabilities needed beyond the default cap-drop=ALL.
/// Most apps need CHOWN/SETUID/SETGID for internal user switching.
fn get_app_capabilities(app_id: &str) -> Vec<String> {
match app_id {
// Apps that need user switching and file ownership changes
"nextcloud" | "homeassistant" | "home-assistant" | "btcpay-server" | "btcpayserver"
| "jellyfin" | "onlyoffice" | "onlyoffice-documentserver" | "portainer" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
"--cap-add=DAC_OVERRIDE".to_string(),
],
// Nginx Proxy Manager needs to bind low ports
"nginx-proxy-manager" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
"--cap-add=NET_BIND_SERVICE".to_string(),
],
// Bitcoin and Lightning need file ownership ops
"bitcoin" | "bitcoin-core" | "bitcoin-knots" | "lnd" | "fedimint" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
],
// Grafana runs as specific UID (472)
"grafana" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
],
// Minimal apps (searxng, filebrowser, uptime-kuma, etc.) need no extra caps
_ => vec![],
}
}
/// Apps safe to run with --read-only root filesystem.
/// These work correctly with volume mounts + tmpfs for /tmp and /run.
fn is_readonly_compatible(app_id: &str) -> bool {
matches!(
app_id,
"searxng" | "grafana" | "uptime-kuma" | "filebrowser" | "photoprism" | "vaultwarden"
)
}
/// Get app-specific configuration
/// Returns: (ports, volumes, env_vars, custom_command, custom_args)
fn get_app_config(