diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b3f7cad6..6ab47f4b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,8 +99,9 @@ jobs: tags: | ghcr.io/${{ github.repository }}/agent:${{ steps.version_early.outputs.version_number }} ghcr.io/${{ github.repository }}/agent:latest - cache-from: type=gha - cache-to: type=gha,mode=max + # Disable cache for agent image to ensure security-critical packages + # (like libcap2-bin for capability dropping) are always freshly installed + no-cache: true - name: Sign Agent image with cosign run: | diff --git a/AGENTS.md b/AGENTS.md index 6d4a95b86..6d684bd8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -208,10 +208,11 @@ The codebase follows a modular architecture with clear separation of concerns: - Based on `ubuntu:22.04` with iptables, curl, git, nodejs, npm, docker-cli - Mounts entire host filesystem at `/host` and user home directory for full access - Mounts Docker socket (`/var/run/docker.sock`) for docker-in-docker support -- `NET_ADMIN` capability required for iptables manipulation +- `NET_ADMIN` capability required for iptables setup during initialization +- **Security:** `NET_ADMIN` is dropped via `capsh --drop=cap_net_admin` before executing user commands, preventing malicious code from modifying iptables rules - Two-stage entrypoint: 1. `setup-iptables.sh`: Configures iptables NAT rules to redirect HTTP/HTTPS traffic to Squid (agent container only) - 2. `entrypoint.sh`: Tests connectivity, then executes user command + 2. `entrypoint.sh`: Drops NET_ADMIN capability, then executes user command as non-root user - **Docker Wrapper** (`docker-wrapper.sh`): Intercepts `docker run` commands to inject network and proxy configuration - Symlinked at `/usr/bin/docker` (real docker at `/usr/bin/docker-real`) - Automatically injects `--network awf-net` to all spawned containers diff --git a/CLAUDE.md b/CLAUDE.md index c23c082da..eb5eb63fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,10 +179,11 @@ The codebase follows a modular architecture with clear separation of concerns: - Based on `ubuntu:22.04` with iptables, curl, git, nodejs, npm, docker-cli - Mounts entire host filesystem at `/host` and user home directory for full access - Mounts Docker socket (`/var/run/docker.sock`) for docker-in-docker support -- `NET_ADMIN` capability required for iptables manipulation +- `NET_ADMIN` capability required for iptables setup during initialization +- **Security:** `NET_ADMIN` is dropped via `capsh --drop=cap_net_admin` before executing user commands, preventing malicious code from modifying iptables rules - Two-stage entrypoint: 1. `setup-iptables.sh`: Configures iptables NAT rules to redirect HTTP/HTTPS traffic to Squid (agent container only) - 2. `entrypoint.sh`: Tests connectivity, then executes user command + 2. `entrypoint.sh`: Drops NET_ADMIN capability, then executes user command as non-root user - **Docker Wrapper** (`docker-wrapper.sh`): Intercepts `docker run` commands to inject network and proxy configuration - Symlinked at `/usr/bin/docker` (real docker at `/usr/bin/docker-real`) - Automatically injects `--network awf-net` to all spawned containers diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index 7762c614c..84fc70857 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -11,7 +11,8 @@ RUN apt-get update && \ dnsutils \ net-tools \ netcat-openbsd \ - gosu && \ + gosu \ + libcap2-bin && \ # Install Node.js 22 from NodeSource curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y nodejs && \ diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index e397e92fc..b1c4fed4b 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -149,10 +149,16 @@ fi runuser -u awfuser -- git config --global --add safe.directory '*' 2>/dev/null || true echo "[entrypoint] ==================================" -echo "[entrypoint] Dropping privileges to awfuser (UID: $(id -u awfuser), GID: $(id -g awfuser))" +echo "[entrypoint] Dropping CAP_NET_ADMIN capability and privileges to awfuser (UID: $(id -u awfuser), GID: $(id -g awfuser))" echo "[entrypoint] Executing command: $@" echo "" -# Drop privileges and execute the provided command as awfuser -# Using gosu instead of su/sudo for cleaner signal handling -exec gosu awfuser "$@" +# Drop CAP_NET_ADMIN capability and privileges, then execute the user command +# This prevents malicious code from modifying iptables rules to bypass the firewall +# Security note: capsh --drop removes the capability from the bounding set, +# preventing any process (even if it escalates to root) from acquiring it +# The order of operations: +# 1. capsh drops CAP_NET_ADMIN from the bounding set (cannot be regained) +# 2. gosu switches to awfuser (drops root privileges) +# 3. exec replaces the current process with the user command +exec capsh --drop=cap_net_admin -- -c "exec gosu awfuser $(printf '%q ' "$@")" diff --git a/docs/architecture.md b/docs/architecture.md index 2301f66a4..0e06ea963 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -78,10 +78,11 @@ The firewall uses a containerized architecture with Squid proxy for L7 (HTTP/HTT - Based on `ubuntu:22.04` with iptables, curl, git, nodejs, npm, docker-cli - Mounts entire host filesystem at `/host` and user home directory for full access - Mounts Docker socket (`/var/run/docker.sock`) for docker-in-docker support -- `NET_ADMIN` capability required for iptables manipulation +- `NET_ADMIN` capability required for iptables setup during initialization +- **Security:** `NET_ADMIN` is dropped via `capsh --drop=cap_net_admin` before executing user commands, preventing malicious code from modifying iptables rules - Two-stage entrypoint: 1. `setup-iptables.sh`: Configures iptables NAT rules to redirect HTTP/HTTPS traffic to Squid (agent container only) - 2. `entrypoint.sh`: Tests connectivity, then executes user command + 2. `entrypoint.sh`: Drops NET_ADMIN capability, then executes user command as non-root user - **Docker Wrapper** (`docker-wrapper.sh`): Intercepts `docker run` commands to inject network and proxy configuration - Symlinked at `/usr/bin/docker` (real docker at `/usr/bin/docker-real`) - Automatically injects `--network awf-net` to all spawned containers diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 9ba81e584..404638903 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -166,7 +166,10 @@ describe('docker-manager', () => { expect(depends['squid-proxy'].condition).toBe('service_healthy'); }); - it('should add NET_ADMIN capability to agent', () => { + it('should add NET_ADMIN capability to agent for iptables setup', () => { + // NET_ADMIN is required at container start for setup-iptables.sh + // The capability is dropped before user command execution via capsh + // (see containers/agent/entrypoint.sh) const result = generateDockerCompose(mockConfig, mockNetworkConfig); const agent = result.services.agent; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 31b1e0094..93ab6207f 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -304,7 +304,11 @@ export function generateDockerCompose( condition: 'service_healthy', }, }, - cap_add: ['NET_ADMIN'], // Required for iptables + // NET_ADMIN is required for iptables setup in entrypoint.sh. + // Security: The capability is dropped before running user commands + // via 'capsh --drop=cap_net_admin' in containers/agent/entrypoint.sh. + // This prevents malicious code from modifying iptables rules. + cap_add: ['NET_ADMIN'], // Drop capabilities to reduce attack surface (security hardening) cap_drop: [ 'NET_RAW', // Prevents raw socket creation (iptables bypass attempts)