Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 27, 2026

The agent container previously required NET_ADMIN capability at startup, then dropped it via capsh before executing user commands. This relied on runtime tooling for security enforcement rather than Docker's layer-based isolation.

Changes

Init container pattern

  • New agent-setup container with NET_ADMIN capability runs first, configures iptables, exits
  • Shares network namespace with agent via network_mode: service:agent
  • Agent container now starts without any capabilities
  • Agent depends on init container completion: service_completed_successfully

Simplified agent container

  • Removed packages: gosu, libcap2-bin, iptables
  • Entrypoint uses su instead of capsh --drop=cap_net_admin
  • No capability manipulation in runtime code

Type system

  • Added network_mode?: string to DockerService interface
  • Made networks optional to support namespace sharing

CI/CD

  • Added agent-setup image build/sign/attest to release workflow

Architecture

services:
  iptables-setup:
    network_mode: "service:agent"  # Share namespace
    cap_add: [NET_ADMIN]            # Only container with capability
    depends_on:
      - agent                        # Wait for agent to create namespace
  
  agent:
    # No cap_add - never has NET_ADMIN
    depends_on:
      iptables-setup:
        condition: service_completed_successfully

Security Impact

Defense-in-depth: agent container never receives NET_ADMIN capability. Iptables manipulation is impossible from user code execution context. Security boundary enforced at Docker layer, not runtime script.

Original prompt

This section details on the original issue you should resolve

<issue_title>Security: Separate privileged iptables setup from unprivileged command execution using Docker layers</issue_title>
<issue_description>## Summary

The current agent container architecture adds and drops CAP_NET_ADMIN capability within the same entrypoint script. While functionally secure, this pattern could be improved by separating privileged setup from unprivileged command execution at the Docker layer level.

Current Architecture

The current flow in containers/agent/entrypoint.sh:

  1. Container starts with NET_ADMIN capability
  2. entrypoint.sh runs as root and sets up iptables rules (line 115)
  3. entrypoint.sh drops CAP_NET_ADMIN using capsh --drop=cap_net_admin (line 144)
  4. gosu awfuser switches to unprivileged user
  5. User command executes
# Current: Single script handles both privileged setup AND privilege drop
exec capsh --drop=cap_net_admin -- -c "exec gosu awfuser $(printf '%q ' "$@")"

Concerns:

  • Same container layer handles both privilege escalation (iptables) and de-escalation (capability drop)
  • Relies on runtime tooling (capsh, gosu) to enforce security rather than Docker's built-in mechanisms
  • A bug or misconfiguration in the entrypoint could potentially skip the privilege drop

Proposed Solution: Init Container Pattern

Separate the privileged iptables setup into a dedicated init container, allowing the agent container to run without ever having NET_ADMIN capability.

Architecture

┌──────────────────────────────────────────────────────────────────────┐
│  awf-net (shared network namespace: 172.30.0.0/24)                  │
│                                                                      │
│  ┌─────────────────────┐      ┌─────────────────────────────────┐   │
│  │ awf-iptables-setup  │      │ awf-agent                       │   │
│  │ (init container)    │      │                                 │   │
│  │                     │      │                                 │   │
│  │ - NET_ADMIN cap     │      │ - NO capabilities               │   │
│  │ - Runs as root      │      │ - USER awfuser (Dockerfile)     │   │
│  │ - Sets up iptables  │      │ - Runs user command directly    │   │
│  │ - Exits on success  │      │                                 │   │
│  └─────────┬───────────┘      └─────────────────────────────────┘   │
│            │                              ↑                          │
│            │ depends_on:                  │                          │
│            │ service_completed_successfully                          │
│            └──────────────────────────────┘                          │
└──────────────────────────────────────────────────────────────────────┘

Docker Compose Changes

