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 covers a deep, evidence-based security analysis of the gh-aw-firewall codebase conducted on 2026-04-04. The overall security posture is strong — the architecture is well-thought-out with multiple defence-in-depth layers. No critical vulnerabilities were found. Two high-severity findings relate to a silent security control bypass (subdomains: false in YAML rulesets has no effect) and container-escape-capable syscalls remaining in the seccomp allowlist (unshare, setns, mount, open_by_handle_at). Several medium and low findings are documented below.
ℹ️ Note on Firewall Escape Test Agent: No workflow named firewall-escape-test or firewall-escape was found in the repository via agenticworkflows-logs. This review is self-contained.
🔍 Phase 1: Context from Previous Security Testing
No complementary firewall-escape-test agent results were available (no such workflow found). This analysis stands on its own codebase review.
The iptables architecture implements solid layered controls:
IPv6 disabled at init (setup-iptables.sh lines 47–53):
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
```IPv6 is disabled inside the container namespace to prevent proxy bypass via IPv6 paths ("Happy Eyeballs").
DNS pinning — Only configured DNS servers (Google 8.8.8.8 by default) receive UDP/TCP port 53 from inside the agent. All other UDP is dropped by a blanket iptables -A OUTPUT -p udp -j DROP rule.
Dangerous port blocking — 15 ports are explicitly enumerated in setup-iptables.sh matching DANGEROUS_PORTS in squid-config.ts: SSH(22), Telnet(23), SMTP(25), POP3(110), IMAP(143), SMB(445), MSSQL(1433), Oracle(1521), MySQL(3306), RDP(3389), PG(5432), Redis(6379), MongoDB(27017/27018/28017).
DNAT redirection — All port 80/443 TCP is redirected to Squid via DNAT before it can leave the container. Proxy-unaware tools are blocked at the TLS level (Squid rejects raw ClientHellos).
Squid rule ordering is correct — The accessRulesSection and denyRule in src/squid-config.ts (line ~596) are emitted BEFORE http_access allow localnet. Squid processes rules in order; a request matching the deny !allowed_domains rule is rejected before reaching the allow localnet rule. The comment "This applies to all sources including localnet" confirms this intent.
Direct IP connection blocking — Squid config includes:
This prevents bypassing domain filtering by connecting to raw IPs.
Host-level DOCKER-USER chain (src/host-iptables.ts) — A dedicated FW_WRAPPER chain is created to filter traffic at the Docker bridge layer, providing a second enforcement point outside the container.
⚠️ Finding (Medium) — Squid resolves DNS directly, bypassing container DNS restrictions
Squid has its own dns_nameservers directive pointing directly to upstream DNS (not through Docker embedded DNS). This means Squid bypasses the container-level DNS filtering enforced by iptables. While Squid's DNS is used only for the proxy's own resolution (and its egress is filtered by the host-level DOCKER-USER chain), this is an architectural asymmetry worth noting.
PID limits applied: agent=1000, init=50, api-proxy=100, squid-proxy has none set (see Finding below)
Memory limit defaults to 6g with swap disabled when user specifies a limit
⚠️⚠️ Finding (High) — Container-escape syscalls unshare, setns, mount, open_by_handle_at are allowed in the custom seccomp profile
# Evidence: verified with:# python3 -c "import json; d=json.load(open('containers/agent/seccomp-profile.json')); [print(s['action'], s['names']) for s in d['syscalls'] if any(x in s.get('names',[]) for x in ['unshare','setns','mount','open_by_handle_at'])]"# Output:# unshare: SCMP_ACT_ALLOW# setns: SCMP_ACT_ALLOW# mount: SCMP_ACT_ALLOW# open_by_handle_at: SCMP_ACT_ALLOW
unshare — creates new namespaces (network, mount, etc.). With CAP_SYS_ADMIN dropped after chroot setup, this is limited but CLONE_NEWUSER (user namespace creation) may still be possible without capabilities in some kernel configurations.
setns — joins another process's namespace. If an agent process obtains a file descriptor to another namespace (e.g., via /proc), it could join it.
mount — needed during chroot setup (mounting procfs), but the seccomp profile doesn't remove it after the capability drop phase. After CAP_SYS_ADMIN is dropped via capsh, mount itself will fail. However, if there is any capability leak, mount remains available.
open_by_handle_at — the "Shocker" container escape vector. This syscall combined with name_to_handle_at (also ALLOW) and a file descriptor to a host filesystem mount point can allow accessing host files outside the container's view. See [CVE-2014-0038]((nvd.nist.gov/redacted) style attacks. While the selective bind mounts reduce exposure, the syscall being allowed is a risk.
The defaultAction: SCMP_ACT_ERRNO (good — block-by-default) combined with ptrace, process_vm_readv, process_vm_writev being explicitly blocked is positive. But the above four syscalls create residual container escape surface.
⚠️ Finding (Medium) — iptables-init container has no custom seccomp profile
grep -n "iptablesInitService.*security_opt\|security_opt.*iptables" src/docker-manager.ts
# No results — the init container uses Docker's default seccomp profile
The iptables-init container has NET_ADMIN and NET_RAW capabilities and uses Docker's default (more permissive) seccomp profile rather than the AWF custom one. This is acceptable given the container exits immediately after running setup-iptables.sh, but represents unnecessary attack surface during its brief lifetime.
Domain Validation Assessment
Command run:
cat src/domain-patterns.ts
cat src/rules.ts
Findings (positive):
validateDomainOrPattern() correctly rejects: empty strings, *, *.*, any pattern matching ^[*.]+$, and dangerous characters: [\s\0"'`;#\]`.
The SQUID_DANGEROUS_CHARS check is applied again at config generation time via assertSafeForSquidConfig() — double validation for injection prevention.
Wildcard * is converted to [a-zA-Z0-9.-]* (bounded character class), preventing ReDoS in Squid regex evaluation.
Protocol-specific domain restrictions ((domain/redacted) vs (domain/redacted)`) are supported and correctly generate separate Squid ACLs.
⚠️⚠️ Finding (High) — subdomains: false in YAML ruleset files is silently ignored — always enables subdomain matching
# Evidence from src/rules.ts:148-158exportfunctionexpandRule(rule: Rule): string[] {
// The existing system already handles subdomain matching when a plain
// domain is provided (e.g., "github.com" matches both github.com and
// *.github.qkg1.top in Squid config). So we just return the domain.
return [rule.domain];
}
A user who writes a ruleset like:
version: 1rules:
- domain: internal.example.comsubdomains: false # Intent: exact match only
...will find that api.internal.example.com is also allowed, contrary to the documented intent. The subdomains field is parsed and stored in the Rule struct but expandRule() ignores it entirely. This creates a false sense of security for operators who use YAML rulesets expecting exact-match control.
The code comment at line 140 acknowledges this: "When subdomains is false, the domain is prefixed with 'exact:' to signal exact-match-only behavior. However, since the current squid config always adds subdomain matching, we return just the bare domain." But operators reading only the YAML API docs will not see this caveat.
Recommendation: Either implement exact-match support, or raise an error/warning when subdomains: false is specified.
Input Validation Assessment
Command run:
grep -n "exec\|spawn\|execa" src/docker-manager.ts | head -20
grep -n '\$\{' containers/agent/setup-iptables.sh | head -20
Findings (positive):
All Docker commands use execa() with argument arrays, never shell string interpolation — no shell injection risk from CLI inputs.
setup-iptables.sh uses \$\{} variable expansion in iptables calls, but values are validated first with is_valid_port_spec() before use.
The TypeScript isValidPortSpec() and Bash is_valid_port_spec() functions are consistent — both reject leading zeros and enforce [1-9][0-9]{0,4} for ports.
The AWF_SQUID_CONFIG_B64 environment variable encoding prevents Squid config injection via Docker environment passing.
⚠️ Finding (Low) — No size/depth limit on YAML ruleset files
# From src/rules.ts:46-53
content = fs.readFileSync(filePath, 'utf-8');
parsed = yaml.load(content);```The YAML ruleset files loaded via `--ruleset-file` have no file size limit and no protection against deeply-nested YAML (billion laughs attack). While `js-yaml` has some built-in protections, an extremely large or deeply nested YAML file could cause DoS on the CLI process. This is a low risk (local DoS only) but worth addressing.---## ⚠️ Threat Model (STRIDE)| ID | Category | Threat | Evidence | Likelihood | Impact | Severity ||----|----------|--------|----------|------------|--------|----------|| T1 |**Spoofing**| Agent impersonates legitimate traffic by targeting whitelisted domains for SSRF | Squid only enforces domain, not path — requests to `https://github.com/evil-path` are allowed if`github.com` is whitelisted | Medium | Low |**Low**|| T2 |**Tampering**| Agent modifies iptables rules post-setup |`NET_ADMIN` never granted to agent;`capsh` drops `SYS_ADMIN` before user code (`entrypoint.sh:357`) | Very Low | Critical |**Medium**|| T3 |**Tampering**| Container escape via `unshare`/`setns`/`mount` syscalls |`unshare`, `setns`, `mount`, `open_by_handle_at` are in seccomp ALLOW list (`seccomp-profile.json`) | Low | Critical |**High**|| T4 |**Repudiation**| Agent traffic not auditable after container teardown | Squid access.log and iptables audit file are preserved post-cleanup in`/tmp/squid-logs-*`| Low | Medium |**Low**|| T5 |**Information Disclosure**| DNS exfiltration via UDP | UDP port 53 only to whitelisted DNS IPs; all other UDP dropped (`setup-iptables.sh` final DROP rule) | Very Low | Medium |**Low**|| T6 |**Information Disclosure**| API keys readable from `/proc/self/environ`|`unset_sensitive_tokens()` clears env vars pre-exec; one-shot-token LD_PRELOAD prevents re-reads | Low | High |**Medium**|| T7 |**Information Disclosure**|`subdomains: false` bypass — unintended subdomain access |`expandRule()` always returns bare domain; subdomain ACL always added in Squid | Medium | Medium |**High**|| T8 |**Denial of Service**| Log flooding via rate-limited LOG rules |`--limit 5/min --limit-burst 10` applied to dangerous port LOG rules (`setup-iptables.sh`) | Low | Low |**Low**|| T9 |**Elevation of Privilege**|`open_by_handle_at` Shocker-variant host escape | Syscall is ALLOW in seccomp; requires host mount FD access | Low | Critical |**High**|| T10 |**Elevation of Privilege**| SSL Bump CA private key accessible to host root | CA key written to `/tmp/awf-*` workDir on host; only accessible to root | Low | Medium |**Medium**|| T11 |**Tampering**| Squid DNS bypasses container DNS iptables filtering | Squid uses `dns_nameservers` directly; not constrained to container iptables DNS allow rules | Low | Low |**Medium**|---## 🎯 Attack Surface Map| Surface | Location | What it does | Current protections | Risk ||---------|----------|--------------|---------------------|------|| CLI `--allow-domains`|`src/cli.ts:1539`| Accepts domain patterns from user |`validateDomainOrPattern()` + `assertSafeForSquidConfig()`| 🟢 Low || CLI `--ruleset-file`|`src/rules.ts:46`| Loads YAML domain rules from file | Schema validation, but no size limits | 🟡 Medium || CLI `--allow-host-ports`|`src/host-iptables.ts:isValidPortSpec`| Adds port bypass rules |`isValidPortSpec()` validates format | 🟢 Low || Agent container entrypoint |`containers/agent/entrypoint.sh`| Runs user commandin chroot |`capsh` drops capabilities;`no-new-privileges`; seccomp | 🟡 Medium || Squid proxy config gen |`src/squid-config.ts`| Generates `squid.conf`|`assertSafeForSquidConfig()` checks every interpolated value | 🟢 Low || iptables init container |`src/docker-manager.ts:1333`| Sets NAT rules in agent's netns | NET_ADMIN scoped only to init; exits immediately | 🟡 Medium || API proxy sidecar HTTP | `containers/api-proxy/` | Credential injection | Plain HTTP internally; isolated by Docker network | 🟡 Medium || `open_by_handle_at` syscall | `seccomp-profile.json` | Can access host filesystem inodes | Only mitigated by selective bind mounts | 🔴 High || `unshare`/`setns`/`mount` syscalls | `seccomp-profile.json` | Namespace/filesystem operations | Mitigated by capability drop (`capsh`) | 🟡 Medium || DoH proxy HTTPS traffic | `src/docker-manager.ts:1532` | DNS resolution via HTTPS | DoH provider must be whitelisted in Squid | 🟢 Low |---## 📋 Evidence Collection<details><summary>seccomp syscall analysis</summary>```python3 containers/agent/seccomp-profile.json analysis: defaultAction: SCMP_ACT_ERRNO Total syscall rules: 5 Blocked: ptrace, process_vm_readv, process_vm_writev Blocked: kexec_load, kexec_file_load, reboot, init_module, ... Blocked: umount, umount2 ALLOWED (high-risk): mount, unshare, setns, open_by_handle_at```</details><details><summary>Squid rule ordering (domain deny before localnet allow)</summary>```# From src/squid-config.ts lines ~596-602:\$\{accessRulesSection} # allow rules for whitelisted domains\$\{denyRule} # deny !allowed_domains (catches ALL sources)# Allow from trusted sources (after domain filtering)http_access allow localnet # only reached if deny didn't matchhttp_access allow localhosthttp_access deny all
subdomains:false in expandRule()
// src/rules.ts:148-158exportfunctionexpandRule(rule: Rule): string[]{// The existing system already handles subdomain matching when a plain// domain is provided (e.g., "github.com" matches both github.com and// *.github.qkg1.top in Squid config). So we just return the domain.return[rule.domain];// subdomains field is ignored}
Capability drop sequence
# entrypoint.sh:351-364if [ "\$\{AWF_CHROOT_ENABLED}"="true" ];then
CAPS_TO_DROP="cap_sys_chroot,cap_sys_admin"echo"[entrypoint] Chroot mode enabled - dropping CAP_SYS_CHROOT and CAP_SYS_ADMIN"else
CAPS_TO_DROP=""echo"[entrypoint] No capabilities to drop (NET_ADMIN never granted to agent)"fi```</details><details><summary>npm audit result</summary>```
Total vulnerabilities: 0
No known vulnerabilities in any npm dependencies.
✅ Recommendations
🔴 Critical
None identified.
🟠 High — Should Fix Soon
H1: Remove open_by_handle_at from seccomp allowlist (or add to explicit blocklist)
Fix: Add security_opt: ['no-new-privileges:true', 'seccomp=/path/to/iptables-init-seccomp.json'] to the iptables-init service; create a minimal seccomp for iptables operations
M3: Squid PID limit for squid-proxy container
File: src/docker-manager.ts:~453 (squid service definition)
Risk: Squid can spawn unlimited processes (no pids_limit)
Fix: Add pids_limit: 50 to the squid-proxy service
M4: Add YAML ruleset file size guard
File: src/rules.ts:loadRuleSet()
Risk: Billion-laughs or large-file DoS on CLI process
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 covers a deep, evidence-based security analysis of the
gh-aw-firewallcodebase conducted on 2026-04-04. The overall security posture is strong — the architecture is well-thought-out with multiple defence-in-depth layers. No critical vulnerabilities were found. Two high-severity findings relate to a silent security control bypass (subdomains: falsein YAML rulesets has no effect) and container-escape-capable syscalls remaining in the seccomp allowlist (unshare,setns,mount,open_by_handle_at). Several medium and low findings are documented below.🔍 Phase 1: Context from Previous Security Testing
No complementary firewall-escape-test agent results were available (no such workflow found). This analysis stands on its own codebase review.
🛡️ Architecture Security Analysis
Network Security Assessment
Command run:
Findings (positive):
The iptables architecture implements solid layered controls:
IPv6 disabled at init (
setup-iptables.shlines 47–53):DNS pinning — Only configured DNS servers (Google 8.8.8.8 by default) receive UDP/TCP port 53 from inside the agent. All other UDP is dropped by a blanket
iptables -A OUTPUT -p udp -j DROPrule.Dangerous port blocking — 15 ports are explicitly enumerated in
setup-iptables.shmatchingDANGEROUS_PORTSinsquid-config.ts: SSH(22), Telnet(23), SMTP(25), POP3(110), IMAP(143), SMB(445), MSSQL(1433), Oracle(1521), MySQL(3306), RDP(3389), PG(5432), Redis(6379), MongoDB(27017/27018/28017).DNAT redirection — All port 80/443 TCP is redirected to Squid via DNAT before it can leave the container. Proxy-unaware tools are blocked at the TLS level (Squid rejects raw ClientHellos).
Squid rule ordering is correct — The
accessRulesSectionanddenyRuleinsrc/squid-config.ts(line ~596) are emitted BEFOREhttp_access allow localnet. Squid processes rules in order; a request matching thedeny !allowed_domainsrule is rejected before reaching theallow localnetrule. The comment "This applies to all sources including localnet" confirms this intent.Direct IP connection blocking — Squid config includes:
This prevents bypassing domain filtering by connecting to raw IPs.
Host-level DOCKER-USER chain (
src/host-iptables.ts) — A dedicatedFW_WRAPPERchain is created to filter traffic at the Docker bridge layer, providing a second enforcement point outside the container.Squid has its own
dns_nameserversdirective pointing directly to upstream DNS (not through Docker embedded DNS). This means Squid bypasses the container-level DNS filtering enforced by iptables. While Squid's DNS is used only for the proxy's own resolution (and its egress is filtered by the host-level DOCKER-USER chain), this is an architectural asymmetry worth noting.Container Security Assessment
Command run:
cat containers/agent/seccomp-profile.json grep -n "cap_drop\|cap_add\|security_opt\|no-new-privileges\|pids_limit" src/docker-manager.tsFindings (positive):
no-new-privileges:trueis applied to the agent container (src/docker-manager.ts:1235)src/docker-manager.ts:1236)cap_add: ['SYS_CHROOT', 'SYS_ADMIN'](needed for chroot setup), then both dropped viacapshbefore user code runs (entrypoint.sh:357)cap_add: ['NET_ADMIN', 'NET_RAW'],cap_drop: ['ALL']; agent container never holdsNET_ADMIN6gwith swap disabled when user specifies a limitunshare,setns,mount,open_by_handle_atare allowed in the custom seccomp profileunshare— creates new namespaces (network, mount, etc.). WithCAP_SYS_ADMINdropped after chroot setup, this is limited butCLONE_NEWUSER(user namespace creation) may still be possible without capabilities in some kernel configurations.setns— joins another process's namespace. If an agent process obtains a file descriptor to another namespace (e.g., via/proc), it could join it.mount— needed during chroot setup (mounting procfs), but the seccomp profile doesn't remove it after the capability drop phase. AfterCAP_SYS_ADMINis dropped viacapsh,mountitself will fail. However, if there is any capability leak,mountremains available.open_by_handle_at— the "Shocker" container escape vector. This syscall combined withname_to_handle_at(also ALLOW) and a file descriptor to a host filesystem mount point can allow accessing host files outside the container's view. See [CVE-2014-0038]((nvd.nist.gov/redacted) style attacks. While the selective bind mounts reduce exposure, the syscall being allowed is a risk.The
defaultAction: SCMP_ACT_ERRNO(good — block-by-default) combined withptrace,process_vm_readv,process_vm_writevbeing explicitly blocked is positive. But the above four syscalls create residual container escape surface.The iptables-init container has
NET_ADMINandNET_RAWcapabilities and uses Docker's default (more permissive) seccomp profile rather than the AWF custom one. This is acceptable given the container exits immediately after runningsetup-iptables.sh, but represents unnecessary attack surface during its brief lifetime.Domain Validation Assessment
Command run:
Findings (positive):
validateDomainOrPattern()correctly rejects: empty strings,*,*.*, any pattern matching^[*.]+$, and dangerous characters:[\s\0"'`;#\]`.SQUID_DANGEROUS_CHARScheck is applied again at config generation time viaassertSafeForSquidConfig()— double validation for injection prevention.*is converted to[a-zA-Z0-9.-]*(bounded character class), preventing ReDoS in Squid regex evaluation.(domain/redacted) vs(domain/redacted)`) are supported and correctly generate separate Squid ACLs.subdomains: falsein YAML ruleset files is silently ignored — always enables subdomain matchingA user who writes a ruleset like:
...will find that
api.internal.example.comis also allowed, contrary to the documented intent. Thesubdomainsfield is parsed and stored in theRulestruct butexpandRule()ignores it entirely. This creates a false sense of security for operators who use YAML rulesets expecting exact-match control.The code comment at line 140 acknowledges this: "When subdomains is false, the domain is prefixed with 'exact:' to signal exact-match-only behavior. However, since the current squid config always adds subdomain matching, we return just the bare domain." But operators reading only the YAML API docs will not see this caveat.
Recommendation: Either implement exact-match support, or raise an error/warning when
subdomains: falseis specified.Input Validation Assessment
Command run:
Findings (positive):
execa()with argument arrays, never shell string interpolation — no shell injection risk from CLI inputs.setup-iptables.shuses\$\{}variable expansion iniptablescalls, but values are validated first withis_valid_port_spec()before use.isValidPortSpec()and Bashis_valid_port_spec()functions are consistent — both reject leading zeros and enforce[1-9][0-9]{0,4}for ports.AWF_SQUID_CONFIG_B64environment variable encoding prevents Squid config injection via Docker environment passing.subdomains:false in expandRule()
Capability drop sequence
✅ Recommendations
🔴 Critical
None identified.
🟠 High — Should Fix Soon
H1: Remove
open_by_handle_atfrom seccomp allowlist (or add to explicit blocklist)containers/agent/seccomp-profile.jsonopen_by_handle_atandname_to_handle_atto the SCMP_ACT_ERRNO blocklistH2: Document or fix
subdomains: falsein YAML rulesetssrc/rules.ts:expandRule(),docs/, ruleset YAML schemaexact:prefix support sosubdomains: falsegeneratesacl allowed_domains dstdomain exact-domain.com(not.exact-domain.com)subdomains: falseis specified until it is implemented🟡 Medium — Plan to Address
M1: Add
unshare,setnsto seccomp blocklistcontainers/agent/seccomp-profile.jsonunshare(at minimumCLONE_NEWNETpath via argument filter) andsetns; these are not needed by any standard agent workflowM2: Apply seccomp profile to iptables-init container
src/docker-manager.ts:1336security_opt: ['no-new-privileges:true', 'seccomp=/path/to/iptables-init-seccomp.json']to the iptables-init service; create a minimal seccomp for iptables operationsM3: Squid PID limit for squid-proxy container
src/docker-manager.ts:~453(squid service definition)pids_limit)pids_limit: 50to the squid-proxy serviceM4: Add YAML ruleset file size guard
src/rules.ts:loadRuleSet()yaml.load()(e.g., reject files > 1MB)🟢 Low — Nice to Have
L1: Rate-limit Squid log writes for long-running sessions
read_timeoutis 30 minutes — long-lived SSE connections could generate large access.log entries in busy sessionsL2: Add an explicit note to ruleset YAML schema about URL-path filtering not being supported
--allow-domains github.qkg1.topallows all paths on github.com; operators may not realize URL-path filtering requires--url-patterns(SSL Bump)📈 Security Metrics
Beta Was this translation helpful? Give feedback.
All reactions