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 review analyzed ~19,971 lines of security-critical code across the AWF firewall codebase (TypeScript sources, Bash entrypoints, iptables scripts, Squid configuration generation, and the seccomp profile). The overall security posture is strong: the architecture is defense-in-depth, input validation is thorough, and credential isolation is well-implemented. No critical vulnerabilities were found. Three medium findings and four lower-severity observations are noted below.
No dedicated "firewall-escape-test" workflow was found in the current workflow list. The security-review and secret-digger-* workflows (Claude, Codex, Copilot variants scheduled hourly/daily) serve overlapping purposes. This review is therefore standalone.
Host-level (src/host-iptables.ts): The DOCKER-USER chain enforces egress filtering for all traffic leaving the awf-net bridge (172.30.0.0/24). A dedicated FW_WRAPPER chain is created and cleaned up on every run. Only Squid IP (172.30.0.10), DNS servers, and localhost are permitted; everything else drops.
Container-level (containers/agent/setup-iptables.sh): NAT OUTPUT rules DNAT port 80/443 to Squid. Docker embedded DNS (127.0.0.11) rules are preserved across NAT flushes. Dangerous ports (SSH 22, databases, RDP 3389, etc.) are explicitly returned from NAT (not redirected) and then dropped by the filter chain's final DROP rule.
Squid-level (src/squid-config.ts): Domain ACLs, IP-literal blocking (dst_ipv4, dst_ipv6 via dstdom_regex), dangerous-port ACLs, and default-deny http_access deny all as final rule.
IPv6: When ip6tables is unavailable, sysctl net.ipv6.conf.all.disable_ipv6=1 is applied to prevent IPv6 bypass paths. When available, mirror rules are applied.
One concern (Medium severity): See Finding M1 below.
Agent container: cap_add: [SYS_CHROOT, SYS_ADMIN] during startup (needed for procfs mount + chroot), then capsh --drop removes them before user code runs. NET_RAW is explicitly dropped to prevent raw socket creation.
Squid/API-proxy: cap_drop: [ALL] with only network capabilities retained.
All Docker/system commands use execa with argument arrays (no shell interpolation).
escapeShellArg() and joinShellArgs() exist and are used for shell-context arguments.
User-provided agentCommand is passed as ['/bin/bash', '-c', ...] with $ replaced with $$ to prevent Docker Compose variable interpolation: config.agentCommand.replace(/\$/g, '$$$$').
Port specifications are sanitized with port.replace(/[^0-9-]/g, '') and validated as integers in 1–65535.
DANGEROUS_PORTS list is checked for all user-specified ports.
UID/GID from AWF_USER_UID/AWF_USER_GID are validated as numeric and non-zero before usermod/groupmod.
One concern (Low severity): See Finding L1 below.
Credential Isolation Assessment ✅ Strong
Evidence gathered:
sed -n '370,410p' src/docker-manager.ts
grep -n "ONE_SHOT\|one.shot" src/docker-manager.ts
````
- `EXCLUDED_ENV_VARS` strips PATH, PWD, SHLVL, sudo metadata from container environment.
- When `--enable-api-proxy` is active: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY`, `CODEX_API_KEY`, `OPENAI_KEY` are excluded from the agent container.
- `COPILOT_GITHUB_TOKEN` is replaced with a placeholder value; a one-shot-token LD_PRELOAD library (`AWF_ONE_SHOT_TOKENS`) controls real token exposure.
- `GITHUB_TOKEN`/`GH_TOKEN` are forwarded into the agent — this is intentional (agents often need GitHub API access) but documented.
---
## ⚠️ Threat Model (STRIDE)|# | Category | Threat | Severity | Evidence ||---|----------|--------|----------|----------|| M1 |**Information Disclosure**| IPv6 DNAT gap in container NAT chain | Medium |`setup-iptables.sh` — IPv6 NAT rules mirror IPv4 for DNS only; no ip6tables DNAT to Squid for port 80/443 || M2 |**Elevation of Privilege**|`mount`/`unshare`/`setns` allowed in seccomp; combined with initial `SYS_ADMIN`, a narrow window exists pre-`capsh` drop | Medium |`seccomp-profile.json` line evidence above || M3 |**Tampering / Firewall Bypass**| Docker-in-Docker: when `--enable-docker` is used, the agent gains access to the host Docker socket, enabling new containers outside the AWF network | Medium |`src/docker-manager.ts:790` — socket bind-mounted read-write || L1 |**Tampering**|`agentCommand``$`→`$$` substitution protects Docker Compose interpolation but not all shell metacharacters; however, the command runs in a bash `-c` context inside the container (already sandboxed) | Low |`src/docker-manager.ts:1050`|| L2 |**Information Disclosure**|`GITHUB_TOKEN`/`GH_TOKEN` are forwarded unconditionally to the agent container (not excluded even with `--enable-api-proxy`) | Low |`src/docker-manager.ts:501-502`|| L3 |**Denial of Service**| No resource limits (`cpu_quota`, `mem_limit`) set on agent container in generated docker-compose | Low |`src/docker-manager.ts` — no `deploy.resources`|| L4 |**Repudiation**| Squid logs are owned by the `proxy` user inside the container; host-side reading requires `sudo cat` — forensic logs may be inaccessible in automated pipelines | Low |`containers/squid/entrypoint.sh`|
---
## 🎯 Attack Surface Map| Entry Point | File:Line | Protection | Risk ||-------------|-----------|------------|------||`--allow-domains` CLI input |`src/domain-patterns.ts:validateDomainOrPattern`| Length limit, pattern validation, ReDoS-safe regex |**Low** — well-validated ||`agentCommand` (user command string) |`src/docker-manager.ts:1050`|`$`→`$$` substitution; runs inside sandboxed container |**Low** — limited blast radius ||`AWF_USER_UID`/`AWF_USER_GID` env vars |`containers/agent/entrypoint.sh:7-38`| Numeric + non-zero validation |**Low**||`--allow-host-ports` CLI input |`src/squid-config.ts:462-495`| Integer validation, dangerous-port blocklist, `[^0-9-]` strip |**Low**|| Docker socket bind-mount (DinD) |`src/docker-manager.ts:790-799`| Only enabled with explicit `--enable-docker` flag; logged as warning |**Medium**|| IPv6 traffic in container |`containers/agent/setup-iptables.sh`| ip6tables rules for DNS; no DNAT for port 80/443 if ip6tables unavailable on host |**Medium**|| Squid DNS resolution |`src/squid-config.ts:570-571`|`dns_nameservers`set from `--dns-servers`; embedded DNS only in agent |**Low**|| API proxy sidecar ports 10000-10002,10004 |`containers/api-proxy/server.js`| Unauthenticated from agent but only reachable inside `awf-net`|**Low**|| Host-level iptables `DOCKER-USER` chain |`src/host-iptables.ts`| Dedicated chain, cleaned up on exit|**Low**||`capsh` drop before user code |`containers/agent/entrypoint.sh:583-637`| SYS_CHROOT + SYS_ADMIN dropped atomically via capsh |**Low**|
---
## 📋 Evidence Collection<details><summary>npm audit (no vulnerabilities)</summary>````
Vulns: {'info': 0, 'low': 0, 'moderate': 0, 'high': 0, 'critical': 0, 'total': 0}
````</details><details><summary>Seccomp profile dangerous syscall allowances</summary>````
Action: SCMP_ACT_ALLOW, Syscalls: {'chroot', 'mount', 'setns', 'clone', 'clone3', 'unshare'}
Action: SCMP_ACT_ERRNO, Syscalls: {'ptrace', 'process_vm_writev', 'process_vm_readv'}
Action: SCMP_ACT_ERRNO, Syscalls: {'pivot_root'}
`````mount` is intentionally allowed (procfs mount in chroot mode requires `SYS_ADMIN + mount`). `unshare` and `setns` are allowed as part of the Docker default seccomp profile but are not needed after the iptables-init pattern was adopted.
</details><details><summary>Squid IP-literal blocking</summary>````
acl dst_ipv4 dstdom_regex ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$
acl dst_ipv6 dstdom_regex ^\[?[0-9a-fA-F:]+\]?$
http_access deny dst_ipv4
http_access deny dst_ipv6
````
Prevents bypassing domain ACLs via direct IP connections.
</details><details><summary>Dangerous port blocking (setup-iptables.sh)</summary>````
DANGEROUS_PORTS=(22 23 25 110 143 445 1433 1521 3306 3389 5432 6379 27017 27018 28017)
# NAT RETURN for each, then DROP in OUTPUT filter chain
EXCLUDED_ENV_VARS (api-proxy disabled path)
constEXCLUDED_ENV_VARS=newSet(['PATH','PWD','OLDPWD','SHLVL','_','SUDO_COMMAND','SUDO_USER','SUDO_UID','SUDO_GID',]);// When api-proxy enabled, also excludes: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.
✅ Recommendations
🔴 Critical
(None found)
🟠 High
(None found)
🟡 Medium
M1 — IPv6 DNAT gap in container NAT chain
In setup-iptables.sh, when IP6TABLES_AVAILABLE=true, IPv6 NAT rules only cover DNS exemptions. Port 80 and 443 DNAT rules are IPv4-only (iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT ...). An agent using IPv6-capable connections could bypass Squid for HTTP/HTTPS traffic if the destination resolves to an AAAA record (and the container has an IPv6 route).
Mitigation: Add ip6tables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT ... and ip6tables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT ... mirrors after the IPv4 DNAT rules in setup-iptables.sh. Also add matching ip6tables DNAT for user-specified AWF_ALLOW_HOST_PORTS.
M2 — unshare and setns allowed in seccomp profile
The seccomp profile (containers/agent/seccomp-profile.json) allows unshare and setns. While these require capabilities (e.g., CAP_SYS_ADMIN, CAP_NET_ADMIN) that are dropped before user code runs, their presence in the allow-list is an unnecessary attack surface. A future misconfiguration that leaks a capability could be exploited.
Mitigation: Add unshare and setns to the SCMP_ACT_ERRNO deny group in the seccomp profile, alongside pivot_root. The iptables-init sidecar pattern means the agent container itself no longer needs these.
M3 — Docker-in-Docker enables full firewall bypass
When --enable-docker is passed, the host Docker socket is bind-mounted read-write into the agent container (/host/var/run/docker.sock). This allows the agent to start new containers on arbitrary networks (bypassing awf-net), pull images from anywhere, and execute code outside the firewall. The code logs a warning (src/docker-manager.ts:788) but does not restrict outbound access from those child containers.
Mitigation: Document clearly that --enable-docker is an escape hatch that disables network isolation guarantees. Consider adding a separate --enable-docker-read-only option that mounts the socket read-only. At minimum, add a prominent CLI warning printed to stderr.
🔵 Low
L1 — GITHUB_TOKEN forwarded unconditionally GITHUB_TOKEN and GH_TOKEN are forwarded to the agent container in all cases (lines 501-502 in docker-manager.ts), including when --enable-api-proxy is active. Unlike the API keys that are withheld in api-proxy mode, these GitHub tokens are always present. A compromised agent could use them to push code or create issues.
Mitigation: Consider adding GITHUB_TOKEN / GH_TOKEN to the one-shot-token list (AWF_ONE_SHOT_TOKENS) so they are consumed on first use, or expose them only when the caller explicitly opts in.
L2 — No CPU/memory resource limits on agent container
The generated docker-compose does not set deploy.resources.limits. A runaway agent could exhaust host resources, causing a denial-of-service to the host.
Mitigation: Add configurable defaults (e.g., --cpu-limit, --memory-limit flags) and apply them via deploy.resources.limits in the generated compose file.
L3 — unshare/setns in seccomp (also listed above as M2)
L4 — Squid logs require sudo to read
Squid container runs as user proxy; log files written to /var/log/squid/ are owned by proxy. On the host, reading preserved logs requires sudo. This makes automated forensic pipelines (e.g., awf logs stats) require privilege escalation.
Mitigation: Consider setting Squid log file permissions to 0644 in containers/squid/entrypoint.sh, or use a shared volume group that matches the host runner UID.
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 review analyzed ~19,971 lines of security-critical code across the AWF firewall codebase (TypeScript sources, Bash entrypoints, iptables scripts, Squid configuration generation, and the seccomp profile). The overall security posture is strong: the architecture is defense-in-depth, input validation is thorough, and credential isolation is well-implemented. No critical vulnerabilities were found. Three medium findings and four lower-severity observations are noted below.
Key metrics:
src/*.ts,containers/agent/entrypoint.sh,containers/agent/setup-iptables.sh,containers/squid/entrypoint.sh,containers/agent/seccomp-profile.jsonnpm audit)🔍 Findings from Firewall Escape Test
No dedicated "firewall-escape-test" workflow was found in the current workflow list. The
security-reviewandsecret-digger-*workflows (Claude, Codex, Copilot variants scheduled hourly/daily) serve overlapping purposes. This review is therefore standalone.🛡️ Architecture Security Analysis
Network Security Assessment ✅ Strong
Evidence gathered:
cat containers/agent/setup-iptables.sh cat src/host-iptables.ts grep -n "dst_ipv4\|dst_ipv6" src/squid-config.tsThe iptables architecture is layered and sound:
Host-level (
src/host-iptables.ts): TheDOCKER-USERchain enforces egress filtering for all traffic leaving theawf-netbridge (172.30.0.0/24). A dedicatedFW_WRAPPERchain is created and cleaned up on every run. Only Squid IP (172.30.0.10), DNS servers, and localhost are permitted; everything else drops.Container-level (
containers/agent/setup-iptables.sh): NAT OUTPUT rules DNAT port 80/443 to Squid. Docker embedded DNS (127.0.0.11) rules are preserved across NAT flushes. Dangerous ports (SSH 22, databases, RDP 3389, etc.) are explicitly returned from NAT (not redirected) and then dropped by the filter chain's finalDROPrule.Squid-level (
src/squid-config.ts): Domain ACLs, IP-literal blocking (dst_ipv4,dst_ipv6via dstdom_regex), dangerous-port ACLs, and default-denyhttp_access deny allas final rule.IPv6: When
ip6tablesis unavailable,sysctl net.ipv6.conf.all.disable_ipv6=1is applied to prevent IPv6 bypass paths. When available, mirror rules are applied.One concern (Medium severity): See Finding M1 below.
Container Security Assessment ✅ Good with caveats
Evidence gathered:
Capability management:
awf-iptables-init):cap_add: [NET_ADMIN, NET_RAW],cap_drop: [ALL]— correct, narrow scope.cap_add: [SYS_CHROOT, SYS_ADMIN]during startup (needed for procfs mount + chroot), thencapsh --dropremoves them before user code runs.NET_RAWis explicitly dropped to prevent raw socket creation.cap_drop: [ALL]with only network capabilities retained.Seccomp profile (
containers/agent/seccomp-profile.json):SCMP_ACT_ERRNO(deny-by-default) ✅ptrace,process_vm_readv,process_vm_writevare blocked ✅pivot_rootis blocked ✅mount,unshare,setns,clone,clone3, andchrootare ALLOWED — see Finding M2.Domain Validation Assessment ✅ Strong
Evidence gathered:
*and*.*patterns are explicitly rejected with clear errors.*.*.com) are rejected.wildcardToRegex()uses character class[a-zA-Z0-9.-]*instead of.*to prevent ReDoS.(redacted)https://`) is well-handled.*.,.*), and empty inputs are all validated.No issues found in domain validation. The implementation reflects careful security thinking.
Input Validation Assessment ✅ Good
Evidence gathered:
execawith argument arrays (no shell interpolation).escapeShellArg()andjoinShellArgs()exist and are used for shell-context arguments.agentCommandis passed as['/bin/bash', '-c', ...]with$replaced with$$to prevent Docker Compose variable interpolation:config.agentCommand.replace(/\$/g, '$$$$').port.replace(/[^0-9-]/g, '')and validated as integers in 1–65535.AWF_USER_UID/AWF_USER_GIDare validated as numeric and non-zero beforeusermod/groupmod.One concern (Low severity): See Finding L1 below.
Credential Isolation Assessment ✅ Strong
Evidence gathered:
EXCLUDED_ENV_VARS (api-proxy disabled path)
✅ Recommendations
🔴 Critical
(None found)
🟠 High
(None found)
🟡 Medium
M1 — IPv6 DNAT gap in container NAT chain
In
setup-iptables.sh, whenIP6TABLES_AVAILABLE=true, IPv6 NAT rules only cover DNS exemptions. Port 80 and 443 DNAT rules are IPv4-only (iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT ...). An agent using IPv6-capable connections could bypass Squid for HTTP/HTTPS traffic if the destination resolves to an AAAA record (and the container has an IPv6 route).Mitigation: Add
ip6tables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT ...andip6tables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT ...mirrors after the IPv4 DNAT rules insetup-iptables.sh. Also add matching ip6tables DNAT for user-specifiedAWF_ALLOW_HOST_PORTS.M2 —
unshareandsetnsallowed in seccomp profileThe seccomp profile (
containers/agent/seccomp-profile.json) allowsunshareandsetns. While these require capabilities (e.g.,CAP_SYS_ADMIN,CAP_NET_ADMIN) that are dropped before user code runs, their presence in the allow-list is an unnecessary attack surface. A future misconfiguration that leaks a capability could be exploited.Mitigation: Add
unshareandsetnsto theSCMP_ACT_ERRNOdeny group in the seccomp profile, alongsidepivot_root. The iptables-init sidecar pattern means the agent container itself no longer needs these.M3 — Docker-in-Docker enables full firewall bypass
When
--enable-dockeris passed, the host Docker socket is bind-mounted read-write into the agent container (/host/var/run/docker.sock). This allows the agent to start new containers on arbitrary networks (bypassingawf-net), pull images from anywhere, and execute code outside the firewall. The code logs a warning (src/docker-manager.ts:788) but does not restrict outbound access from those child containers.Mitigation: Document clearly that
--enable-dockeris an escape hatch that disables network isolation guarantees. Consider adding a separate--enable-docker-read-onlyoption that mounts the socket read-only. At minimum, add a prominent CLI warning printed to stderr.🔵 Low
L1 — GITHUB_TOKEN forwarded unconditionally
GITHUB_TOKENandGH_TOKENare forwarded to the agent container in all cases (lines 501-502 indocker-manager.ts), including when--enable-api-proxyis active. Unlike the API keys that are withheld in api-proxy mode, these GitHub tokens are always present. A compromised agent could use them to push code or create issues.Mitigation: Consider adding
GITHUB_TOKEN/GH_TOKENto the one-shot-token list (AWF_ONE_SHOT_TOKENS) so they are consumed on first use, or expose them only when the caller explicitly opts in.L2 — No CPU/memory resource limits on agent container
The generated docker-compose does not set
deploy.resources.limits. A runaway agent could exhaust host resources, causing a denial-of-service to the host.Mitigation: Add configurable defaults (e.g.,
--cpu-limit,--memory-limitflags) and apply them viadeploy.resources.limitsin the generated compose file.L3 —
unshare/setnsin seccomp (also listed above as M2)L4 — Squid logs require
sudoto readSquid container runs as user
proxy; log files written to/var/log/squid/are owned byproxy. On the host, reading preserved logs requiressudo. This makes automated forensic pipelines (e.g.,awf logs stats) require privilege escalation.Mitigation: Consider setting Squid log file permissions to
0644incontainers/squid/entrypoint.sh, or use a shared volume group that matches the host runner UID.📈 Security Metrics
Beta Was this translation helpful? Give feedback.
All reactions