You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This security review analyzed 6,506 lines of security-critical code across the AWF codebase, covering the full defense-in-depth stack: host-level iptables, container-level iptables, Squid proxy, Docker compose hardening, capability management, seccomp profile, the one-shot token library, and the API proxy sidecar.
Overall assessment: Secure architecture with strong defense-in-depth. No critical firewall bypass vulnerabilities were found. The system correctly layers multiple enforcement mechanisms. Several lower-severity hardening opportunities and two outdated dependencies (including one with known critical CVEs) are documented below.
Category
Status
Network firewall (host-level)
✅ Strong — catch-all REJECT, DOCKER-USER chain
Network firewall (container-level)
⚠️ ICMP gap at container layer (blocked at host)
Capability hardening
✅ SYS_ADMIN/SYS_CHROOT dropped before user code
Seccomp profile
⚠️ Allows mount/unshare (capabilities compensate)
AppArmor
⚠️ Disabled (unconfined) to allow procfs mount
Privilege drop
✅ capsh + gosu/chroot + no-new-privileges
Token protection
✅ One-shot-token + unset_sensitive_tokens()
Domain validation
✅ Regex-safe, wildcard depth limits
DNS exfiltration
✅ Blocked — Docker embedded DNS only, UDP default-deny
The agenticworkflows-logs tool was unable to retrieve prior firewall-escape-test run logs due to a git binary path issue in the sandbox environment. This review is therefore based entirely on static analysis of the current codebase.
🛡️ Architecture Security Analysis
Network Security — Two-Layer Enforcement
AWF implements a two-layer network enforcement model:
Layer 1 — Host-level (src/host-iptables.ts)
The FW_WRAPPER iptables chain is inserted into DOCKER-USER and applies to all egress from the container bridge. Evidence:
This catch-all REJECT blocks all traffic not explicitly allowed, including ICMP. UDP is explicitly blocked at line 515–519. Multicast and link-local (169.254.0.0/16, 224.0.0.0/4) are rejected at lines 488–505.
Inside the agent container, NAT rules redirect HTTP/HTTPS to Squid, and OUTPUT filter rules enforce a default-deny on TCP and UDP:
# setup-iptables.sh:405,407
iptables -A OUTPUT -p tcp -j DROP
iptables -A OUTPUT -p udp -j DROP
⚠️ Finding N-1 (Low): ICMP unfiltered at container level
The container-level OUTPUT chain only drops TCP and UDP — ICMP is never addressed. While host-level rules REJECT ICMP at the Docker bridge, a misconfigured or single-layer deployment (container without host rules) would leave ICMP unfiltered. Modern kernels allow unprivileged ICMP echo via ping sockets (without NET_RAW), so even with NET_RAW dropped, ping may work within the container.
The NET_RAW capability is correctly dropped in docker-manager.ts line 1159, blocking raw socket ICMP. However, defense-in-depth recommends an explicit container-level drop.
Container Security Hardening
Capability management is well-implemented:
// src/docker-manager.ts:1156-1164cap_add: ['SYS_CHROOT','SYS_ADMIN'],cap_drop: ['NET_RAW',// Prevents raw socket creation (iptables bypass attempts)'SYS_PTRACE',// Prevents process inspection/debugging (container escape vector)'SYS_MODULE',// Prevents kernel module loading'SYS_RAWIO',// Prevents raw I/O access'MKNOD',// Prevents device node creation],
SYS_ADMIN (required for procfs mount) and SYS_CHROOT are granted to the container but dropped before user code runs via capsh in chroot mode (always enabled):
AppArmor is disabled because Docker's default profile blocks the mount() syscall needed for procfs. This is a documented trade-off, but it removes an important MAC layer. If Docker's AppArmor profile could be customized (allow only the specific mount target needed), the container could run with AppArmor protection.
Seccomp Profile Assessment
The seccomp profile (containers/agent/seccomp-profile.json) uses SCMP_ACT_ERRNO as the default action — correctly deny-by-default. However, the broad allow list includes sensitive syscalls:
⚠️ Finding N-3 (Low): mount and unshare allowed in seccomp
Without SYS_ADMIN (dropped by capsh), mount() with filesystem types and unshare -m (new mount namespace) will fail with EPERM at the kernel level. However, unshare --user (user namespace creation) may succeed on Ubuntu 22.04 kernels if unprivileged_userns_clone=1. Creating a user namespace could allow privilege mapping that re-grants some capabilities within that namespace. This should be evaluated against the host kernel's sysctl settings.
The explicit ptrace, process_vm_readv, process_vm_writev block is a good addition:
Anchor-wrapped output: ^...$ anchoring prevents partial matches — line 130
The Squid config also blocks raw IPv4/IPv6 connections:
// src/squid-config.ts (near end of squid config generation)acldst_ipv4dstdom_regex^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$acldst_ipv6dstdom_regex^\[?[0-9a-fA-F:]+\]?$http_accessdenydst_ipv4http_accessdenydst_ipv6
This prevents agents from bypassing domain filtering by connecting to raw IP addresses.
Input Validation — Command Injection Assessment
The CLI uses commander for argument parsing. All Docker/Squid commands are executed via execa with argument arrays (not shell strings), which prevents shell injection:
// Pattern used throughout src/docker-manager.ts and src/host-iptables.tsawaitexeca('iptables',['-t','filter','-A',CHAIN_NAME, ...]);
The entrypoint script writes user commands to a temporary script file. The key section that writes the command uses printf '%s\n' "$3" (not eval), and requires exactly 3 arguments to avoid shell injection:
The trust comment is explicit: "Trust assumption: $3 comes from Docker CMD set by docker-manager.ts using the user's own --allow-domains command."
Token Protection Assessment
The one-shot-token library (containers/agent/one-shot-token/one-shot-token.c) intercepts getenv() calls, caches the value in memory, and calls unsetenv() on first access. Token names are XOR-obfuscated with key 0x5A to prevent discovery via strings.
The entrypoint runs the agent in the background and unsets tokens after 5 seconds:
# containers/agent/entrypoint.sh:676-682
chroot /host /bin/bash -c "... exec capsh ...\$\{SCRIPT_FILE}"&
AGENT_PID=$!
sleep 5
unset_sensitive_tokens
wait$AGENT_PID```**⚠️ Finding N-4 (Low): 5-second token inheritance window**During the 5 seconds between agent start and `unset_sensitive_tokens()`, any subprocess spawned by the agent that does NOT use the one-shot-token library will have full access to the unprotected environment variables via its own `/proc/self/environ`. The one-shot-token library protects the _agent process_ but not sub-processes loaded via `dlopen` or spawned tools that don't link against it.---## ⚠️ Threat Model (STRIDE)| Threat | Category | Evidence | Likelihood | Impact | Notes ||---|---|---|---|---|---|| Agent bypasses domain filter via raw IP | Spoofing | `dst_ipv4`/`dst_ipv6` ACLs in Squid | Low | High | Blocked by Squid ACL at squid-config.ts || Agent modifies iptables rules | Tampering | `NET_ADMIN` never granted to agent (docker-manager.ts:1148) | Very Low | Critical | Init container holds `NET_ADMIN` only during setup || Agent reads tokens via /proc/1/environ | Info Disclosure | one-shot-token.so + `unset_sensitive_tokens()` | Low | High | 5-second window remains || Agent exfiltrates data via DNS TXT records | Info Disclosure | UDP blocked after DNS allowlist (setup-iptables.sh:407) | Low | Medium | Only whitelisted DNS servers reachable || Agent uses ICMP tunneling | Info Disclosure | `NET_RAW` dropped; host-level catch-all REJECT | Low | Medium | Container level has gap; host layer compensates || Agent manipulates AppArmor/seccomp | Tampering | `no-new-privileges:true`; `SYS_ADMIN` dropped | Very Low | Critical | `seccomp` syscall is allowed but requires CAP || API proxy credential leakage | Info Disclosure | Proxy strips auth headers (server.js:28-43) | Low | Critical | Credentials never reach agent container || Squid JSONL log injection | Tampering | User-Agent omitted from JSONL (squid-config.ts comment) | Low | Low | URL field still unescaped; audit integrity risk || Container escape via user namespace | Elevation | `unshare` allowed in seccomp; SYS_ADMIN dropped | Low | Critical | Depends on kernel's `unprivileged_userns_clone` sysctl || DoS via large agent request body | DoS | API proxy limits body to 10 MB (server.js:27) | Medium | Low | Rate limiter also applied || Sensitive env exposure via JVM config | Info Disclosure | Maven/Gradle configs written before agent runs | Low | Low | No secrets written, only proxy host:port |---## 🎯 Attack Surface Map| Entry Point | File:Line | What it does | Current Protections | Risk ||---|---|---|---|---||`--allow-domains` CLI flag |`src/cli.ts`| Domain allowlist input |`validateDomainOrPattern()`, wildcardToRegex() | Low ||`--env` / `--env-all` CLI flags |`src/docker-manager.ts:494-510`| Env var passthrough to container | Base vars set first; excluded vars list | Medium || Docker CMD / command string |`src/docker-manager.ts`| User commandin agent container | chroot + capsh privilege drop | Medium || Squid ACL configuration |`src/squid-config.ts`| Domain filtering | Generated from validated domain list | Low || Agent container network |`containers/agent/setup-iptables.sh`| HTTP/HTTPS egress | Squid proxy + DNAT + default-deny TCP/UDP | Medium || API proxy sidecar |`containers/api-proxy/server.js`| API credential injection | Credential stripping; no auth from agent side | Low || iptables init container |`containers/agent/setup-iptables.sh`| Firewall rule setup | Separate from agent; NET_ADMIN isolated | Low || One-shot token library |`containers/agent/one-shot-token/`| Token delivery to agent |`getenv()` interception + unset| Low || Host iptables chain |`src/host-iptables.ts`| Host egress filtering | DOCKER-USER chain, catch-all REJECT | Low || Squid JSONL audit log |`src/squid-config.ts`| Machine-readable audit | URL field not JSON-escaped | Low |---## 📋 Evidence Collection<details><summary>Command: npm audit</summary>```
brace-expansion 4.0.0 - 5.0.4
Severity: moderate
brace-expansion: Zero-step sequence causes process hang and memory exhaustion
fix available via `npm audit fix`
handlebars 4.0.0 - 4.7.8
Severity: critical
- Handlebars.js has JavaScript Injection in CLI Precompiler (GHSA-xjpj-3mr7-gcpf)
- Handlebars.js has JavaScript Injection via AST Type Confusion (GHSA-xhpv-hc6g-r9c6)
- Handlebars.js has Denial of Service via Malformed Decorator Syntax (GHSA-9cx6-37pm-9jff)
- 3 additional CVEs
fix available via `npm audit fix`
2 vulnerabilities (1 moderate, 1 critical)
```Both packages are **devDependencies only** — not included in any container image or deployed artifact.</details><details><summary>Command: seccomp sensitive syscalls check</summary>```
defaultAction: SCMP_ACT_ERRNO (deny by default — correct)
Total syscall rules: 5
Allow rules: 2
Sensitive syscalls allowed: clone, mount, setgid, setuid, unshare
Explicitly blocked: ptrace, process_vm_readv, process_vm_writev, kexec_*, pivot_root, umount
mount and unshare are allowed but SYS_ADMIN is dropped before user code, making exploitation infeasible in practice.
# TCP and UDP explicitly dropped — lines 405,407
iptables -A OUTPUT -p tcp -j DROP
iptables -A OUTPUT -p udp -j DROP
# ICMP: no explicit rule — passes through container filter chain# (blocked at host level by FW_WRAPPER catch-all REJECT)
cap_add: ['SYS_CHROOT','SYS_ADMIN'],cap_drop: ['NET_RAW','SYS_PTRACE','SYS_MODULE','SYS_RAWIO','MKNOD'],// AWF_CHROOT_ENABLED = 'true' (hardcoded, line 707)// CAPS_TO_DROP = "cap_sys_chroot,cap_sys_admin" (entrypoint.sh:338)// capsh --drop drops caps from bounding set before user code executes
✅ Recommendations
🔴 High — Fix Now
H-1: Update vulnerable devDependencies
cd /home/runner/work/gh-aw-firewall/gh-aw-firewall
npm audit fix
handlebars@4.7.8 has 6 known CVEs (JavaScript injection, prototype pollution, DoS). While currently devDependencies only, these could be exploited in CI build pipelines if npm install runs in a context where attacker-controlled templates are processed. Update to latest.
🟡 Medium — Fix Soon
M-1: Add explicit ICMP DROP at container level (setup-iptables.sh)
The container OUTPUT chain drops TCP and UDP but not ICMP. While the host-level FW_WRAPPER catch-all REJECT handles this, defense-in-depth recommends an explicit container-level block. Add before the TCP/UDP DROP rules:
# Block ICMP (prevents ICMP tunneling even without NET_RAW)
iptables -A OUTPUT -p icmp -m limit --limit 5/min -j LOG --log-prefix "[FW_BLOCKED_ICMP] " --log-level 4
iptables -A OUTPUT -p icmp -j DROP
```**M-2: Evaluate custom AppArmor profile for agent container**Currently `apparmor:unconfined` to allow `mount()`for procfs. A custom AppArmor profile that only permits `mount` of procfs type (`proc`) at the exact target path would restore MAC protection while allowing the needed operation. Example:```mount fstype=proc -> /host/proc/,deny mount,
This is a higher-effort change but closes a meaningful isolation gap.
🟢 Low — Plan to Address
L-1: Evaluate blocking unshare with CLONE_NEWUSER flag in seccomp
If the host kernel has unprivileged_userns_clone=1, an unprivileged agent could create user namespaces to re-map capabilities. Consider adding a seccomp rule to block unshare with the CLONE_NEWUSER flag:
Or verify the host kernel has kernel.unprivileged_userns_clone=0.
L-2: JSON-escape URL field in JSONL audit log
From src/squid-config.ts, the JSONL log format comment notes: "Squid logformat does not JSON-escape strings, so fields like User-Agent could break JSON parsing." The url field (%ru) is still present in the JSONL format and can contain unescaped double-quotes or backslashes from agent-generated URLs, breaking audit log parsing. Consider switching to access_log_json_output on (Squid 5+) or post-processing logs.
L-3: Reduce the 5-second token inheritance window
The sleep 5 in entrypoint.sh before calling unset_sensitive_tokens() is a fixed window during which sub-processes inherit tokens. Consider reducing to 1 second, or using a process-readiness signal (e.g., wait for a specific log line or file) rather than a fixed delay. The one-shot-token library helps, but sub-processes that don't call getenv() bypass it.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
📊 Executive Summary
This security review analyzed 6,506 lines of security-critical code across the AWF codebase, covering the full defense-in-depth stack: host-level iptables, container-level iptables, Squid proxy, Docker compose hardening, capability management, seccomp profile, the one-shot token library, and the API proxy sidecar.
Overall assessment: Secure architecture with strong defense-in-depth. No critical firewall bypass vulnerabilities were found. The system correctly layers multiple enforcement mechanisms. Several lower-severity hardening opportunities and two outdated dependencies (including one with known critical CVEs) are documented below.
mount/unshare(capabilities compensate)unconfined) to allow procfs mountcapsh+gosu/chroot+no-new-privilegesunset_sensitive_tokens()handlebars@4.7.8(critical, devDeps);brace-expansion(moderate)🔍 Findings from Previous Security Test Runs
The
agenticworkflows-logstool was unable to retrieve prior firewall-escape-test run logs due to agitbinary path issue in the sandbox environment. This review is therefore based entirely on static analysis of the current codebase.🛡️ Architecture Security Analysis
Network Security — Two-Layer Enforcement
AWF implements a two-layer network enforcement model:
Layer 1 — Host-level (
src/host-iptables.ts)The
FW_WRAPPERiptables chain is inserted intoDOCKER-USERand applies to all egress from the container bridge. Evidence:This catch-all REJECT blocks all traffic not explicitly allowed, including ICMP. UDP is explicitly blocked at line 515–519. Multicast and link-local (
169.254.0.0/16,224.0.0.0/4) are rejected at lines 488–505.Layer 2 — Container-level (
containers/agent/setup-iptables.sh)Inside the agent container, NAT rules redirect HTTP/HTTPS to Squid, and OUTPUT filter rules enforce a default-deny on TCP and UDP:
# setup-iptables.sh:405,407 iptables -A OUTPUT -p tcp -j DROP iptables -A OUTPUT -p udp -j DROPThe container-level OUTPUT chain only drops TCP and UDP — ICMP is never addressed. While host-level rules REJECT ICMP at the Docker bridge, a misconfigured or single-layer deployment (container without host rules) would leave ICMP unfiltered. Modern kernels allow unprivileged ICMP echo via ping sockets (without
NET_RAW), so even withNET_RAWdropped, ping may work within the container.The
NET_RAWcapability is correctly dropped in docker-manager.ts line 1159, blocking raw socket ICMP. However, defense-in-depth recommends an explicit container-level drop.Container Security Hardening
Capability management is well-implemented:
SYS_ADMIN(required for procfs mount) andSYS_CHROOTare granted to the container but dropped before user code runs via capsh in chroot mode (always enabled):The
AWF_CHROOT_ENABLED=trueis hardcoded insrc/docker-manager.ts:707, ensuring the capability drop always fires.no-new-privileges:trueis set at line 1170, preventing privilege escalation via setuid binaries.unconfinedAppArmor is disabled because Docker's default profile blocks the
mount()syscall needed for procfs. This is a documented trade-off, but it removes an important MAC layer. If Docker's AppArmor profile could be customized (allow only the specificmounttarget needed), the container could run with AppArmor protection.Seccomp Profile Assessment
The seccomp profile (
containers/agent/seccomp-profile.json) usesSCMP_ACT_ERRNOas the default action — correctly deny-by-default. However, the broad allow list includes sensitive syscalls:# Sensitive syscalls allowed in seccomp: mount, unshare, clone, clone3, setuid, setgid, capset, prctlmountandunshareallowed in seccompWithout
SYS_ADMIN(dropped by capsh),mount()with filesystem types andunshare -m(new mount namespace) will fail with EPERM at the kernel level. However,unshare --user(user namespace creation) may succeed on Ubuntu 22.04 kernels ifunprivileged_userns_clone=1. Creating a user namespace could allow privilege mapping that re-grants some capabilities within that namespace. This should be evaluated against the host kernel's sysctl settings.The explicit
ptrace,process_vm_readv,process_vm_writevblock is a good addition:{ "names": ["ptrace", "process_vm_readv", "process_vm_writev"], "action": "SCMP_ACT_ERRNO", "errnoRet": 1 }Domain Validation Assessment
Domain validation in
src/domain-patterns.tsis robust:[a-zA-Z0-9.-]*(character class, not.*) — line 77*,*.*, patterns with >50% wildcard segments — lines 155–197^...$anchoring prevents partial matches — line 130The Squid config also blocks raw IPv4/IPv6 connections:
This prevents agents from bypassing domain filtering by connecting to raw IP addresses.
Input Validation — Command Injection Assessment
The CLI uses
commanderfor argument parsing. All Docker/Squid commands are executed viaexecawith argument arrays (not shell strings), which prevents shell injection:The entrypoint script writes user commands to a temporary script file. The key section that writes the command uses
printf '%s\n' "$3"(not eval), and requires exactly 3 arguments to avoid shell injection:The trust comment is explicit: "Trust assumption: $3 comes from Docker CMD set by docker-manager.ts using the user's own --allow-domains command."
Token Protection Assessment
The one-shot-token library (
containers/agent/one-shot-token/one-shot-token.c) interceptsgetenv()calls, caches the value in memory, and callsunsetenv()on first access. Token names are XOR-obfuscated with key0x5Ato prevent discovery viastrings.The entrypoint runs the agent in the background and unsets tokens after 5 seconds:
mountandunshareare allowed butSYS_ADMINis dropped before user code, making exploitation infeasible in practice.Command: iptables OUTPUT chain analysis (setup-iptables.sh)
Command: capability inspection (docker-manager.ts:1156-1164)
✅ Recommendations
🔴 High — Fix Now
H-1: Update vulnerable devDependencies
cd /home/runner/work/gh-aw-firewall/gh-aw-firewall npm audit fixhandlebars@4.7.8has 6 known CVEs (JavaScript injection, prototype pollution, DoS). While currently devDependencies only, these could be exploited in CI build pipelines ifnpm installruns in a context where attacker-controlled templates are processed. Update to latest.🟡 Medium — Fix Soon
M-1: Add explicit ICMP DROP at container level (
setup-iptables.sh)The container OUTPUT chain drops TCP and UDP but not ICMP. While the host-level
FW_WRAPPERcatch-all REJECT handles this, defense-in-depth recommends an explicit container-level block. Add before the TCP/UDP DROP rules:This is a higher-effort change but closes a meaningful isolation gap.
🟢 Low — Plan to Address
L-1: Evaluate blocking
unsharewithCLONE_NEWUSERflag in seccompIf the host kernel has
unprivileged_userns_clone=1, an unprivileged agent could create user namespaces to re-map capabilities. Consider adding a seccomp rule to blockunsharewith theCLONE_NEWUSERflag:{ "names": ["unshare"], "action": "SCMP_ACT_ERRNO", "args": [{"index": 0, "value": 268435456, "op": "SCMP_CMP_MASKED_EQ", "valueTwo": 268435456}] }Or verify the host kernel has
kernel.unprivileged_userns_clone=0.L-2: JSON-escape URL field in JSONL audit log
From
src/squid-config.ts, the JSONL log format comment notes: "Squid logformat does not JSON-escape strings, so fields like User-Agent could break JSON parsing." Theurlfield (%ru) is still present in the JSONL format and can contain unescaped double-quotes or backslashes from agent-generated URLs, breaking audit log parsing. Consider switching toaccess_log_json_output on(Squid 5+) or post-processing logs.L-3: Reduce the 5-second token inheritance window
The
sleep 5inentrypoint.shbefore callingunset_sensitive_tokens()is a fixed window during which sub-processes inherit tokens. Consider reducing to 1 second, or using a process-readiness signal (e.g., wait for a specific log line or file) rather than a fixed delay. The one-shot-token library helps, but sub-processes that don't callgetenv()bypass it.📈 Security Metrics
Review conducted: 2026-03-29. Methodology: static analysis of codebase at current HEAD. No live container testing was performed.
Beta Was this translation helpful? Give feedback.
All reactions