Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#!/bin/bash
set -e

print_banner() {
echo "[entrypoint] Agentic Workflow Firewall - Agent Container"
echo "[entrypoint] =================================="
}

setup_user_identity() {
# Adjust awfuser UID/GID to match host user at runtime
# This ensures file ownership is correct regardless of whether using GHCR images or local builds
HOST_UID=${AWF_USER_UID:-$(id -u awfuser)}
Expand Down Expand Up @@ -64,7 +67,9 @@ if [ "$CURRENT_UID" != "$HOST_UID" ] || [ "$CURRENT_GID" != "$HOST_GID" ]; then
chown -R awfuser:awfuser /home/awfuser 2>/dev/null || true
echo "[entrypoint] UID/GID adjustment complete"
fi
}

configure_dns() {
# Configure DNS to use only Docker's embedded DNS (127.0.0.11)
# Docker embedded DNS handles all name resolution:
# - Container names (e.g., squid-proxy) → resolved directly
Expand Down Expand Up @@ -100,7 +105,9 @@ if [ -f /etc/resolv.conf ]; then
echo "[entrypoint] DNS configured with Docker embedded DNS (127.0.0.11) only"
fi
fi
}

configure_ssl_certs() {
# 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
Expand All @@ -122,7 +129,9 @@ if [ "${AWF_SSL_BUMP_ENABLED}" = "true" ]; then
echo "[entrypoint][WARN] SSL Bump enabled but CA certificate not found"
fi
fi
}

wait_for_iptables() {
# Wait for iptables init container to complete setup
# The awf-iptables-init container shares our network namespace and runs
# setup-iptables.sh, then writes a ready signal file. This ensures the agent
Expand All @@ -145,7 +154,9 @@ while [ ! -f /tmp/awf-init/ready ]; do
INIT_ELAPSED=$((INIT_ELAPSED + 1))
done
echo "[entrypoint] iptables initialization complete"
}

check_service_health() {
# Run API proxy health checks (verifies credential isolation and connectivity)
# This must run AFTER iptables setup (which allows api-proxy traffic) but BEFORE user command
# If health check fails, the script exits with non-zero code and prevents agent from running
Expand All @@ -169,7 +180,9 @@ if [ -n "$AWF_CLI_PROXY_URL" ]; then
exit 1
fi
fi
}

configure_claude_api_key() {
# Configure Claude Code API key helper
# This ensures the apiKeyHelper is properly configured in the config files
# The config files must exist before Claude Code starts for authentication to work
Expand Down Expand Up @@ -258,7 +271,9 @@ if [ -n "$CLAUDE_CODE_API_KEY_HELPER" ]; then
chmod 777 "$SETTINGS_DIR" 2>/dev/null || true
write_api_key_helper "$SETTINGS_FILE" "$SETTINGS_FILE"
fi
}

configure_jvm_proxy() {
# Pre-seed JVM build tool proxy configuration
# Java build tools (Maven, Gradle, sbt) do not honor HTTP_PROXY/HTTPS_PROXY env vars
# and need explicit proxy configuration files
Expand Down Expand Up @@ -350,7 +365,9 @@ GRADLE_EOF
fi
export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:-} ${JVM_PROXY_FLAGS}"
fi
}

log_environment_details() {
# Print proxy environment
echo "[entrypoint] Proxy configuration:"
echo "[entrypoint] HTTP_PROXY=$HTTP_PROXY"
Expand All @@ -366,7 +383,9 @@ echo "[entrypoint] Hostname: $(hostname)"
runuser -u awfuser -- git config --global --add safe.directory '*' 2>/dev/null || true

echo "[entrypoint] =================================="
}

determine_capabilities_to_drop() {
# Determine which capabilities to drop
# - CAP_NET_ADMIN is NOT present (never granted to agent container - iptables setup
# is handled by the separate awf-iptables-init container)
Expand All @@ -382,6 +401,7 @@ else
CAPS_TO_DROP=""
echo "[entrypoint] No capabilities to drop (NET_ADMIN never granted to agent)"
fi
}

# Function to unset sensitive tokens from the entrypoint's environment
# This prevents tokens from being accessible via /proc/1/environ after the agent has started
Expand Down Expand Up @@ -467,13 +487,15 @@ run_agent_with_token_protection() {
exit $EXIT_CODE
}