services:
  awf-iptables-setup:
    image: ghcr.io/githubnext/gh-aw-firewall/agent-setup:latest
    container_name: awf-iptables-setup
    network_mode: "service:awf-agent"  # Share network namespace
    cap_add:
      - NET_ADMIN
    environment:
      - SQUID_IP=172.30.0.10
      - SQUID_PORT=3128
    command: ["/usr/local/bin/setup-iptables.sh"]
    # Container exits after setup completes

  awf-agent:
    image: ghcr.io/githubnext/gh-aw-firewall/agent:latest
    container_name: awf-agent
    # NO cap_add - container never has NET_ADMIN
    user: "awfuser"  # Or via USER directive in Dockerfile
    depends_on:
      awf-iptables-setup:
        condition: service_completed_successfully
    command: ["user-command-here"]

Dockerfile Changes

# containers/agent/Dockerfile
FROM ubuntu:22.04

# ... existing package installation ...

# Create non-root user
RUN groupadd -g 1000 awfuser && \
    useradd -u 1000 -g 1000 -m -s /bin/bash awfuser

# ... existing setup ...

# Set user at Docker layer - container NEVER runs as root
USER awfuser

# Simple entrypoint - just run the command
ENTRYPOINT ["/bin/bash", "-c"]

Benefits

  1. Defense in depth: Agent container never has NET_ADMIN capability, even at startup
  2. Explicit security boundary: Docker layer (USER awfuser) enforces user, not runtime script
  3. Auditable: Security posture visible in Dockerfile and docker-compose.yml, not hidden in entrypoint logic
  4. Reduced attack surface: No capsh, gosu in the command execution path
  5. Simpler agent container: Entrypoint becomes trivial - just execute the user's command

Challenges to Address

  1. Network namespace sharing: Docker Compose's network_mode: "service:X" requires careful ordering
  2. UID/GID runtime adjustment: Currently handled in entrypoint.sh; may need alternative approach
  3. Two container images: Need to publish and version both agent-setup and agent images
  4. Backward compatibility: Existing users may have scripts expecting single-container behavior

Alternative: Simpler Two-Stage Entrypoint

If the init container pattern is too complex,...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits January 27, 2026 01:31
- Create separate agent-setup container for privileged iptables configuration
- Remove NET_ADMIN capability from agent container
- Update agent Dockerfile to remove gosu, libcap2-bin, iptables packages
- Modify entrypoint.sh to use su instead of capsh (no NET_ADMIN to drop)
- Add iptables-setup service to docker-compose generation
- Init container shares network namespace with agent via network_mode

Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
- Add tests verifying iptables-setup container configuration
- Add tests verifying agent container NO longer has NET_ADMIN
- Add tests for network namespace sharing via network_mode
- Update existing tests to reflect removal of NET_ADMIN from agent
- Add network_mode property to DockerService type
- Create init-container.test.ts integration test file

Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
- Build and push agent-setup init container image
- Sign agent-setup image with cosign
- Generate and attest SBOM for agent-setup image
- Tag with version number and latest

Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor Docker entrypoint for iptables setup separation feat: separate privileged iptables setup into init container Jan 27, 2026
Copilot AI requested a review from Mossaka January 27, 2026 01:37
@Mossaka
Copy link
Collaborator

Mossaka commented Feb 3, 2026

@copilot please merge origin/main

Merged latest changes from main which include:
- Chroot mode functionality (--enable-chroot)
- Organization migration from githubnext to github
- Various workflow and documentation updates

Resolved merge conflicts:
- containers/agent/entrypoint.sh: Updated to only drop SYS_CHROOT
  capability in chroot mode, since NET_ADMIN is never granted to agent
  (handled by init container)
- src/docker-manager.ts: Agent container only gets SYS_CHROOT when
  chroot is enabled, never NET_ADMIN
- src/docker-manager.test.ts: Updated tests to reflect init container
  pattern and organization change

Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Separate privileged iptables setup from unprivileged command execution using Docker layers

2 participants