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 the gh-aw-firewall codebase as of 2026-04-19. The system provides L7 HTTP/HTTPS egress control via Squid proxy with iptables enforcement, chroot-based filesystem isolation, and capability dropping. Overall, the security architecture is well-structured with multiple layers of defense. The most significant risks are a SYS_ADMIN window during agent startup, the absence of complete AppArmor confinement, and limited non-HTTP protocol blocking.
The pre-fetched escape test results (/tmp/gh-aw/escape-test-summary.txt) came from the "Secret Digger (Copilot)" workflow (run 24273493151, 2026-04-11). Key observations:
Conclusion: success with noop outputs — the agent completed but found nothing to report, indicating no secrets were exfiltrated and no escape succeeded in that run.
GH_AW_SECRET_VERIFICATION_RESULT: success — the verification harness confirmed the result.
GH_AW_INFERENCE_ACCESS_ERROR: false — no inference access errors (network blocking worked).
Interpretation: The Secret Digger escape test agent ran under AWF confinement and was unable to exfiltrate credentials or break out. This is a positive signal confirming that the primary HTTP/HTTPS egress controls work as intended against an LLM-based adversary.
🛡️ Architecture Security Analysis
Network Security Assessment
Evidence collected:
src/host-iptables.ts (703 lines) — host-level DOCKER-USER chain filtering
containers/agent/setup-iptables.sh (460 lines) — container NAT + OUTPUT filter
src/squid-config.ts (894 lines) — L7 domain ACL generation
```
**Strengths:**
- IPv6 is explicitly disabled in the container namespace via `sysctl net.ipv6.conf.all.disable_ipv6=1` and at the host level via ip6tables — prevents Happy Eyeballs IPv6 bypass (`setup-iptables.sh:~line 42`).
- DNAT fallback catches proxy-unaware tools that ignore `HTTP_PROXY`/`HTTPS_PROXY` env vars.
- DNS is locked to Docker's embedded resolver (`127.0.0.11`) only in `/etc/resolv.conf`; upstream servers are strictly whitelisted (`entrypoint.sh:~line 66–80`).
- Dangerous ports blocked at NAT + OUTPUT layers matching `DANGEROUS_PORTS` in `squid-config.ts:~line 56–76`.
- Squid domain injection is prevented by `assertSafeForSquidConfig()` / `SQUID_DANGEROUS_CHARS = /[\s\0"'\``;#]/` (`domain-patterns.ts:27`).
**Weaknesses / Findings:**
**[HIGH-1] ICMP and raw socket egress is unblocked for proxy-aware paths.** The iptables OUTPUT chain has a final `DROP` rule for TCP and UDP, but ICMP (`-p icmp`) is not explicitly DROPped in the container's OUTPUT filter chain. An agent that can craft ICMP packets (if any residual raw socket capability exists) could exfiltrate data via ICMP tunneling.
```
# setup-iptables.sh (near end of file):
iptables -A OUTPUT -p tcp -j DROP
iptables -A OUTPUT -p udp ... -j DROP
# No: iptables -A OUTPUT -p icmp -j DROP ← missing
```
**[MED-1] `HTTP_PROXY` (lowercase) is intentionally NOT set, documented in AGENTS.md:** "curl on Ubuntu 22.04 ignores uppercase `HTTP_PROXY` for HTTP (httpoxy mitigation), so HTTP falls through to iptables DNAT → Squid." This is a design choice but means plain-HTTP traffic from tools that do respect `http_proxy` (e.g., Python `requests`, `wget`) goes through DNAT rather than an explicit proxy, making the control path slightly less obvious and harder to audit.
**[MED-2] Squid operates as a forward proxy for HTTP, not a transparent intercept proxy for HTTPS.** TLS-aware tools use `HTTPS_PROXY` for `CONNECT`. Proxy-unaware TLS tools hit DNAT and their raw `ClientHello` is rejected. This works for blocking but produces a TLS error rather than an HTTP 403 — some agents may interpret this as a transient network error and retry, potentially masking blocked access attempts in logs.
### Container Security Assessment
**Evidence collected:**
```
src/docker-manager.ts:1529 cap_add: ['SYS_CHROOT', 'SYS_ADMIN']
src/docker-manager.ts:1531 cap_drop: [NET_RAW, ...]
src/docker-manager.ts:1538 security_opt: ['no-new-privileges:true', 'seccomp=...', 'apparmor:unconfined']
src/docker-manager.ts:1683 iptables-init: cap_add: ['NET_ADMIN', 'NET_RAW'], cap_drop: ['ALL']
```
**Strengths:**
- `NET_ADMIN` is confined to the short-lived `awf-iptables-init` init container, not the agent container.
- `no-new-privileges:true` prevents `setuid`/`setgid` privilege escalation after container start.
- Custom seccomp profile (`containers/agent/seccomp-profile.json`) is applied to restrict syscalls.
- `SYS_CHROOT` and `SYS_ADMIN` are dropped via `capsh` before user code runs (in `entrypoint.sh`).
**Weaknesses / Findings:**
**[CRITICAL-1] AppArmor is explicitly set to `unconfined` on the agent container** (`docker-manager.ts:1545`). The comment explains this is needed because Docker's default AppArmor profile blocks `mount` (which `SYS_ADMIN` needs for procfs). However, during the window between container start and when `capsh` drops capabilities, the container runs with `SYS_ADMIN` + no AppArmor confinement. A race-condition exploit or a bug in the entrypoint script that crashes before the `capsh` capability drop could leave the container with full `SYS_ADMIN` and no AppArmor confinement. A custom AppArmor profile that permits only the specific mount operations needed while denying other dangerous operations would close this window.
```
# docker-manager.ts:1542–1545
security_opt: [
'no-new-privileges:true',
`seccomp=\$\{config.workDir}/seccomp-profile.json`,
'apparmor:unconfined', // ← no AppArmor confinement on the agent
],
```
**[HIGH-2] `SYS_ADMIN` is granted to the agent container during the entire startup sequence**, not just for the specific `mount` call. If the entrypoint script is exploited or crashes before `capsh` drops capabilities, the agent could perform privileged mount operations, access kernel namespaces, or manipulate network interfaces — all protected by `SYS_ADMIN`. Mitigations: use a minimal wrapper that does only the mount and immediately drops caps, or use a separate privileged init container (similar to `awf-iptables-init`) for the mount operation.
**[MED-3] UID/GID 0 (root) is blocked, but UIDs 1–999 are also blocked** (`docker-manager.ts:124`: "Reject system UIDs/GIDs (0-999)"). If the host user runs with a legitimate non-zero UID < 1000 (possible in some CI environments), the container silently falls back to UID 1000. This could cause file ownership mismatches but is a safe default.
### Domain Validation Assessment
**Evidence collected:**
```
src/domain-patterns.ts:27 SQUID_DANGEROUS_CHARS = /[\s\0"'`;#]/
src/squid-config.ts:~line 95 assertSafeForSquidConfig(value)
src/cli.ts:1052 escapeShellArg(arg)
```
**Strengths:**
- Domain injection into `squid.conf` is guarded by `assertSafeForSquidConfig()` which throws on dangerous characters.
- Wildcard domains (`*.github.com`) are converted to Squid `dstdom_regex` ACLs with proper escaping (dots escaped, `*` converted to `[^.]*`).
- Protocol-specific restrictions (`(redacted) `https://`) allow fine-grained allow-listing.
**Weaknesses / Findings:**
**[MED-4] Backslash (`\`) is excluded from `SQUID_DANGEROUS_CHARS`** by design to allow URL regex patterns. The comment in `domain-patterns.ts:27` notes "backslash is intentionally excluded here because URL regex patterns ... legitimately use `\`." However, a domain passed as a plain domain (not a URL regex) that contains `\` could potentially confuse the Squid config parser in edge cases. The `validateDomainOrPattern()` function is supposed to reject `\` for plain domains, but the defense relies on correct routing between the two code paths.
### Input Validation Assessment
**Evidence collected:**
```
src/cli.ts:1052 escapeShellArg(): wraps in single quotes, escapes embedded single quotes
src/docker-manager.ts:124 getSafeHostUid/Gid: validates numeric, blocks 0–999
containers/agent/entrypoint.sh:~line 14 validates AWF_USER_UID/GID regex [0-9]+, blocks 0
src/host-iptables.ts:~line 45 isValidPortSpec(): validates N-M or N format, no leading zeros
UID/GID validation occurs at both the TypeScript layer and the bash entrypoint (defense-in-depth).
Port spec validation rejects leading zeros (aligns TypeScript and bash logic).
[HIGH-3] The agent user command is passed as a shell string (/bin/bash -c "<cmd>") inside the container. The CLI escapes CLI arguments with escapeShellArg, but the entire command can still be a single shell string provided by the user containing arbitrary shell constructs. This is by design (users want shell pipelines), but it means the boundary between "shell injection" and "intended shell usage" is entirely the responsibility of the operator providing the command — there is no additional sandboxing of the shell command itself beyond the container boundary.
⚠️ Threat Model (STRIDE)
ID
Category
Threat
Likelihood
Impact
Evidence
T1
Spoofing
Agent spoofs DNS to redirect allowed domains to attacker IPs
Low
High
DNS locked to 127.0.0.11 + upstream whitelist; host iptables blocks unauthorized DNS servers
T2
Spoofing
Agent forges Squid-recognized Host header to bypass ACL
Low
High
Squid uses SNI for HTTPS CONNECT, not just Host header
T3
Tampering
Malicious domain injected via --allow-domains to break squid.conf
Low
Critical
assertSafeForSquidConfig() prevents this
T4
Tampering
Agent modifies iptables after init container exits
Low
Critical
Agent has no NET_ADMIN; no-new-privileges blocks escalation
T5
Repudiation
Agent denies network access; no Squid logs available
Low
Medium
Squid logs preserved automatically; access.log records all traffic
T6
Information Disclosure
Agent reads /etc/shadow or host secrets via bind mount
Low
Critical
/etc/shadow explicitly excluded from bind mounts
T7
Information Disclosure
Agent reads workDir (squid.conf, API keys) via bind mount
Low
High
tmpfs overlay hides workDir from agent container
T8
Information Disclosure
Agent exfiltrates data via ICMP
Medium
High
ICMP not DROPped in container OUTPUT chain (Finding HIGH-1)
T9
Information Disclosure
Agent exfiltrates via DNS queries to whitelisted servers
Low
Medium
DNS restricted to configured upstream servers only
T10
Denial of Service
Agent consumes all Docker subnet IPs or network resources
SYS_ADMIN grant (src/docker-manager.ts:1524–1529):
// SYS_CHROOT is required for chroot operations.// SYS_ADMIN is required to mount procfs at /host/proc// Security: SYS_CHROOT and SYS_ADMIN are dropped before running user commandscap_add: ['SYS_CHROOT','SYS_ADMIN'],
Missing ICMP DROP (containers/agent/setup-iptables.sh, final rules):
iptables -A OUTPUT -p tcp -j DROP
iptables -A OUTPUT -p udp ... -j DROP
# ICMP has no DROP rule
// Reject system UIDs/GIDs (0-999) - use default unprivileged user insteadif(id<MIN_REGULAR_UID){returnMIN_REGULAR_UID.toString();}```**Secret Digger escape test result** (`/tmp/gh-aw/escape-test-summary.txt`):```
GH_AW_SECRET_VERIFICATION_RESULT: success
GH_AW_INFERENCE_ACCESS_ERROR: falseAgentsucceededwithonlynoopoutputs-thisisnotafailure
✅ Recommendations
🔴 Critical
C1 — Create a custom AppArmor profile for the agent container instead of apparmor:unconfined. The profile should permit the specific mount syscall needed for procfs while denying other dangerous operations (arbitrary mount, ptrace, module loading, etc.). This closes the privilege escalation window during the SYS_ADMIN startup phase. Reference: Docker AppArmor documentation; use aa-genprof as a starting point.
🟠 High
H1 — Add ICMP DROP rule to the container OUTPUT chain in setup-iptables.sh. Add:
iptables -A OUTPUT -p icmp -j DROP
This prevents ICMP-based data exfiltration (ping tunnels, ICMP echo covert channels). Also add a LOG rule before the DROP for visibility.
H2 — Reduce SYS_ADMIN exposure window by moving the procfs mount into a dedicated privileged init container (analogous to awf-iptables-init). The agent container should receive neither SYS_CHROOT nor SYS_ADMIN at startup; only SYS_CHROOT strictly needed for the chroot() syscall itself needs to be present during execution, and can be handled differently.
H3 — Document and enforce the shell injection boundary for awf -- <command>. Consider adding a warning when the command contains shell metacharacters that could be interpreted dangerously, and provide a --no-shell mode that passes arguments directly via exec form rather than bash -c.
🟡 Medium
M1 — Add an explicit ICMP LOG rule before the DROP for auditability (analogous to existing [FW_BLOCKED_UDP_AGENT] LOG rules).
M2 — Validate that validateDomainOrPattern() correctly rejects backslashes in plain domain paths and add a test case for domain\ninjected_directive to confirm the two code paths (plain domain vs. URL regex) are correctly separated.
M3 — Consider setting http_proxy (lowercase) with a specific value that returns a non-zero exit code on blocked requests, or document clearly why it must remain unset — the current behavior where plain HTTP goes via DNAT is correct but non-obvious and should be verified on each Ubuntu LTS upgrade.
M4 — Add a seccomp rule to block ptrace and process_vm_readv/process_vm_writev if not already in seccomp-profile.json — these can be used to read memory of other processes in the container and potentially extract credentials from the API proxy sidecar process.
🟢 Low
L1 — Periodic review of DANGEROUS_PORTS list against current threat landscape (e.g., add port 11211 for Memcached, 2379/2380 for etcd).
L2 — Consider rate-limiting DNS queries in iptables to detect DNS-based exfiltration attempts even to whitelisted servers.
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 the gh-aw-firewall codebase as of 2026-04-19. The system provides L7 HTTP/HTTPS egress control via Squid proxy with iptables enforcement, chroot-based filesystem isolation, and capability dropping. Overall, the security architecture is well-structured with multiple layers of defense. The most significant risks are a
SYS_ADMINwindow during agent startup, the absence of complete AppArmor confinement, and limited non-HTTP protocol blocking.🔍 Findings from Firewall Escape Test
The pre-fetched escape test results (
/tmp/gh-aw/escape-test-summary.txt) came from the "Secret Digger (Copilot)" workflow (run24273493151, 2026-04-11). Key observations:successwith noop outputs — the agent completed but found nothing to report, indicating no secrets were exfiltrated and no escape succeeded in that run.GH_AW_SECRET_VERIFICATION_RESULT: success— the verification harness confirmed the result.GH_AW_INFERENCE_ACCESS_ERROR: false— no inference access errors (network blocking worked).GH_AW_LOCKDOWN_CHECK_FAILED: false— lockdown checks passed.Interpretation: The Secret Digger escape test agent ran under AWF confinement and was unable to exfiltrate credentials or break out. This is a positive signal confirming that the primary HTTP/HTTPS egress controls work as intended against an LLM-based adversary.
🛡️ Architecture Security Analysis
Network Security Assessment
Evidence collected:
Strengths:
[HIGH-3] The agent user command is passed as a shell string (
/bin/bash -c "<cmd>") inside the container. The CLI escapes CLI arguments withescapeShellArg, but the entire command can still be a single shell string provided by the user containing arbitrary shell constructs. This is by design (users want shell pipelines), but it means the boundary between "shell injection" and "intended shell usage" is entirely the responsibility of the operator providing the command — there is no additional sandboxing of the shell command itself beyond the container boundary.127.0.0.11+ upstream whitelist; host iptables blocks unauthorized DNS servers--allow-domainsto break squid.confassertSafeForSquidConfig()prevents thisNET_ADMIN;no-new-privilegesblocks escalationaccess.logrecords all traffic/etc/shadowor host secrets via bind mount/etc/shadowexplicitly excluded from bind mounts172.30.0.0/24; cleanup scripts prevent accumulation/tmp, not host rootSYS_ADMINbefore capsh dropunconfined+ SYS_ADMIN window (Finding CRITICAL-1)setuidbinaries in chrootno-new-privileges:trueblocks setuid escalation🎯 Attack Surface Map
assertSafeForSquidConfig, DNAT fallback/hostbind mounts/etc/shadow, tmpfs on workDir--allow-domainsCLI argassertSafeForSquidConfig, regex escapingawf -- <command>CLI argescapeShellArg, container boundary📋 Evidence Collection
Key code references
Domain injection guard (
src/domain-patterns.ts:27):AppArmor unconfined (
src/docker-manager.ts:1542–1545):SYS_ADMIN grant (
src/docker-manager.ts:1524–1529):Missing ICMP DROP (
containers/agent/setup-iptables.sh, final rules):iptables -A OUTPUT -p tcp -j DROP iptables -A OUTPUT -p udp ... -j DROP # ICMP has no DROP ruleUID/GID validation (
src/docker-manager.ts:122–135):✅ Recommendations
🔴 Critical
C1 — Create a custom AppArmor profile for the agent container instead of
apparmor:unconfined. The profile should permit the specificmountsyscall needed for procfs while denying other dangerous operations (arbitrarymount,ptrace, module loading, etc.). This closes the privilege escalation window during theSYS_ADMINstartup phase. Reference: Docker AppArmor documentation; useaa-genprofas a starting point.🟠 High
H1 — Add ICMP DROP rule to the container OUTPUT chain in
setup-iptables.sh. Add:This prevents ICMP-based data exfiltration (ping tunnels, ICMP echo covert channels). Also add a LOG rule before the DROP for visibility.
H2 — Reduce
SYS_ADMINexposure window by moving the procfs mount into a dedicated privileged init container (analogous toawf-iptables-init). The agent container should receive neitherSYS_CHROOTnorSYS_ADMINat startup; onlySYS_CHROOTstrictly needed for thechroot()syscall itself needs to be present during execution, and can be handled differently.H3 — Document and enforce the shell injection boundary for
awf -- <command>. Consider adding a warning when the command contains shell metacharacters that could be interpreted dangerously, and provide a--no-shellmode that passes arguments directly viaexecform rather thanbash -c.🟡 Medium
M1 — Add an explicit ICMP LOG rule before the DROP for auditability (analogous to existing
[FW_BLOCKED_UDP_AGENT]LOG rules).M2 — Validate that
validateDomainOrPattern()correctly rejects backslashes in plain domain paths and add a test case fordomain\ninjected_directiveto confirm the two code paths (plain domain vs. URL regex) are correctly separated.M3 — Consider setting
http_proxy(lowercase) with a specific value that returns a non-zero exit code on blocked requests, or document clearly why it must remain unset — the current behavior where plain HTTP goes via DNAT is correct but non-obvious and should be verified on each Ubuntu LTS upgrade.M4 — Add a seccomp rule to block
ptraceandprocess_vm_readv/process_vm_writevif not already inseccomp-profile.json— these can be used to read memory of other processes in the container and potentially extract credentials from the API proxy sidecar process.🟢 Low
L1 — Periodic review of
DANGEROUS_PORTSlist against current threat landscape (e.g., add port 11211 for Memcached, 2379/2380 for etcd).L2 — Consider rate-limiting DNS queries in iptables to detect DNS-based exfiltration attempts even to whitelisted servers.
📈 Security Metrics
Beta Was this translation helpful? Give feedback.
All reactions