fix: harden container isolation in first-boot script (PENTEST-03)

Add --cap-drop ALL and --security-opt no-new-privileges:true to all
containers in first-boot-containers.sh that were missing it:
- Bitcoin Knots, LND, Fedimint, Fedimint Gateway (+ CHOWN/SETUID/SETGID)
- BTCPay Server, Home Assistant (+ CHOWN/SETUID/SETGID/DAC_OVERRIDE)
- Nextcloud (+ CHOWN/SETUID/SETGID/DAC_OVERRIDE)
- Grafana, Uptime Kuma, PhotoPrism, Ollama, Vaultwarden, FileBrowser
  (zero extra caps + --read-only + tmpfs for /tmp and /run)
- Jellyfin (zero extra caps)

Tailscale retains --privileged (required for TUN/iptables/routing).
SearXNG, OnlyOffice, Nginx Proxy Manager, Portainer already hardened.

The Rust RPC layer already applies equivalent hardening for all UI
installs; this brings the ISO first-boot path to parity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-11 14:40:04 +00:00
parent 89acc3ed5c
commit ec92e5e756
2 changed files with 32 additions and 1 deletions

View File

@@ -45,6 +45,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
log "Creating Bitcoin Knots..."
mkdir -p /var/lib/archipelago/bitcoin
if $DOCKER run -d --name bitcoin-knots --restart unless-stopped --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8332:8332 -p 8333:8333 \
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
docker.io/bitcoinknots/bitcoin:latest \
@@ -166,6 +168,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then
log "Creating BTCPay Server..."
mkdir -p /var/lib/archipelago/btcpay
$DOCKER run -d --name btcpay-server --restart unless-stopped --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 23000:49392 -v /var/lib/archipelago/btcpay:/datadir \
-e ASPNETCORE_URLS=http://0.0.0.0:49392 -e BTCPAY_PROTOCOL=http \
-e BTCPAY_HOST="$TARGET_IP:23000" -e BTCPAY_CHAINS=btc \
@@ -208,6 +212,8 @@ LNDCONF
log "LND config created (archy-net → bitcoin-knots:8332, rpcpolling)"
fi
$DOCKER run -d --name lnd --restart unless-stopped --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 9735:9735 -p 10009:10009 -p 8080:8080 \
-v /var/lib/archipelago/lnd:/root/.lnd \
docker.io/lightninglabs/lnd:v0.18.4-beta 2>>"$LOG" || true
@@ -218,6 +224,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint; then
log "Creating Fedimint..."
mkdir -p /var/lib/archipelago/fedimint
$DOCKER run -d --name fedimint --restart unless-stopped --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8173:8173 -p 8174:8174 -p 8175:8175 \
-v /var/lib/archipelago/fedimint:/data \
-e FM_DATA_DIR=/data -e FM_BITCOIND_USERNAME=archipelago -e FM_BITCOIND_PASSWORD=archipelago123 \
@@ -238,6 +246,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q '^lnd$' && [ -f "$LND_CERT" ] && [ -f "$LND_MACAROON" ]; then
log " LND detected — using lnd mode"
$DOCKER run -d --name fedimint-gateway --restart unless-stopped --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8176:8176 \
-v /var/lib/archipelago/fedimint-gateway:/data \
-v "$LND_CERT":/lnd/tls.cert:ro \
@@ -251,6 +261,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th
else
log " No LND found — using ldk (built-in Lightning)"
$DOCKER run -d --name fedimint-gateway --restart unless-stopped --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8176:8176 -p 9737:9737 \
-v /var/lib/archipelago/fedimint-gateway:/data \
docker.io/fedimint/gatewayd:v0.10.0 \
@@ -267,6 +279,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'homeassistant|home
log "Creating Home Assistant..."
mkdir -p /var/lib/archipelago/home-assistant
$DOCKER run -d --name homeassistant --restart unless-stopped \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 8123:8123 -v /var/lib/archipelago/home-assistant:/config \
-e TZ=UTC \
docker.io/homeassistant/home-assistant:2024.1 2>>"$LOG" || true
@@ -278,6 +292,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q grafana; then
mkdir -p /var/lib/archipelago/grafana
chown 472:472 /var/lib/archipelago/grafana 2>/dev/null || true
$DOCKER run -d --name grafana --restart unless-stopped \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 3000:3000 -v /var/lib/archipelago/grafana:/var/lib/grafana \
-e GF_PATHS_DATA=/var/lib/grafana -e GF_USERS_ALLOW_SIGN_UP=false \
docker.io/grafana/grafana:10.2.0 2>>"$LOG" || true
@@ -286,6 +303,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q uptime-kuma; then
log "Creating Uptime Kuma..."
mkdir -p /var/lib/archipelago/uptime-kuma
$DOCKER run -d --name uptime-kuma --restart unless-stopped \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 3001:3001 -v /var/lib/archipelago/uptime-kuma:/app/data \
-e TZ=UTC \
docker.io/louislam/uptime-kuma:1 2>>"$LOG" || true
@@ -294,6 +313,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q jellyfin; then
log "Creating Jellyfin..."
mkdir -p /var/lib/archipelago/jellyfin/config /var/lib/archipelago/jellyfin/cache
$DOCKER run -d --name jellyfin --restart unless-stopped \
--cap-drop ALL --security-opt no-new-privileges:true \
-p 8096:8096 \
-v /var/lib/archipelago/jellyfin/config:/config \
-v /var/lib/archipelago/jellyfin/cache:/cache \
@@ -303,6 +323,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q photoprism; then
log "Creating PhotoPrism..."
mkdir -p /var/lib/archipelago/photoprism
$DOCKER run -d --name photoprism --restart unless-stopped \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 2342:2342 -v /var/lib/archipelago/photoprism:/photoprism/storage \
-e PHOTOPRISM_ADMIN_PASSWORD=archipelago -e PHOTOPRISM_DEFAULT_LOCALE=en \
docker.io/photoprism/photoprism:latest 2>>"$LOG" || true
@@ -311,6 +333,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q ollama; then
log "Creating Ollama..."
mkdir -p /var/lib/archipelago/ollama
$DOCKER run -d --name ollama --restart unless-stopped \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 11434:11434 -v /var/lib/archipelago/ollama:/root/.ollama \
docker.io/ollama/ollama:latest 2>>"$LOG" || true
fi
@@ -318,6 +342,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q vaultwarden; then
log "Creating Vaultwarden..."
mkdir -p /var/lib/archipelago/vaultwarden
$DOCKER run -d --name vaultwarden --restart unless-stopped \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 8082:80 -v /var/lib/archipelago/vaultwarden:/data \
docker.io/vaultwarden/server:1.30.0-alpine 2>>"$LOG" || true
fi
@@ -325,6 +351,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nextcloud; then
log "Creating Nextcloud..."
mkdir -p /var/lib/archipelago/nextcloud
$DOCKER run -d --name nextcloud --restart unless-stopped \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 8085:80 -v /var/lib/archipelago/nextcloud:/var/www/html \
docker.io/library/nextcloud:28 2>>"$LOG" || true
fi
@@ -348,6 +376,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
log "Creating File Browser..."
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-db
$DOCKER run -d --name filebrowser --restart unless-stopped \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 8083:80 -v /var/lib/archipelago/filebrowser:/srv \
-v /var/lib/archipelago/filebrowser-db:/database \
docker.io/filebrowser/filebrowser:v2.27.0 2>>"$LOG" || true
@@ -377,6 +407,7 @@ fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q tailscale; then
log "Creating Tailscale..."
mkdir -p /var/lib/archipelago/tailscale
# Tailscale requires --privileged for TUN/iptables/routing table access
$DOCKER run -d --name tailscale --restart unless-stopped \
--network host --privileged \
--cap-add NET_ADMIN --cap-add NET_RAW \