From 1201a1c681e268249a74a390b61a28faa00ee4ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:29:29 +0000 Subject: [PATCH 1/4] Initial plan From 0e4a47f4235202662453be581e5ca0d99c837b4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:35:17 +0000 Subject: [PATCH 2/4] feat(docker): propagate host.docker.internal DNS to spawned containers When --enable-host-access is used, containers spawned by the agent via docker run now also get host.docker.internal DNS resolution. Changes: - Pass AWF_ENABLE_HOST_ACCESS env var to agent container - Update docker-wrapper.sh to: - Allow --add-host host.docker.internal:host-gateway when enabled - Inject --add-host to spawned containers when enabled - Add unit tests for the new environment variable - Update documentation with new "How It Works" section Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- containers/agent/docker-wrapper.sh | 53 +++++++++++++++++++++++------- docs/usage.md | 21 ++++++++++++ src/docker-manager.test.ts | 16 +++++++++ src/docker-manager.ts | 6 ++++ 4 files changed, 85 insertions(+), 11 deletions(-) diff --git a/containers/agent/docker-wrapper.sh b/containers/agent/docker-wrapper.sh index 44d6cb40e..33efc54be 100644 --- a/containers/agent/docker-wrapper.sh +++ b/containers/agent/docker-wrapper.sh @@ -18,14 +18,31 @@ if [ "$1" = "run" ]; then network_value="" has_add_host=false has_privileged=false + has_unsafe_add_host=false declare -a args=("$@") for i in "${!args[@]}"; do arg="${args[$i]}" - # Check for --add-host flag - if [[ "$arg" == "--add-host="* ]] || [[ "$arg" == "--add-host" ]]; then + # Check for --add-host flag and validate it + if [[ "$arg" == "--add-host="* ]]; then has_add_host=true + add_host_value="${arg#--add-host=}" + # Only allow host.docker.internal:host-gateway when AWF_ENABLE_HOST_ACCESS is set + if [[ "$add_host_value" != "host.docker.internal:host-gateway" ]] || [[ "$AWF_ENABLE_HOST_ACCESS" != "true" ]]; then + has_unsafe_add_host=true + fi + elif [[ "$arg" == "--add-host" ]]; then + has_add_host=true + # Get next argument as the value + next_idx=$((i + 1)) + if [ $next_idx -lt ${#args[@]} ]; then + add_host_value="${args[$next_idx]}" + # Only allow host.docker.internal:host-gateway when AWF_ENABLE_HOST_ACCESS is set + if [[ "$add_host_value" != "host.docker.internal:host-gateway" ]] || [[ "$AWF_ENABLE_HOST_ACCESS" != "true" ]]; then + has_unsafe_add_host=true + fi + fi fi # Check for --privileged flag @@ -62,8 +79,8 @@ if [ "$1" = "run" ]; then exit 1 fi - # Block --add-host as it enables DNS poisoning attacks - if [ "$has_add_host" = true ]; then + # Block --add-host unless it's specifically host.docker.internal:host-gateway with AWF_ENABLE_HOST_ACCESS + if [ "$has_unsafe_add_host" = true ]; then echo "[$(date -Iseconds)] BLOCKED: --add-host enables DNS poisoning to bypass firewall" >> "$LOG_FILE" echo "[FIREWALL] ERROR: --add-host is not allowed (enables DNS poisoning)" >&2 echo "[FIREWALL] This flag can map allowed domains to unauthorized IPs" >&2 @@ -83,13 +100,27 @@ if [ "$1" = "run" ]; then # Build new args: docker run --network awf-net -e HTTP_PROXY -e HTTPS_PROXY shift # remove 'run' echo "[$(date -Iseconds)] INJECTING --network $NETWORK_NAME and proxy env vars" >> "$LOG_FILE" - exec /usr/bin/docker-real run \ - --network "$NETWORK_NAME" \ - -e HTTP_PROXY="$SQUID_PROXY" \ - -e HTTPS_PROXY="$SQUID_PROXY" \ - -e http_proxy="$SQUID_PROXY" \ - -e https_proxy="$SQUID_PROXY" \ - "$@" + + # Inject host.docker.internal if AWF_ENABLE_HOST_ACCESS is enabled and not already present + if [[ "$AWF_ENABLE_HOST_ACCESS" = "true" ]] && [ "$has_add_host" = false ]; then + echo "[$(date -Iseconds)] INJECTING --add-host host.docker.internal:host-gateway (AWF_ENABLE_HOST_ACCESS=true)" >> "$LOG_FILE" + exec /usr/bin/docker-real run \ + --network "$NETWORK_NAME" \ + --add-host host.docker.internal:host-gateway \ + -e HTTP_PROXY="$SQUID_PROXY" \ + -e HTTPS_PROXY="$SQUID_PROXY" \ + -e http_proxy="$SQUID_PROXY" \ + -e https_proxy="$SQUID_PROXY" \ + "$@" + else + exec /usr/bin/docker-real run \ + --network "$NETWORK_NAME" \ + -e HTTP_PROXY="$SQUID_PROXY" \ + -e HTTPS_PROXY="$SQUID_PROXY" \ + -e http_proxy="$SQUID_PROXY" \ + -e https_proxy="$SQUID_PROXY" \ + "$@" + fi else echo "[$(date -Iseconds)] --network $network_value already specified, passing through" >> "$LOG_FILE" fi diff --git a/docs/usage.md b/docs/usage.md index 43b81d9bd..1dc414adf 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -282,6 +282,15 @@ sudo awf \ -- curl http://host.docker.internal:8080 ``` +### How It Works + +When `--enable-host-access` is enabled: + +1. **Agent container**: Gets `host.docker.internal` DNS resolution via Docker's `extra_hosts` feature +2. **Spawned containers**: The docker-wrapper automatically injects `--add-host host.docker.internal:host-gateway` to any containers spawned by the agent + +This ensures that both the agent and any containers it spawns can resolve `host.docker.internal` to access services on the host machine. + ### Security Considerations > ⚠️ **Security Warning**: When `--enable-host-access` is combined with `host.docker.internal` in `--allow-domains`, containers can access **ANY service** running on the host machine, including: @@ -306,6 +315,18 @@ sudo awf \ -- 'copilot --mcp-gateway http://host.docker.internal:8080 --prompt "test"' ``` +### Docker-in-Docker with Host Access + +When spawning containers via `docker run` from within the agent, host access is automatically propagated: + +```bash +# Spawned containers can also reach host.docker.internal +sudo awf \ + --enable-host-access \ + --allow-domains host.docker.internal,registry-1.docker.io,auth.docker.io \ + -- 'docker run --rm curlimages/curl http://host.docker.internal:8080' +``` + ### CONNECT Method on Port 80 The firewall allows the HTTP CONNECT method on both ports 80 and 443. This is required because some HTTP clients (e.g., Node.js fetch) use the CONNECT method even for HTTP connections when going through a proxy. Domain ACLs remain the primary security control. diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 179496a3c..c313efb74 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -358,6 +358,22 @@ describe('docker-manager', () => { expect(agent.extra_hosts).toBeUndefined(); expect(squid.extra_hosts).toBeUndefined(); }); + + it('should set AWF_ENABLE_HOST_ACCESS environment variable when enableHostAccess is true', () => { + const config = { ...mockConfig, enableHostAccess: true }; + const result = generateDockerCompose(config, mockNetworkConfig); + const env = result.services.agent.environment as Record; + + expect(env.AWF_ENABLE_HOST_ACCESS).toBe('true'); + }); + + it('should NOT set AWF_ENABLE_HOST_ACCESS environment variable when enableHostAccess is false', () => { + const config = { ...mockConfig, enableHostAccess: false }; + const result = generateDockerCompose(config, mockNetworkConfig); + const env = result.services.agent.environment as Record; + + expect(env.AWF_ENABLE_HOST_ACCESS).toBeUndefined(); + }); }); it('should override environment variables with additionalEnv', () => { diff --git a/src/docker-manager.ts b/src/docker-manager.ts index e7eef8307..1e37089e5 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -325,6 +325,12 @@ export function generateDockerCompose( environment.AWF_SSL_BUMP_ENABLED = 'true'; } + // Pass host access flag to container for docker-wrapper.sh to inject into spawned containers + // This enables host.docker.internal DNS resolution in containers spawned by the agent + if (config.enableHostAccess) { + environment.AWF_ENABLE_HOST_ACCESS = 'true'; + } + // Add custom volume mounts if specified if (config.volumeMounts && config.volumeMounts.length > 0) { logger.debug(`Adding ${config.volumeMounts.length} custom volume mount(s)`); From b524543f0970943718c4a247bac63d44aa40d7ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:38:15 +0000 Subject: [PATCH 3/4] refactor(docker): extract add-host validation into function Address code review feedback: - Extract validate_add_host() function to reduce duplication - Track has_host_docker_internal separately from has_add_host - Remove extra blank line for consistent formatting Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- containers/agent/docker-wrapper.sh | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/containers/agent/docker-wrapper.sh b/containers/agent/docker-wrapper.sh index 33efc54be..d2a54ee07 100644 --- a/containers/agent/docker-wrapper.sh +++ b/containers/agent/docker-wrapper.sh @@ -6,6 +6,17 @@ NETWORK_NAME="awf-net" SQUID_PROXY="http://172.30.0.10:3128" LOG_FILE="/tmp/docker-wrapper.log" +# Validates an --add-host value +# Returns 0 if the value is allowed, 1 if it should be blocked +# Only host.docker.internal:host-gateway is allowed when AWF_ENABLE_HOST_ACCESS is set +validate_add_host() { + local value="$1" + if [[ "$value" == "host.docker.internal:host-gateway" ]] && [[ "$AWF_ENABLE_HOST_ACCESS" == "true" ]]; then + return 0 # Allowed + fi + return 1 # Blocked +} + # Log all docker commands echo "[$(date -Iseconds)] WRAPPER CALLED: docker $@" >> "$LOG_FILE" @@ -17,6 +28,7 @@ if [ "$1" = "run" ]; then has_network=false network_value="" has_add_host=false + has_host_docker_internal=false has_privileged=false has_unsafe_add_host=false declare -a args=("$@") @@ -28,8 +40,9 @@ if [ "$1" = "run" ]; then if [[ "$arg" == "--add-host="* ]]; then has_add_host=true add_host_value="${arg#--add-host=}" - # Only allow host.docker.internal:host-gateway when AWF_ENABLE_HOST_ACCESS is set - if [[ "$add_host_value" != "host.docker.internal:host-gateway" ]] || [[ "$AWF_ENABLE_HOST_ACCESS" != "true" ]]; then + if validate_add_host "$add_host_value"; then + has_host_docker_internal=true + else has_unsafe_add_host=true fi elif [[ "$arg" == "--add-host" ]]; then @@ -38,8 +51,9 @@ if [ "$1" = "run" ]; then next_idx=$((i + 1)) if [ $next_idx -lt ${#args[@]} ]; then add_host_value="${args[$next_idx]}" - # Only allow host.docker.internal:host-gateway when AWF_ENABLE_HOST_ACCESS is set - if [[ "$add_host_value" != "host.docker.internal:host-gateway" ]] || [[ "$AWF_ENABLE_HOST_ACCESS" != "true" ]]; then + if validate_add_host "$add_host_value"; then + has_host_docker_internal=true + else has_unsafe_add_host=true fi fi @@ -100,9 +114,8 @@ if [ "$1" = "run" ]; then # Build new args: docker run --network awf-net -e HTTP_PROXY -e HTTPS_PROXY shift # remove 'run' echo "[$(date -Iseconds)] INJECTING --network $NETWORK_NAME and proxy env vars" >> "$LOG_FILE" - # Inject host.docker.internal if AWF_ENABLE_HOST_ACCESS is enabled and not already present - if [[ "$AWF_ENABLE_HOST_ACCESS" = "true" ]] && [ "$has_add_host" = false ]; then + if [[ "$AWF_ENABLE_HOST_ACCESS" = "true" ]] && [ "$has_host_docker_internal" = false ]; then echo "[$(date -Iseconds)] INJECTING --add-host host.docker.internal:host-gateway (AWF_ENABLE_HOST_ACCESS=true)" >> "$LOG_FILE" exec /usr/bin/docker-real run \ --network "$NETWORK_NAME" \ From a27a2f8c3c0a12037e6279f315efd18096f7ae8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:52:32 +0000 Subject: [PATCH 4/4] fix(security): make AWF_ENABLE_HOST_ACCESS readonly to prevent tampering Address security concern from Security Guard review. Make the AWF_ENABLE_HOST_ACCESS environment variable readonly in entrypoint.sh to prevent malicious code from enabling host access when it wasn't authorized by the user via --enable-host-access flag. The attack scenario this prevents: 1. User runs awf WITHOUT --enable-host-access 2. Malicious code sets: export AWF_ENABLE_HOST_ACCESS=true 3. Malicious code spawns container with --add-host host.docker.internal:host-gateway 4. Previously this would bypass the restriction; now it's blocked because the readonly variable cannot be modified by user code. Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- containers/agent/entrypoint.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 2178d7297..84b52c55b 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -98,6 +98,13 @@ if [ -f /etc/resolv.conf ]; then echo "[entrypoint] DNS configured with Docker embedded DNS (127.0.0.11) and trusted servers: $DNS_SERVERS" fi +# Make security-sensitive environment variables read-only to prevent tampering by malicious code +# AWF_ENABLE_HOST_ACCESS controls host.docker.internal access in docker-wrapper.sh +# Setting it as readonly prevents user code from enabling host access when it wasn't authorized +readonly AWF_ENABLE_HOST_ACCESS="${AWF_ENABLE_HOST_ACCESS:-false}" +export AWF_ENABLE_HOST_ACCESS +echo "[entrypoint] Host access enabled: $AWF_ENABLE_HOST_ACCESS (readonly)" + # Update CA certificates if SSL Bump is enabled # The CA certificate is mounted at /usr/local/share/ca-certificates/awf-ca.crt if [ "${AWF_SSL_BUMP_ENABLED}" = "true" ]; then