Skip to content

Commit f18e2a0

Browse files
Mossakaclaude
andcommitted
fix: address review feedback on child container NAT enforcement
- Fix get_subcommand() parsing: match against known Docker subcommands instead of assuming first non-flag token is the subcommand, preventing misidentification of global option values (e.g., --context foo) - Hardcode agent container name 'awf-agent' in docker-stub.sh instead of reading from AWF_AGENT_CONTAINER env var to prevent namespace hijacking - Stop exporting AWF_REAL_DOCKER to user environment; write the real Docker path to /tmp/awf-lib/.docker-path file that only the wrapper reads - Add comment acknowledging that readonly AWF_DIND_ENABLED only protects the entrypoint shell, not subshells — real enforcement is the wrapper - Use AGENT_CONTAINER_NAME constant instead of string literal in docker-manager.ts - Block docker build/buildx commands to prevent BuildKit containers from bypassing NAT rules with unrestricted network access Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 07e71a4 commit f18e2a0

3 files changed

Lines changed: 65 additions & 10 deletions

File tree

containers/agent/docker-stub.sh

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,46 @@ fi
2929

3030
# --- DinD enabled: enforce shared network namespace ---
3131

32-
REAL_DOCKER="${AWF_REAL_DOCKER:-}"
32+
# SECURITY: Read the real Docker path from a private file rather than an environment
33+
# variable. Environment variables are visible to (and modifiable by) user code, which
34+
# could invoke the real Docker binary directly to bypass the wrapper.
35+
AWF_DOCKER_CONFIG="/tmp/awf-lib/.docker-path"
36+
if [ ! -f "$AWF_DOCKER_CONFIG" ]; then
37+
echo "ERROR: Docker wrapper config not found at $AWF_DOCKER_CONFIG" >&2
38+
exit 127
39+
fi
40+
REAL_DOCKER=$(cat "$AWF_DOCKER_CONFIG")
3341
if [ -z "$REAL_DOCKER" ] || [ ! -x "$REAL_DOCKER" ]; then
34-
echo "ERROR: AWF_REAL_DOCKER is not set or not executable: '$REAL_DOCKER'" >&2
42+
echo "ERROR: Real Docker binary not found or not executable: '$REAL_DOCKER'" >&2
3543
exit 127
3644
fi
3745

38-
AGENT_CONTAINER="${AWF_AGENT_CONTAINER:-awf-agent}"
46+
# SECURITY: Hardcode the agent container name to prevent user code from tampering
47+
# via the AWF_AGENT_CONTAINER environment variable to join arbitrary container namespaces.
48+
AGENT_CONTAINER="awf-agent"
49+
50+
# Known Docker subcommands we intercept or pass through.
51+
# We match against this list rather than assuming the first non-flag token is
52+
# a subcommand, because Docker global options can take values (e.g., --context foo,
53+
# -H unix:///...) which would be misidentified as subcommands.
54+
KNOWN_SUBCOMMANDS="run create exec build buildx pull push network compose images ps logs inspect rm rmi tag stop start restart kill pause unpause wait top stats attach commit cp diff export history import load save port rename update volume system info version events"
3955

40-
# Get the subcommand (first non-flag argument)
4156
get_subcommand() {
4257
for arg in "$@"; do
4358
case "$arg" in
4459
-*) continue ;;
45-
*) echo "$arg"; return ;;
60+
*)
61+
# Check if this token is a known Docker subcommand
62+
for cmd in $KNOWN_SUBCOMMANDS; do
63+
if [ "$arg" = "$cmd" ]; then
64+
echo "$arg"
65+
return
66+
fi
67+
done
68+
# Unknown token — could be a value for a global option (e.g., --context foo),
69+
# so skip it and keep looking.
70+
continue
71+
;;
4672
esac
4773
done
4874
}
@@ -101,6 +127,25 @@ case "$SUBCOMMAND" in
101127
exec "$REAL_DOCKER" "$CMD" --network "container:${AGENT_CONTAINER}" "${FILTERED_ARGS[@]}"
102128
;;
103129