log_execution_context() {
echo "[entrypoint] Switching to awfuser (UID: $(id -u awfuser), GID: $(id -g awfuser))"
echo "[entrypoint] Executing command: $@"
echo ""
}

run_chroot_command() {
# If chroot mode is enabled, run user command INSIDE the chroot /host
# This provides transparent host binary access - user command sees host filesystem as /
if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
echo "[entrypoint] Chroot mode: running command inside host filesystem (/host)"

# Mount a container-scoped procfs at /host/proc
Expand Down Expand Up @@ -1125,7 +1147,9 @@ AWFEOF
${LD_PRELOAD_CMD}
exec capsh --drop=${CAPS_TO_DROP} ${CAPSH_IDENTITY_ARGS} -- -c 'exec ${SCRIPT_FILE}'
"
else
}

run_non_chroot_command() {
# Original behavior - run in container filesystem
# Drop capabilities and privileges, then execute the user command

Expand Down Expand Up @@ -1161,4 +1185,25 @@ else
# No capabilities to drop - just switch to unprivileged user
run_agent_with_token_protection gosu awfuser "$@"
fi
}

main() {
print_banner
setup_user_identity
configure_dns
configure_ssl_certs
wait_for_iptables
check_service_health
configure_claude_api_key
configure_jvm_proxy
log_environment_details
determine_capabilities_to_drop
log_execution_context "$@"
if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
run_chroot_command "$@"
else
run_non_chroot_command "$@"
fi
}

main "$@"
93 changes: 93 additions & 0 deletions tests/entrypoint-phase-functions.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/bin/bash
set -e

ENTRYPOINT="$(dirname "$0")/../containers/agent/entrypoint.sh"

Comment on lines +1 to +5
if [ ! -f "${ENTRYPOINT}" ]; then
echo "❌ Cannot find entrypoint.sh at ${ENTRYPOINT}"
exit 1
fi

PASS=0
FAIL=0

pass() { echo "✓ $1"; PASS=$((PASS + 1)); }
fail() { echo "❌ FAIL: $1"; FAIL=$((FAIL + 1)); }

required_functions=(
print_banner
setup_user_identity
configure_dns
configure_ssl_certs
wait_for_iptables
check_service_health
configure_claude_api_key
configure_jvm_proxy
log_environment_details
determine_capabilities_to_drop
log_execution_context
run_chroot_command
run_non_chroot_command
main
)

for fn in "${required_functions[@]}"; do
if grep -Eq "^${fn}\(\) \{" "${ENTRYPOINT}"; then
pass "${fn}() is defined"
else
fail "${fn}() is not defined"
fi
done

if bash -n "${ENTRYPOINT}"; then
pass "entrypoint.sh passes bash syntax check"
else
fail "entrypoint.sh failed bash syntax check"
fi

MAIN_BLOCK="$(awk '
/^main\(\) \{/ { in_main=1; next }
in_main && /^}/ { in_main=0; exit }
in_main { print }
' "${ENTRYPOINT}")"

required_calls=(
'print_banner'
'setup_user_identity'
'configure_dns'
'configure_ssl_certs'
'wait_for_iptables'
'check_service_health'
'configure_claude_api_key'
'configure_jvm_proxy'
'log_environment_details'
'determine_capabilities_to_drop'
'log_execution_context "$@"'
)

last_line=0
for call in "${required_calls[@]}"; do
line_number="$(printf '%s\n' "${MAIN_BLOCK}" | grep -n -F "${call}" | cut -d: -f1 | head -1)"
if [ -z "${line_number}" ]; then
fail "main() does not call ${call}"
continue
fi
if [ "${line_number}" -le "${last_line}" ]; then
fail "main() calls ${call} out of order"
continue
fi
last_line="${line_number}"
pass "main() calls ${call} in order"
done

if printf '%s\n' "${MAIN_BLOCK}" | grep -Fq 'run_chroot_command "$@"' && \
printf '%s\n' "${MAIN_BLOCK}" | grep -Fq 'run_non_chroot_command "$@"'; then
pass "main() dispatches to chroot and non-chroot execution helpers"
else
fail "main() is missing chroot/non-chroot dispatch"
fi

echo ""
echo "Results: ${PASS} passed, ${FAIL} failed"

[ "${FAIL}" -eq 0 ]
Loading