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 is an evidence-based security review of the gh-aw-firewall repository, covering ~6,986 lines of security-critical code across 6 core files. The overall security posture is strong with well-layered defenses: mandatory Squid proxy, iptables NAT redirection, selective bind mounts, capability dropping, seccomp filtering, one-shot token protection, and DNS exfiltration controls. Three items warrant attention.
Severity
Count
High
1
Medium
3
Low
3
🔍 Findings from Firewall Escape Test
Previous run data was unavailable due to git not being in PATH during the workflow-log download. The analysis below is based entirely on static codebase review.
🛡️ Architecture Security Analysis
Network Security Assessment ✅ Strong
Evidence collected:
# Host-level iptables (src/host-iptables.ts)# Dedicated FW_WRAPPER chain in DOCKER-USER (affects all containers on awf-net)# Rule ordering (lines 248–529):# 1. ACCEPT traffic FROM Squid (it needs unrestricted egress)# 2. ACCEPT ESTABLISHED,RELATED# 3. ACCEPT localhost# 4. ACCEPT DNS only to configured upstream servers (prevents DNS exfiltration)# 5. ACCEPT traffic TO Squid (forward proxy)# 6. REJECT everything else (icmp-port-unreachable)
All TCP/UDP from the agent that isn't DNAT'd to Squid hits the REJECT rule. The host-side DOCKER-USER chain is the second line of defense (after iptables NAT inside the container namespace).
NAT chain: RETURN for localhost, self-IP, DNS, Squid, API proxy; DNAT ports 80 and 443 → Squid:3128
Filter chain OUTPUT: ACCEPT loop, Squid, DNS (UDP), then LOG+DROP for all TCP and UDP
Gap identified: ICMP is not explicitly covered by the DROP rules in setup-iptables.sh (lines 404–407 only drop TCP and UDP). However, NET_RAW is dropped from the agent container (docker-manager.ts:1152), which prevents raw socket creation for ICMP tunneling. The host's FW_WRAPPER chain terminates unrecognized traffic with icmp-port-unreachable, which effectively blocks ICMP egress at the network level as well.
IPv6 handling: Both host (src/host-iptables.ts) and container (setup-iptables.sh) detect ip6tables availability and fall back to disabling IPv6 via sysctl when ip6tables is unavailable — preventing an unfiltered IPv6 bypass path.
DNS exfiltration prevention: Container resolv.conf is overwritten to contain only 127.0.0.11 (Docker embedded DNS). iptables only allows UDP/TCP port 53 to the explicitly configured upstream servers. This is well-designed.
Container Security Assessment ⚠️ Adequate with noted gaps
Capability model (src/docker-manager.ts:1149–1157):
cap_add: ['SYS_CHROOT','SYS_ADMIN'],cap_drop: ['NET_RAW',// Prevents raw socket creation'SYS_PTRACE',// Prevents process inspection'SYS_MODULE',// Prevents kernel module loading'SYS_RAWIO',// Prevents raw I/O access'MKNOD',// Prevents device node creation],
SYS_ADMIN and SYS_CHROOT are added for the entrypoint (procfs mount + chroot), then explicitly dropped by capsh before user code runs (containers/agent/entrypoint.sh:338):
'apparmor:unconfined',```This is intentional — Docker's default AppArmor profile blocks `mount`, which is required for `mount-tproc`. Since `SYS_ADMIN` is dropped before user code runs, a user cannot abuse `mount`. But AppArmor provides defense-in-depth that is absent here. If `capsh` were to fail silently (e.g., binary missing in an edge case), user code would retain `SYS_ADMIN` with no AppArmor backstop.**Seccomp profile (containers/agent/seccomp-profile.json):**- Default action: `SCMP_ACT_ERRNO` ✅- Allowed dangerous syscalls: `mount`, `setuid`, `setgid`, `unshare`, `clone`, `chroot`, `capset`, `prctl`- Explicitly blocked: `ptrace`, `process_vm_readv`, `process_vm_writev`, `kexec_load`, kernel module syscalls, `pivot_root`The profile is intentionally permissive to support broad tool compatibility (git, npm, Java, Python, Rust). Combined with capability dropping, the effective risk from allowed syscalls like `mount` is low — you can call `mount(2)` but the kernel will reject it without `CAP_SYS_ADMIN`.**iptables-init isolation:** `NET_ADMIN` is granted only to the `awf-iptables-init` init container which shares the agent's network namespace. The agent itself never holds `NET_ADMIN`. This is an excellent design that eliminates the startup window where the agent could modify its own firewall rules.---### Domain Validation Assessment ✅ Strong```
# src/domain-patterns.tsvalidatesagainstoverly-broadpatterns:
'*'→rejected: "Pattern '*' matches all domains and is not allowed"'*.*'→ rejected: "Pattern '*.*' is too broad and is not allowed"'*.*.*'→rejectedbywildcard-segmentratiocheck'.*'→ rejected: "Invalid pattern '.*': incomplete domain"```Wildcard-to-regex conversion (line ~90) uses `[a-zA-Z0-9.-]*` instead of `.*` — prevents catastrophic ReDoS backtracking. Input length is capped at 512 bytes before regex matching.Protocol-specific allow (`(domain.com/redacted)vs`(domain.com/redacted)`vsboth) is implementedcorrectlywithseparateSquidACLs.---
### InputValidationAssessment✅AdequateShellargumentconstructionin`src/cli.ts:932–949`usessingle-quoteescapingformulti-argumentmode.Single-argumentmodeintentionallypassesthestringunescapedtoletthecontainershellexpandvariables—thisisdocumentedandintentional.
`execa` is used throughout (not `exec`/`spawn` with `shell:true`), which prevents shell injection via array argument passing.UID/GID validation in `containers/agent/entrypoint.sh` (lines ~16–35) rejects non-numeric values and UID/GID = 0.---## ⚠️ Threat Model (STRIDE)| # | Category | Threat | Severity | Likelihood | Evidence ||---|----------|--------|----------|------------|----------|| T1 | **Elevation of Privilege** | `apparmor:unconfined` + SYS_ADMIN window: if `capsh` fails or is not on host PATH, user code retains SYS_ADMIN with no AppArmor backstop | **High** | Low | `docker-manager.ts:1159–1165`; `entrypoint.sh:429–431` || T2 | **Information Disclosure** | `--enable-dind` flag mounts live Docker socket (`/var/run/docker.sock`), giving agent full container spawn capability without AWF network restrictions | **Medium** | Low | `docker-manager.ts:922–930` || T3 | **Information Disclosure** | `--env-all` passes the host environment into the container. While API keys are excluded when `--enable-api-proxy` is on, other sensitive env vars not in `EXCLUDED_ENV_VARS` (AWS creds, SSH keys as env, etc.) may be forwarded | **Medium** | Medium | `docker-manager.ts:467–477`, `596–605` || T4 | **Tampering** | Log rate limiting (`--limit10/min`) on DROP rules means a high-volume exfiltration attempt may generate only 10 kernel log lines per minute — incomplete forensic record | **Medium** | Medium | `setup-iptables.sh:404–407` || T5 | **Information Disclosure** | ICMP not explicitly dropped in agent OUTPUT chain — ICMP echo can be sent outbound. NET_RAW drop prevents raw ICMP sockets, but mitigates rather than fully eliminates the covert channel | **Low** | Low | `setup-iptables.sh:404–407`; `docker-manager.ts:1152` || T6 | **Denial of Service** | `brace-expansion` transitive dependency has a ReDoS issue (process hang) on malicious input patterns | **Low** | Low | `npmaudit` output || T7 | **Repudiation** | Squid access logs use Unix timestamps requiring post-hoc conversion; no centralized log aggregation outside the container — logs are lost if container removed before `--keep-containers` | **Low** | Medium | `src/squid-config.ts:40`; cleanup logic |---## 🎯 Attack Surface Map| Entry Point | File:Line | What It Does | Current Protections | Risk ||-------------|-----------|--------------|---------------------|------|| `--allow-domains` CLI arg | `src/cli.ts:1411` | Injects domains into Squid ACL | `validateDomainOrPattern()`, wildcard limits, ReDoS-safe regex | Low || `--env` / `--env-all` / `--env-file` | `docker-manager.ts:596–648` | Forwards env vars into container | `EXCLUDED_ENV_VARS` set, API keys removed when api-proxy enabled | Medium || Docker socket (`/var/run/docker.sock`) | `docker-manager.ts:922–937` | Full Docker access if `--enable-dind` | Masked with `/dev/null` by default; explicit opt-in only | Medium (when enabled) || User command string | `cli.ts:1422–1444` | Executed inside container | Shell-escaped (multi-arg); single-arg intentionally unescaped for variable expansion | Low || `AWF_USER_UID` / `AWF_USER_GID` env vars | `entrypoint.sh:16–35` | Sets container user identity | Numeric validation, UID=0 rejected | Low || Squid config (base64 env var) | `docker-manager.ts:441` | Configures proxy ACLs | No bind mount (DinD-safe), base64 decode in container only | Low || `--enable-host-access` gateway | `docker-manager.ts:445–543` | Bypasses Squid for host gateway | Opt-in only; iptables allow only ports 80/443 + user-specified ports | Medium (when enabled) || SSL bump CA key | `ssl-bump.ts:70,209` | TLS MITM for content inspection | Stored in tmpfs (size=4m, noexec, nosuid); dropped before container exit | Low (when enabled) || MCP log path | `docker-manager.ts:1130` | MCP server log area | tmpfs overlay (1MB, noexec, nosuid) blocks agent reads | Low || workDir (`/tmp/awf-*`) | `docker-manager.ts:1132` | Contains docker-compose.yml with secrets | tmpfs overlay (1MB) prevents container from reading | Low |---## 📋 Evidence Collection<details><summary>npm audit (dependency vulnerabilities)</summary>```Total vulnerabilities: 2(1critical,1moderate)
Package: handlebars(CRITICAL-notaruntimedependency)
Severity: critical
Via: JSinjectioninCLIprecompiler,prototypepollution,ASTtypeconfusionIsdirect dep: False(transitivedevtoolingchain)
Runtime risk: NONE(notindependencies,onlydevDependencieschain)
Package: brace-expansion(MODERATE)
Severity: moderate
Via: Zero-stepsequencecausesprocesshangandmemoryexhaustionIsdirect dep: False(transitive)
Runtime risk: LOW(onlytriggeredbyuser-controlledglobpatterns)```</details><details><summary>Seccomp profile analysis</summary>```
Default action: SCMP_ACT_ERRNO✅deny-by-defaultTotalsyscallrule groups: 5ALLOWgroup(large): mount,setuid,setgid,unshare,clone,chroot,capset,prctl→Thesesucceedonlywhileprocessholdsthecorrespondingcapability→AftercapshdropsSYS_CHROOT+SYS_ADMIN,mount/chroot/capsetareeffectivelyblockedDENY explicit: ptrace,process_vm_readv/writev,kexec_load,kernelmodulesyscalls,pivot_root,keyctl,reboot,syslog```</details><details><summary>Capability matrix</summary>```Containercap_addcap_drop─────────────────────────────────────────────────────────squid-proxy(default)SYS_PTRACE,SYS_MODULE,SYS_RAWIO,SYS_ADMIN,MKNOD,SETFCAPagentSYS_CHROOT,SYS_ADMINNET_RAW,SYS_PTRACE,SYS_MODULE,SYS_RAWIO,MKNOD(droppedbycapshbeforeusercoderuns)iptables-initNET_ADMIN,NET_RAWALL(exceptNET_ADMIN,NET_RAW)api-proxy(noneadded)ALLdoh-proxy(noneadded)ALL
H1 — Harden AppArmor unconfined with fallback capability check
The agent uses apparmor:unconfined so the entrypoint can run mount -t proc. While SYS_ADMIN is dropped by capsh before user code runs, if capsh is unavailable on the host, the entrypoint logs an error and exits (entrypoint.sh:431) — but in a degraded failure mode. A defense-in-depth improvement would be to write and ship a custom AWF AppArmor profile that allows only the specific mount call (tmpfs/proc) rather than disabling AppArmor entirely.
# Proposed minimal AppArmor profile approach:# apparmor:awf-agent (custom profile shipped in containers/agent/)# Allows: mount -t proc/tmpfs only; denies: module loading, capability modification, etc.
Effort: Medium | Impact: High
🟡 Medium Priority
M1 — Document --enable-dind risk more prominently
--enable-dind mounts the live Docker socket into the agent's chroot. A compromised agent with Docker socket access can launch new containers bypassing AWF network restrictions entirely. The CLI currently logs a warning (docker-manager.ts:924) but there is no prominent documentation warning in the help text.
Recommendation: Add a visible [SECURITY RISK] marker to the --enable-dind help string in src/cli.ts, and consider requiring an additional acknowledgment flag (e.g., --enable-dind --i-understand-dind-bypasses-firewall).
M2 — Add additional sensitive env var exclusions to EXCLUDED_ENV_VARS
The current EXCLUDED_ENV_VARS set (docker-manager.ts:467) only excludes shell metadata (PATH, PWD, sudo vars). When --env-all is used, host vars like AWS_SECRET_ACCESS_KEY, AZURE_CLIENT_SECRET, VAULT_TOKEN, or SSH keys stored as environment variables are forwarded to the agent container.
Recommendation: Expand the base EXCLUDED_ENV_VARS with common cloud credential patterns (AWS_*_KEY, AZURE_*_SECRET, ARM_*_KEY, VAULT_TOKEN, etc.), independent of the api-proxy flag. These are never needed in the agent container.
M3 — Increase LOG rate limits or use connection tracking for audit completeness
Current rate limits (--limit 10/min --limit-burst 20) on DROP rules in setup-iptables.sh mean a burst of >20 blocked packets generates only 10 log entries per minute. For security audit purposes, consider using --limit 60/min --limit-burst 60 or using conntrack state-based logging (log only new connections) to capture the first blocked attempt per flow.
🟢 Low Priority
L1 — Explicitly block ICMP in agent OUTPUT chain
While NET_RAW is dropped (preventing raw socket ICMP construction), adding an explicit iptables -A OUTPUT -p icmp -j DROP rule to setup-iptables.sh would eliminate any residual ICMP covert channel risk and make the intent explicit in the ruleset.
Run npm audit fix or pin the transitive dependency versions:
brace-expansion: update to >=2.0.2 which resolves the ReDoS issue
handlebars is not a runtime dependency (not in dependencies or devDependencies directly) — identify which dev tool chain introduces it and pin or remove
L3 — Squid log timestamp readability
Squid uses Unix timestamps in access logs (src/squid-config.ts:40). Consider adding a human-readable timestamp field to the firewall_detailed logformat to ease forensic analysis without requiring date -d @TIMESTAMP`` conversion.
📈 Security Metrics
Metric
Value
Lines of security-critical code analyzed
6,986
Attack surfaces identified
8
Threats modeled (STRIDE)
7
Critical vulnerabilities
0
High findings
1
Medium findings
3
Low findings
3
Runtime dependency vulnerabilities
0 (handlebars/brace-expansion are transitive dev deps)
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 is an evidence-based security review of the
gh-aw-firewallrepository, covering ~6,986 lines of security-critical code across 6 core files. The overall security posture is strong with well-layered defenses: mandatory Squid proxy, iptables NAT redirection, selective bind mounts, capability dropping, seccomp filtering, one-shot token protection, and DNS exfiltration controls. Three items warrant attention.🔍 Findings from Firewall Escape Test
Previous run data was unavailable due to
gitnot being in PATH during the workflow-log download. The analysis below is based entirely on static codebase review.🛡️ Architecture Security Analysis
Network Security Assessment ✅ Strong
Evidence collected:
All TCP/UDP from the agent that isn't DNAT'd to Squid hits the REJECT rule. The host-side DOCKER-USER chain is the second line of defense (after iptables NAT inside the container namespace).
Container-level iptables (containers/agent/setup-iptables.sh):
Gap identified: ICMP is not explicitly covered by the DROP rules in
setup-iptables.sh(lines 404–407 only drop TCP and UDP). However,NET_RAWis dropped from the agent container (docker-manager.ts:1152), which prevents raw socket creation for ICMP tunneling. The host'sFW_WRAPPERchain terminates unrecognized traffic withicmp-port-unreachable, which effectively blocks ICMP egress at the network level as well.IPv6 handling: Both host (
src/host-iptables.ts) and container (setup-iptables.sh) detect ip6tables availability and fall back to disabling IPv6 via sysctl when ip6tables is unavailable — preventing an unfiltered IPv6 bypass path.DNS exfiltration prevention: Container
resolv.confis overwritten to contain only127.0.0.11(Docker embedded DNS). iptables only allows UDP/TCP port 53 to the explicitly configured upstream servers. This is well-designed.Container Security Assessment⚠️ Adequate with noted gaps
Capability model (src/docker-manager.ts:1149–1157):
SYS_ADMINandSYS_CHROOTare added for the entrypoint (procfs mount + chroot), then explicitly dropped bycapshbefore user code runs (containers/agent/entrypoint.sh:338):CAPS_TO_DROP="cap_sys_chroot,cap_sys_admin"AppArmor disabled (Medium finding):
src/docker-manager.ts:1165:AppArmor / seccomp settings per container
✅ Recommendations
🔴 High Priority
H1 — Harden AppArmor unconfined with fallback capability check
The agent uses
apparmor:unconfinedso the entrypoint can runmount -t proc. WhileSYS_ADMINis dropped bycapshbefore user code runs, ifcapshis unavailable on the host, the entrypoint logs an error and exits (entrypoint.sh:431) — but in a degraded failure mode. A defense-in-depth improvement would be to write and ship a custom AWF AppArmor profile that allows only the specificmountcall (tmpfs/proc) rather than disabling AppArmor entirely.Effort: Medium | Impact: High
🟡 Medium Priority
M1 — Document
--enable-dindrisk more prominently--enable-dindmounts the live Docker socket into the agent's chroot. A compromised agent with Docker socket access can launch new containers bypassing AWF network restrictions entirely. The CLI currently logs a warning (docker-manager.ts:924) but there is no prominent documentation warning in the help text.Recommendation: Add a visible
[SECURITY RISK]marker to the--enable-dindhelp string insrc/cli.ts, and consider requiring an additional acknowledgment flag (e.g.,--enable-dind --i-understand-dind-bypasses-firewall).M2 — Add additional sensitive env var exclusions to
EXCLUDED_ENV_VARSThe current
EXCLUDED_ENV_VARSset (docker-manager.ts:467) only excludes shell metadata (PATH,PWD, sudo vars). When--env-allis used, host vars likeAWS_SECRET_ACCESS_KEY,AZURE_CLIENT_SECRET,VAULT_TOKEN, or SSH keys stored as environment variables are forwarded to the agent container.Recommendation: Expand the base
EXCLUDED_ENV_VARSwith common cloud credential patterns (AWS_*_KEY,AZURE_*_SECRET,ARM_*_KEY,VAULT_TOKEN, etc.), independent of the api-proxy flag. These are never needed in the agent container.M3 — Increase LOG rate limits or use connection tracking for audit completeness
Current rate limits (
--limit 10/min --limit-burst 20) on DROP rules insetup-iptables.shmean a burst of >20 blocked packets generates only 10 log entries per minute. For security audit purposes, consider using--limit 60/min --limit-burst 60or usingconntrackstate-based logging (log only new connections) to capture the first blocked attempt per flow.🟢 Low Priority
L1 — Explicitly block ICMP in agent OUTPUT chain
While
NET_RAWis dropped (preventing raw socket ICMP construction), adding an explicitiptables -A OUTPUT -p icmp -j DROPrule tosetup-iptables.shwould eliminate any residual ICMP covert channel risk and make the intent explicit in the ruleset.L2 — Address transitive dependency vulnerabilities
Run
npm audit fixor pin the transitive dependency versions:brace-expansion: update to>=2.0.2which resolves the ReDoS issuehandlebarsis not a runtime dependency (not independenciesordevDependenciesdirectly) — identify which dev tool chain introduces it and pin or removeL3 — Squid log timestamp readability
Squid uses Unix timestamps in access logs (
src/squid-config.ts:40). Consider adding a human-readable timestamp field to thefirewall_detailedlogformat to ease forensic analysis without requiringdate -d@TIMESTAMP`` conversion.📈 Security Metrics
Beta Was this translation helpful? Give feedback.
All reactions