130+
"build")
131+
# SECURITY: Block 'docker build' because BuildKit intermediate containers get their
132+
# own network namespace and are not subject to the agent's NAT/iptables rules.
133+
# This means build steps (e.g., RUN curl) could make unrestricted outbound requests,
134+
# bypassing the Squid proxy entirely.
135+
echo "ERROR: 'docker build' is blocked by AWF firewall." >&2
136+
echo "BuildKit containers bypass NAT rules and could make unrestricted network requests." >&2
137+
echo "Build images outside the AWF wrapper before invoking awf." >&2
138+
exit 1
139+
;;
140+
141+
"buildx")
142+
# SECURITY: Block 'docker buildx' for the same reason as 'docker build' above.
143+
echo "ERROR: 'docker buildx' is blocked by AWF firewall." >&2
144+
echo "BuildKit containers bypass NAT rules and could make unrestricted network requests." >&2
145+
echo "Build images outside the AWF wrapper before invoking awf." >&2
146+
exit 1
147+
;;
148+
104149
"compose")
105150
# For docker compose, we cannot easily rewrite compose files.
106151
# Block it to prevent spawning services on separate networks.

containers/agent/entrypoint.sh

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,12 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
520520
# Copy our wrapper to /tmp/awf-lib/docker (writable in chroot)
521521
if cp /usr/bin/docker /host/tmp/awf-lib/docker 2>/dev/null && \
522522
chmod +x /host/tmp/awf-lib/docker 2>/dev/null; then
523-
export AWF_REAL_DOCKER="$REAL_DOCKER_PATH"
523+
# SECURITY: Write the real Docker path to a private file instead of an env var.
524+
# This prevents user code from reading AWF_REAL_DOCKER to find and invoke the
525+
# real Docker binary directly, bypassing the wrapper.
526+
echo "$REAL_DOCKER_PATH" > /host/tmp/awf-lib/.docker-path
527+
chmod 444 /host/tmp/awf-lib/.docker-path
528+
AWF_REAL_DOCKER="$REAL_DOCKER_PATH" # local use only, not exported
524529
AWF_DOCKER_WRAPPER_INSTALLED=true
525530
echo "[entrypoint] Docker wrapper installed at /tmp/awf-lib/docker"
526531
echo "[entrypoint] Real docker binary: $REAL_DOCKER_PATH"
@@ -533,7 +538,10 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
533538
else
534539
echo "[entrypoint][WARN] Could not create /tmp/awf-lib for Docker wrapper"
535540
fi
536-
# Make AWF_DIND_ENABLED readonly to prevent tampering by user code
541+
# Make AWF_DIND_ENABLED readonly in this shell. Note: this does NOT propagate to
542+
# subshells or user code — shell readonly is per-process. The real security enforcement
543+
# is the Docker wrapper (docker-stub.sh) intercepting all docker commands via PATH
544+
# precedence, not this environment variable.
537545
readonly AWF_DIND_ENABLED
538546
fi
539547

@@ -719,9 +727,11 @@ AWFEOF
719727
if [ "$AWF_DOCKER_WRAPPER_INSTALLED" = "true" ]; then
720728
echo "# AWF Docker wrapper: enforce shared network namespace for child containers" >> "/host${SCRIPT_FILE}"
721729
echo "export PATH=\"/tmp/awf-lib:\$PATH\"" >> "/host${SCRIPT_FILE}"
722-
echo "export AWF_REAL_DOCKER=\"${AWF_REAL_DOCKER}\"" >> "/host${SCRIPT_FILE}"
730+
# SECURITY: AWF_REAL_DOCKER is NOT exported — the wrapper reads it from
731+
# /tmp/awf-lib/.docker-path instead, preventing user code from discovering
732+
# the real Docker binary path via the environment.
733+
# AWF_AGENT_CONTAINER is NOT exported — the wrapper hardcodes 'awf-agent'.
723734
echo "export AWF_DIND_ENABLED=\"1\"" >> "/host${SCRIPT_FILE}"
724-
echo "export AWF_AGENT_CONTAINER=\"${AWF_AGENT_CONTAINER:-awf-agent}\"" >> "/host${SCRIPT_FILE}"
725735
fi
726736

727737
# Append the actual command arguments

src/docker-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1007,7 +1007,7 @@ export function generateDockerCompose(
10071007
// Set DinD environment variables so the docker-stub wrapper enforces
10081008
// shared network namespace for child containers (prevents proxy bypass)
10091009
environment.AWF_DIND_ENABLED = '1';
1010-
environment.AWF_AGENT_CONTAINER = 'awf-agent';
1010+
environment.AWF_AGENT_CONTAINER = AGENT_CONTAINER_NAME;
10111011
logger.debug('Selective mounts configured: system paths (ro), home (rw), Docker socket exposed');
10121012
} else {
10131013
// Hide Docker socket to prevent firewall bypass via 'docker run'

0 commit comments

Comments
 (0)