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 covers the gh-aw-firewall codebase as of 2026-03-11. The overall security posture is strong, with layered defenses (host-level iptables, container-level iptables, Squid proxy ACLs, capability dropping, seccomp, and credential file hiding). No critical or high-severity vulnerabilities were found. npm audit reports 0 known vulnerabilities in dependencies. Four medium-severity findings and two low-severity findings are identified below.
Note: No "firewall-escape-test" workflow exists; the complementary automated test agent section is replaced with direct codebase evidence.
🔍 Phase 1: Escape Test Agent Context
No firewall-escape-test workflow was found via agenticworkflows-status. This section is therefore based entirely on direct code analysis in Phases 2–5.
🛡️ Phase 2: Architecture Security Analysis
2.1 Network Security Architecture
Two-layer enforcement:
Host layer (src/host-iptables.ts): Creates a dedicated FW_WRAPPER chain in DOCKER-USER, enforcing allowlist via REJECT on exit from the bridge interface. Blocks multicast (ADDRTYPE MULTICAST), link-local (169.254.0.0/16), multicast (224.0.0.0/4), all UDP except whitelisted DNS servers, and then all other traffic with a default REJECT.
Container layer (containers/agent/setup-iptables.sh): NAT-based DNAT redirects HTTP/HTTPS through Squid; dangerous ports (22, 25, 445, 3306, 5432, 6379, 27017, etc.) are sent through the RETURN path and hit the OUTPUT filter chain DROP. Final rule: iptables -A OUTPUT -p tcp -j DROP.
Only whitelisted IP addresses (default 8.8.8.8,8.8.4.4) receive DNS traffic. Custom DNS servers validated via parseDnsServers() against isValidIPv4/isValidIPv6.
AWF_DNS_SERVERS environment variable is validated at CLI entry before being passed to the container.
IPv6 handling:
If ip6tables is unavailable, IPv6 is disabled via sysctl net.ipv6.conf.all.disable_ipv6=1 — preventing an unfiltered bypass path. ✅
Rule ordering:
Squid ACLs place blocked-domain deny rules before allow rules (src/squid-config.ts:388). ✅
Assessment: The default action is SCMP_ACT_ALLOW, meaning all syscalls NOT explicitly listed are permitted. Docker's built-in default seccomp profile also uses SCMP_ACT_ALLOW but maintains a much more comprehensive blocklist (~44 syscalls). This custom profile only explicitly blocks 28 syscalls total.
Risk: Capability drops provide good defense-in-depth here, but an allow-by-default seccomp profile with a sparse blocklist provides weaker isolation than Docker's default. Notably, syscalls like socket(AF_NETLINK), clone3, and various namespace-related calls are not explicitly blocked.
Severity: Medium
AppArmor Unconfined — ⚠️ FINDING M-2
Evidence (src/docker-manager.ts:912):
security_opt: ['no-new-privileges:true',`seccomp=\$\{config.workDir}/seccomp-profile.json`,'apparmor:unconfined',// Required for procfs mount in chroot mode],
Assessment: AppArmor is disabled to allow mounting procfs at /host/proc in chroot mode. While SYS_ADMIN is dropped before user code runs (making unauthorized mounts impossible), the absence of AppArmor removes MAC-layer protections that would otherwise restrict file access patterns, network operations, and capability usage at the OS level.
Note: This trade-off is documented and intentional (comment at line 907).
Severity: Medium
2.3 Domain Pattern Validation
ReDoS protection: Wildcards are converted to [a-zA-Z0-9.-]* (not .*), preventing catastrophic backtracking. ✅
Overly broad patterns blocked:*, *.*, and purely wildcard patterns are rejected. ✅
$ node -e "let s = 'github.com\nfoo'; console.log('After trim:', JSON.stringify(s.trim()));"
After trim: "github.com\nfoo"
The validateDomainOrPattern function does not check for embedded newline or carriage-return characters. If a domain string contains a literal \n, the resulting squid.conf line would be split across two lines, potentially injecting arbitrary Squid directives.
Affected paths:
--allow-domains CLI flag (requires shell $'...' quoting or environment variable expansion)
--allow-domains-file (if a single comma-separated entry within a line contains \n)
parseDomainsFile (src/cli.ts:46) splits on \n first, so file-based injection via line separators is already mitigated. The risk applies only to domains passed via the CLI flag with embedded literal newlines or programmatic API usage.
Severity: Medium (requires attacker to control the domain argument; typically self-exploitation)
2.4 SSH Directory Coverage Gap — ⚠️ FINDING M-4
Evidence (src/docker-manager.ts:787-803):
Hidden SSH private keys:
~/.ssh/id_rsa ✅
~/.ssh/id_ed25519 ✅
~/.ssh/id_ecdsa ✅
~/.ssh/id_dsa ✅
NOT hidden:
~/.ssh/config — may reveal hostnames, jump hosts, and SSH server configuration
~/.ssh/known_hosts — reveals SSH server fingerprints and internal hostnames
~/.ssh/authorized_keys — reveals which public keys have access
~/.ssh/id_ecdsa_sk / ~/.ssh/id_ed25519_sk — FIDO hardware-backed keys (less critical but still private keys)
While these files cannot be used for direct authentication without the corresponding private key (which IS hidden), ~/.ssh/config and known_hosts can leak internal infrastructure topology.
# Final OUTPUT filter rule - only drops TCP
iptables -A OUTPUT -p tcp -j DROP
```The container-level OUTPUT chain explicitly `DROP`s TCP but does not explicitly block ICMP. ICMP packets (ping, traceroute) from within the container are not caught by the container-level filter.**Mitigation:** The host-level `FW_WRAPPER` chain in`DOCKER-USER` has a default `REJECT` rule covering all traffic not previously matched, which includes ICMP. This provides defense-in-depth. However, ICMP to container-local addresses (loopback, Squid at `172.30.0.10`) would not be blocked by the host-level chain since that only applies to inter-network egress.**Severity: Low** (mitigated by host-level rules)---### 2.7 SYS_ADMIN Window During Startup — ⚠️ FINDING L-2**Evidence (`containers/agent/entrypoint.sh`):**The entrypoint runs with `SYS_ADMIN`, `NET_ADMIN`, and `SYS_CHROOT` from container start until`capsh --drop=...` is executed. This includes UID/GID adjustment, DNS configuration, SSL certificate setup, iptables setup, and API proxy health checks — all running as root with full `SYS_ADMIN`.If an attacker could control any of these early-phase inputs (e.g., a malicious Docker image or entrypoint override), they would have elevated privilege. However, these inputs come from the AWF CLI itself, not from agent-controlled code.**Severity: Low**---## ⚠️ Threat Model (STRIDE)|# | Category | Threat | Evidence | Likelihood | Impact | Risk ||---|----------|--------|----------|------------|--------|------|| T1 |**Spoofing**| DNS spoofing to reach non-whitelisted hosts | DNS restricted to whitelisted IPs; mitigated | Low | High |**Low**|| T2 |**Tampering**| Agent modifies iptables after startup |`NET_ADMIN` dropped before user code via `capsh`;`no-new-privileges:true`| Low | Critical |**Low**|| T3 |**Tampering**| Docker socket used to spawn unrestricted container | Socket hidden via `/dev/null` mount in chroot mode | Low | Critical |**Low**|| T4 |**Repudiation**| Agent denies network activity | Squid access log + iptables kernel log (`[FW_BLOCKED_*]`) provide comprehensive audit trail | Low | Medium |**Low**|| T5 |**Info Disclosure**| Credential exfiltration via mounted files | 17 credential files hidden via `/dev/null` overlay | Low | Critical |**Low**|| T6 |**Info Disclosure**| SSH topology leak via `.ssh/config` / `known_hosts`| NOT hidden — see Finding M-4 | Medium | Medium |**Medium**|| T7 |**Info Disclosure**| DNS exfiltration via arbitrary resolver | DNS restricted to whitelisted servers only | Low | High |**Low**|| T8 |**Info Disclosure**| Token leak via `/proc/self/environ`|`one-shot-token.so` + `unset_sensitive_tokens()` clears tokens after agent startup | Low | Critical |**Low**|| T9 |**Denial of Service**| Resource exhaustion in container |`mem_limit: 4g`, `pids_limit: 1000`, `cpu_shares: 1024`| Low | Medium |**Low**|| T10 |**Elevation of Privilege**| Seccomp bypass via allowed syscall combinations | Allow-by-default profile; capability drops provide primary defense | Medium | High |**Medium**|| T11 |**Elevation of Privilege**| AppArmor bypass (unconfined) | AppArmor disabled for chroot mount; capsh mitigates post-startup | Medium | Medium |**Medium**|| T12 |**Spoofing**| Squid config injection via newline in domain | No newline check in`validateDomainOrPattern`| Low | High |**Medium**|---## 🎯 Attack Surface Map| Entry Point | File:Line | Protection | Risk ||-------------|-----------|------------|------||`--allow-domains` / `--block-domains`|`src/cli.ts:867`, `src/domain-patterns.ts:134`| Regex validation, pattern checks |**Medium** (no newline check) ||`--dns-servers`|`src/cli.ts:989`, `parseDnsServers`| IPv4/IPv6 format validation | Low ||`--allow-urls` (SSL bump) |`src/ssl-bump.ts:313`| Regex escaping, anchoring | Low ||`--allow-host-ports`|`setup-iptables.sh`| Passed as `AWF_ALLOW_HOST_PORTS` env var | Low || Agent command|`src/cli.ts:857`, `docker-manager.ts:922`| Shell escaping + `$` doubling | Low || Squid proxy traffic |`containers/squid/`| ACL allowlist, dangerous-port list | Low || Docker compose env injection |`src/docker-manager.ts:395+`| Selective env passthrough | Low || Container filesystem mounts |`src/docker-manager.ts:787+`| Selective mounting, /dev/null overlays | Low (SSH gap) || Host iptables |`src/host-iptables.ts`| DOCKER-USER chain, dedicated FW_WRAPPER | Low || Container iptables |`containers/agent/setup-iptables.sh`| NAT redirect + OUTPUT DROP | Low |---## 📋 Evidence Collection<details><summary>Command: seccomp profile analysis</summary>```$ python3 -c "import json,sys; d=json.load(open('containers/agent/seccomp-profile.json')); ..."Default action: SCMP_ACT_ALLOWBlocked syscalls: ptrace, process_vm_readv, process_vm_writev, kexec_load, kexec_file_load, reboot, init_module, finit_module, delete_module, acct, swapon, swapoff, pivot_root, syslog, add_key, request_key, keyctl, uselib, personality, ustat, sysfs, vhangup, get_kernel_syms, query_module, create_module, nfsservctl, umount, umount2Architectures: SCMP_ARCH_X86_64, SCMP_ARCH_X86, SCMP_ARCH_AARCH64```</details><details><summary>Command: capability configuration</summary>```src/docker-manager.ts:896: cap_add: ['NET_ADMIN', 'SYS_CHROOT', 'SYS_ADMIN'], cap_drop: ['NET_RAW', 'SYS_PTRACE', 'SYS_MODULE', 'SYS_RAWIO', 'MKNOD'], security_opt: ['no-new-privileges:true', 'seccomp=...', 'apparmor:unconfined']containers/agent/entrypoint.sh:649: capsh --drop=$CAPS_TO_DROP -- -c "exec gosu awfuser ..."```</details><details><summary>Command: SSH credential hiding gap</summary>```Hidden: ~/.ssh/id_rsa, id_ed25519, id_ecdsa, id_dsaNOT hidden: ~/.ssh/config, known_hosts, authorized_keys, id_ecdsa_sk, id_ed25519_sk
Command: domain newline injection test
$ node -e "let s = 'github.com\nhttp_access allow all'; console.log(JSON.stringify(s.trim()));""github.com\nhttp_access allow all"# .trim() does NOT remove embedded newlines — only leading/trailing whitespace# validateDomainOrPattern() has no \n check; the string would pass validation```</details><details><summary>Command: npm audit</summary>```
Vulnerabilities: {'info': 0, 'low': 0, 'moderate': 0, 'high': 0, 'critical': 0, 'total': 0}
```</details><details><summary>Command: host-level 169.254.0.0/16 block</summary>```
src/host-iptables.ts:516:
'-d', '169.254.0.0/16', '-j', 'REJECT', '--reject-with', 'icmp-port-unreachable'# Cloud metadata endpoint (AWS IMDS, GCP metadata, Azure IMDS) is explicitly blocked ✅
✅ Recommendations
🔴 Critical
None identified.
🟠 High
None identified.
🟡 Medium
[M-1] Harden seccomp profile to blocklist more syscalls
Replace allow-by-default custom profile with Docker's built-in default profile (which has a more comprehensive blocklist) or extend the current profile to explicitly block namespace, netlink socket, and other container-escape-relevant syscalls.
File: containers/agent/seccomp-profile.json
[M-2] Re-evaluate AppArmor:unconfined scope
Consider whether AppArmor can be re-enabled with a custom profile that permits only the specific mount operations needed for chroot (mounting procfs), rather than fully disabling it.
File: src/docker-manager.ts:912
[M-3] Add newline/control-character validation in domain parsing
Add a check for newline (\n, \r) and other control characters in validateDomainOrPattern(). This prevents Squid configuration injection if domains are sourced from untrusted automated inputs.
File: src/domain-patterns.ts — add to validateDomainOrPattern():
if(/[\n\r\0]/.test(trimmed)){thrownewError(`Invalid domain '\$\{trimmed}': contains control characters`);}
[M-4] Extend SSH directory credential hiding
Add ~/.ssh/config and ~/.ssh/known_hosts to the credential file hiding list to prevent leakage of internal SSH topology.
Also consider adding ~/.ssh/id_ecdsa_sk and ~/.ssh/id_ed25519_sk for completeness.
[L-1] Add explicit ICMP DROP in container OUTPUT chain
Add iptables -A OUTPUT -p icmp -j DROP to setup-iptables.sh before the final TCP DROP rule to prevent ICMP to container-local addresses.
File: containers/agent/setup-iptables.sh
[L-2] Document the SYS_ADMIN startup window
Add a comment or note to the security docs explaining the brief SYS_ADMIN window during container startup and why it is acceptable (all inputs come from the trusted AWF CLI).
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 covers the gh-aw-firewall codebase as of 2026-03-11. The overall security posture is strong, with layered defenses (host-level iptables, container-level iptables, Squid proxy ACLs, capability dropping, seccomp, and credential file hiding). No critical or high-severity vulnerabilities were found. npm audit reports 0 known vulnerabilities in dependencies. Four medium-severity findings and two low-severity findings are identified below.
🔍 Phase 1: Escape Test Agent Context
No
firewall-escape-testworkflow was found viaagenticworkflows-status. This section is therefore based entirely on direct code analysis in Phases 2–5.🛡️ Phase 2: Architecture Security Analysis
2.1 Network Security Architecture
Two-layer enforcement:
Host layer (
src/host-iptables.ts): Creates a dedicatedFW_WRAPPERchain inDOCKER-USER, enforcing allowlist via REJECT on exit from the bridge interface. Blocks multicast (ADDRTYPE MULTICAST), link-local (169.254.0.0/16), multicast (224.0.0.0/4), all UDP except whitelisted DNS servers, and then all other traffic with a defaultREJECT.Container layer (
containers/agent/setup-iptables.sh): NAT-based DNAT redirects HTTP/HTTPS through Squid; dangerous ports (22, 25, 445, 3306, 5432, 6379, 27017, etc.) are sent through theRETURNpath and hit the OUTPUT filter chainDROP. Final rule:iptables -A OUTPUT -p tcp -j DROP.Cloud metadata endpoint blocked:
DNS exfiltration prevention:
8.8.8.8,8.8.4.4) receive DNS traffic. Custom DNS servers validated viaparseDnsServers()againstisValidIPv4/isValidIPv6.AWF_DNS_SERVERSenvironment variable is validated at CLI entry before being passed to the container.IPv6 handling:
ip6tablesis unavailable, IPv6 is disabled viasysctl net.ipv6.conf.all.disable_ipv6=1— preventing an unfiltered bypass path. ✅Rule ordering:
src/squid-config.ts:388). ✅2.2 Container Security Hardening
Capabilities
Agent container (
src/docker-manager.ts:896):All three added capabilities are dropped via
capshbefore user commands execute:no-new-privileges:trueis set, preventing privilege escalation after capsh runs. ✅The API proxy sidecar drops ALL capabilities (
cap_drop: ['ALL']). ✅Seccomp Profile —⚠️ FINDING M-1
Evidence (
containers/agent/seccomp-profile.json):{ "defaultAction": "SCMP_ACT_ALLOW", "syscalls": [ { "action": "SCMP_ACT_ERRNO", "names": ["ptrace", "process_vm_readv", "process_vm_writev"] }, { "action": "SCMP_ACT_ERRNO", "names": ["kexec_load", "kexec_file_load", "reboot", "init_module", ...] }, { "action": "SCMP_ACT_ERRNO", "names": ["umount", "umount2"] } ] }Assessment: The default action is
SCMP_ACT_ALLOW, meaning all syscalls NOT explicitly listed are permitted. Docker's built-in default seccomp profile also usesSCMP_ACT_ALLOWbut maintains a much more comprehensive blocklist (~44 syscalls). This custom profile only explicitly blocks 28 syscalls total.Risk: Capability drops provide good defense-in-depth here, but an allow-by-default seccomp profile with a sparse blocklist provides weaker isolation than Docker's default. Notably, syscalls like
socket(AF_NETLINK),clone3, and various namespace-related calls are not explicitly blocked.Severity: Medium
AppArmor Unconfined —⚠️ FINDING M-2
Evidence (
src/docker-manager.ts:912):Assessment: AppArmor is disabled to allow mounting procfs at
/host/procin chroot mode. WhileSYS_ADMINis dropped before user code runs (making unauthorized mounts impossible), the absence of AppArmor removes MAC-layer protections that would otherwise restrict file access patterns, network operations, and capability usage at the OS level.Note: This trade-off is documented and intentional (comment at line 907).
Severity: Medium
2.3 Domain Pattern Validation
ReDoS protection: Wildcards are converted to
[a-zA-Z0-9.-]*(not.*), preventing catastrophic backtracking. ✅Overly broad patterns blocked:
*,*.*, and purely wildcard patterns are rejected. ✅Subdomain matching:
github.com→.github.comin Squid (matches parent + subdomains). ✅Domain Config Injection —⚠️ FINDING M-3
Evidence (
src/domain-patterns.ts—validateDomainOrPatternfunction):The
validateDomainOrPatternfunction does not check for embedded newline or carriage-return characters. If a domain string contains a literal\n, the resultingsquid.confline would be split across two lines, potentially injecting arbitrary Squid directives.Affected paths:
--allow-domainsCLI flag (requires shell$'...'quoting or environment variable expansion)--allow-domains-file(if a single comma-separated entry within a line contains\n)parseDomainsFile(src/cli.ts:46) splits on\nfirst, so file-based injection via line separators is already mitigated. The risk applies only to domains passed via the CLI flag with embedded literal newlines or programmatic API usage.Example PoC (self-exploitation):
Severity: Medium (requires attacker to control the domain argument; typically self-exploitation)
2.4 SSH Directory Coverage Gap —⚠️ FINDING M-4
Evidence (
src/docker-manager.ts:787-803):Hidden SSH private keys:
~/.ssh/id_rsa✅~/.ssh/id_ed25519✅~/.ssh/id_ecdsa✅~/.ssh/id_dsa✅NOT hidden:
~/.ssh/config— may reveal hostnames, jump hosts, and SSH server configuration~/.ssh/known_hosts— reveals SSH server fingerprints and internal hostnames~/.ssh/authorized_keys— reveals which public keys have access~/.ssh/id_ecdsa_sk/~/.ssh/id_ed25519_sk— FIDO hardware-backed keys (less critical but still private keys)While these files cannot be used for direct authentication without the corresponding private key (which IS hidden),
~/.ssh/configandknown_hostscan leak internal infrastructure topology.Severity: Medium
2.5 Input Validation Assessment
Shell argument escaping (
src/cli.ts:504):Proper single-quote escaping prevents command injection when multi-word arguments are joined. ✅
Docker Compose
$escaping (src/docker-manager.ts:922):Prevents Docker Compose variable interpolation from consuming shell variables in user commands. ✅
execausage: All external commands use array argument form (not shell interpolation), preventing command injection through arguments. ✅2.6 Container-Level ICMP Handling —⚠️ FINDING L-1
Evidence (
containers/agent/setup-iptables.sh:295):Command: domain newline injection test
✅ Recommendations
🔴 Critical
None identified.
🟠 High
None identified.
🟡 Medium
[M-1] Harden seccomp profile to blocklist more syscalls
containers/agent/seccomp-profile.json[M-2] Re-evaluate AppArmor:unconfined scope
src/docker-manager.ts:912[M-3] Add newline/control-character validation in domain parsing
\n,\r) and other control characters invalidateDomainOrPattern(). This prevents Squid configuration injection if domains are sourced from untrusted automated inputs.src/domain-patterns.ts— add tovalidateDomainOrPattern():[M-4] Extend SSH directory credential hiding
~/.ssh/configand~/.ssh/known_hoststo the credential file hiding list to prevent leakage of internal SSH topology.~/.ssh/id_ecdsa_skand~/.ssh/id_ed25519_skfor completeness.src/docker-manager.ts:787-803(credentialFiles array)🟢 Low
[L-1] Add explicit ICMP DROP in container OUTPUT chain
iptables -A OUTPUT -p icmp -j DROPtosetup-iptables.shbefore the final TCP DROP rule to prevent ICMP to container-local addresses.containers/agent/setup-iptables.sh[L-2] Document the SYS_ADMIN startup window
📈 Security Metrics
Beta Was this translation helpful? Give feedback.
All reactions