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-03-17. The system implements a well-architected defense-in-depth approach combining host-level iptables, container NAT redirection, Squid domain-filtering proxy, capability dropping, seccomp, and one-shot credential protection.
Overall security posture: GOOD — No critical vulnerabilities were found. Three medium-severity issues require attention, primarily around the seccomp profile allowing dangerous syscalls without argument-level filtering and the AppArmor unconfined configuration.
Severity
Count
Status
Critical
0
✅ None found
High
0
✅ None found
Medium
3
⚠️ Requires attention
Low
4
ℹ️ Track and plan
Lines of security-critical code analyzed: 6,579 across 8 core files.
🔍 Findings from Firewall Escape Test
No "firewall-escape-test" workflow found in the repository's agentic workflows. The complementary context was gathered by running the security-review workflow status lookup and manual code analysis. The secret-digger workflows (Claude, Codex, Copilot variants) appear to serve a related purpose.
The traffic flow implements three enforcing layers:
Host iptables (src/host-iptables.ts): DOCKER-USER chain → FW_WRAPPER chain; allows only Squid+DNS, rejects everything else including UDP and multicast
Container NAT (setup-iptables.sh): DNAT rules redirect port 80/443 to Squid; dangerous ports explicitly RETURN before DNAT so filter chain's DROP catches them
IPv4 direct-IP blocking in Squid (src/squid-config.ts:551-554):
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 tunneling via raw IPs. Standard IPv4 and hex-colon IPv6 forms are covered.
**IPv6 fallback** (`src/host-iptables.ts:66-76`): When `ip6tables` is unavailable, IPv6 is disabled via `sysctl`. If sysctl fails, the code logs a `warn` but continues rather than aborting — a potential gap (see Low findings).
**DNS exfiltration prevention**: Container `resolv.conf` is rewritten to only list Docker's embedded DNS (`127.0.0.11`). Upstream DNS forwarding is controlled via docker-compose `dns:` config. UDP port 53 to non-configured servers is blocked in both host and container filter chains.
### Container Security Assessment — **Good with caveats**
**Evidence gathered:**
```
grep -n "cap_drop\|cap_add\|seccomp\|security_opt\|apparmor" src/docker-manager.ts
cat containers/agent/seccomp-profile.json
```
**Positive findings:**
- `no-new-privileges:true` set on all containers (`src/docker-manager.ts:1026`)
- `NET_RAW`, `SYS_PTRACE`, `SYS_MODULE`, `SYS_RAWIO`, `MKNOD` explicitly dropped (`src/docker-manager.ts:1014-1019`)
- `NET_ADMIN` never granted to the agent container (init container pattern)
- `pids_limit: 1000`, `mem_limit: 2g` resource limits prevent DoS
- `ptrace`, `process_vm_readv/writev`, `kexec_load`, `bpf`, `keyctl` blocked in seccomp profile
- tmpfs overlays hide `/tmp/awf-*` (docker-compose.yml with secrets) from the agent
- One-shot token LD_PRELOAD library zeros credentials after first read
**Caveat — SYS_ADMIN + AppArmor unconfined (Medium Finding #1):**
The agent container starts with `SYS_ADMIN` and `SYS_CHROOT` capabilities plus `apparmor:unconfined` for procfs mounting (`src/docker-manager.ts:1012,1028`). These are dropped via `capsh` before user code runs, but the comment notes AppArmor is unconfined because Docker's default AppArmor profile blocks `mount` (required for `/host/proc`). This means during container initialization, the process has both SYS_ADMIN and no AppArmor confinement simultaneously.
### Domain Validation Assessment — **Strong**
**Evidence gathered:**
```
cat src/domain-patterns.ts
```
- `*` and `*.*` patterns explicitly rejected (`src/domain-patterns.ts:155-164`)
- Patterns with `> 50% wildcard segments` rejected
- Wildcard `*` converts to `[a-zA-Z0-9.-]*` (character class, prevents ReDoS)
- Domain length capped at 512 chars before regex matching (ReDoS defense)
- Protocol-specific restrictions (`(redacted) `https://`) supported
### Input Validation Assessment — **Good**
- All `execa()` calls use array-form arguments (no shell interpolation)
- Port validation rejects dangerous ports list (SSH, DBs, etc.) in both iptables and Squid configs
- URL patterns processed through `parseUrlPatterns()` which escapes regex metacharacters
- `AWF_USER_UID/GID` validated as numeric and non-zero in entrypoint.sh
---
## ⚠️ Threat Model (STRIDE)
| ID | Threat | Severity | Evidence | Mitigations |
|----|--------|----------|----------|-------------|
| S1 | Spoofing via IPv4 alternative encodings | Low | `src/squid-config.ts:551` regex covers dotted notation; hex/decimal forms not explicitly blocked | Squid normalizes IPs internally; host-level filtering provides defense-in-depth |
| T1 | Tampering with iptables (container namespace escape) | Medium | `clone`/`unshare` allowed in seccomp without argument filter; `NET_ADMIN` absent but namespaces possible | `no-new-privileges:true`, NET_RAW dropped, no NET_ADMIN |
| T2 | Seccomp bypass via `mount` syscall | Medium | `mount` allowed unrestricted (`containers/agent/seccomp-profile.json:175`); AppArmor unconfined | SYS_ADMIN dropped via capsh before user code |
| R1 | Log tampering / repudiation | Low | Agent runs without explicit log integrity guarantees | Squid access log with custom format; iptables LOG prefix markers; logs preserved post-cleanup |
| I1 | Data exfiltration via DNS tunneling | Low | DNS blocked to non-configured servers; only Docker embedded DNS allowed | Host and container UDP port 53 filter chains; DoH proxy option available |
| I2 | Credential leakage via `--env-all` | Medium | `EXCLUDED_ENV_VARS` only blocks shell metadata; cloud credentials (AWS_SECRET_ACCESS_KEY, AZURE_CLIENT_SECRET, etc.) pass through (`src/docker-manager.ts:375-395`) | --env-all is explicit opt-in with warning; DLP URL scanning available |
| I3 | Credential leakage via docker-compose.yml | Low | tmpfs overlays prevent container from reading workDir | `src/docker-manager.ts:974-978` |
| D1 | Resource exhaustion DoS | Low | Container has `pids_limit: 1000`, `mem_limit: 2g` | Docker resource limits applied |
| E1 | Privilege escalation via user namespaces | Medium | `clone`/`clone3`/`unshare` allowed without `CLONE_NEWUSER` flag restriction (`containers/agent/seccomp-profile.json:31,175,340`) | `no-new-privileges:true`; Docker's default seccomp would restrict this but custom profile does not |
---
## 🎯 Attack Surface Map
| Surface | Location | Risk | Current Protections |
|---------|----------|------|---------------------|
| `--allow-domains` CLI input | `src/cli.ts`, `src/domain-patterns.ts` | Low | `validateDomainOrPattern()`, wildcard restrictions, ReDoS protection |
| `--url-pattern` CLI input | `src/cli.ts`, `src/squid-config.ts:120` | Low | `parseUrlPatterns()` escapes regex metacharacters |
| `--allow-host-ports` CLI input | `src/squid-config.ts:467-490` | Low | Port range validation, dangerous port blocklist |
| Container env (--env-all) | `src/docker-manager.ts:491-498` | Medium | EXCLUDED_ENV_VARS set (incomplete for cloud creds) |
| Squid proxy config generation | `src/squid-config.ts` | Low | Generated from typed config objects, not raw strings |
| Host iptables modification | `src/host-iptables.ts` | Low | execa array-form prevents injection; runs as root (required) |
| Agent container syscalls | `containers/agent/seccomp-profile.json` | Medium | Custom seccomp but allows clone/mount/unshare |
| Agent container capabilities | `src/docker-manager.ts:1012` | Medium | SYS_ADMIN+AppArmor-unconfined window at startup |
| DNS resolution | `containers/agent/entrypoint.sh` | Low | resolv.conf hardcoded to 127.0.0.11 |
| Credential environment variables | `containers/agent/entrypoint.sh`, one-shot-token lib | Low | One-shot token zeroes creds after first read |
| Volume mounts | `src/docker-manager.ts:596-783` | Low | Granular mounts (not full HOME), /etc/shadow excluded, credential files blocked |
| Docker socket | N/A | N/A | Docker socket not mounted in containers |
---
## 📋 Evidence Collection
<details>
<summary>Seccomp dangerous syscalls analysis</summary>
```
$ python3 -c "
import json
with open('containers/agent/seccomp-profile.json') as f:
data = json.load(f)
for group in data['syscalls']:
names = group.get('names', [])
args = group.get('args', [])
if 'clone' in names or 'mount' in names or 'unshare' in names:
print('Action:', group.get('action'))
print('Syscalls:', [n for n in names if n in ['clone','clone3','mount','unshare']])
print('Args:', args)
print('Comment:', group.get('comment', 'none'))
"
Action: SCMP_ACT_ALLOW
Syscalls: ['clone', 'clone3', 'mount', 'unshare']
Args: [] # ← NO argument filtering
Comment: Allow standard syscalls needed by development tools
```
Note: `umount`/`umount2` are explicitly **blocked** (`SCMP_ACT_ERRNO`) with comment "Block unmounting filesystems - mount is allowed for procfs but unmount is not needed". This shows the authors were aware of mount security but the `clone`/`unshare` gap was not addressed.
</details>
<details>
<summary>Capability configuration for agent container</summary>
```
# src/docker-manager.ts:1004-1029
cap_add: ['SYS_CHROOT', 'SYS_ADMIN'],
cap_drop: ['NET_RAW', 'SYS_PTRACE', 'SYS_MODULE', 'SYS_RAWIO', 'MKNOD'],
security_opt: [
'no-new-privileges:true',
'seccomp=/path/to/seccomp-profile.json',
'apparmor:unconfined', # ← Required for procfs mount at /host/proc
]
EXCLUDED_ENV_VARS list (--env-all gap)
// src/docker-manager.ts:375-395constEXCLUDED_ENV_VARS=newSet(['PATH',// Must use container's PATH'PWD',// Container's working directory'OLDPWD','SHLVL','_','SUDO_COMMAND','SUDO_USER','SUDO_UID','SUDO_GID',]);// When api-proxy enabled, also excludes API keys.// NOT excluded: AWS_SECRET_ACCESS_KEY, AZURE_CLIENT_SECRET, GOOGLE_APPLICATION_CREDENTIALS,// VAULT_TOKEN, DATABASE_URL, etc.```</details><details><summary>npm audit results</summary>```$npmaudit
MODERATE: js-yaml(viamarkdownlint-cli2)
MODERATE: markdown-it(viamarkdownlint-cli2)
MODERATE: markdownlint
MODERATE: markdownlint-cli2
Total: 4moderate,0high,0critical
Fixable: Upgrademarkdownlint-cli2to0.21.0(majorversionbumprequired)
Assessment: These are workflow/dev tooling dependencies. The core production security path (execa, commander, chalk, js-yaml production usage) does not include these packages.
IPv6 fallback — non-fatal failure path
# containers/agent/setup-iptables.sh:35-37echo"[iptables] WARNING: ip6tables is not available, disabling IPv6 via sysctl..."
sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null || \
echo"[iptables] WARNING: failed to disable IPv6 (net.ipv6.conf.all.disable_ipv6)"# ← Execution continues if both ip6tables AND sysctl fail
Same pattern exists in src/host-iptables.ts:71-76.
✅ Recommendations
🔴 Medium — Restrict clone syscall to prevent user namespace creation
File:containers/agent/seccomp-profile.json
Docker's default seccomp profile restricts clone with argument filtering to block CLONE_NEWUSER (user namespace creation). The custom profile allows clone/clone3/unshare without argument restrictions, enabling potential namespace-based privilege escalation.
Recommended fix: Add an argument-filtered entry that blocks CLONE_NEWUSER (flag value 0x10000000) in the seccomp profile, following the Docker default seccomp pattern for namespace restriction. Alternatively, split the clone entry into: (a) allow clone with arg CLONE_NEWUSER=0, (b) block clone with CLONE_NEWUSER set.
🔴 Medium — Document and reduce the AppArmor unconfined + SYS_ADMIN window
File:src/docker-manager.ts:1012-1028
The combination of apparmor:unconfined, SYS_ADMIN, and custom seccomp (which allows mount) during the container startup phase creates a window with fewer restrictions than intended. While capsh drops SYS_ADMIN before user code executes, the startup phase (entrypoint.sh) runs with full capabilities.
Recommended mitigations:
Document the exact lifecycle: which functions run before vs. after capsh --drop=cap_sys_admin
Consider using a tighter AppArmor profile that only allows the specific mount operations needed (procfs at /host/proc) rather than full unconfined
Add an integration test that verifies SYS_ADMIN is unavailable when the user command runs
The --env-all flag passes all host environment variables to the container, but EXCLUDED_ENV_VARS only filters out shell metadata variables. Cloud provider credentials (AWS_SECRET_ACCESS_KEY, AZURE_CLIENT_SECRET, GOOGLE_APPLICATION_CREDENTIALS, VAULT_TOKEN, DATABASE_URL) are passed through when present.
Recommended actions:
Expand EXCLUDED_ENV_VARS to include well-known cloud provider credential variable names
Add a warning if any known-sensitive env var names (AWS_, AZURE_, GCP_*) are detected during --env-all
Update documentation to explicitly warn about this behavior
🟢 Low — Abort if both ip6tables and sysctl IPv6 disable fail
If ip6tables is unavailable AND sysctl fails to disable IPv6, the script logs a warning but continues. This could leave IPv6 traffic unfiltered.
Recommended fix: Change the warning to an error and exit 1 if both fallbacks fail, preventing the agent from running with unfiltered IPv6.
🟢 Low — Upgrade markdownlint-cli2 to address moderate npm vulnerabilities
File:package.json
Four moderate vulnerabilities in markdownlint-cli2 and its dependencies. These appear to be dev/workflow tooling, not the production security path.
Recommended action: Evaluate the major version bump (0.21.0) and upgrade, or accept the risk if markdownlint-cli2 is not used in security-sensitive contexts.
🟢 Low — Verify Squid handles alternative IP notations
File:src/squid-config.ts:551
The dst_ipv4 ACL regex ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ matches standard dotted-decimal notation. Alternative IPv4 representations (hex 0x7f000001, pure decimal 2130706433, octal 0177.0.0.1) are not explicitly covered.
Recommended action: Verify that Squid's internal URL normalization converts all IPv4 representations to standard form before ACL matching (it likely does), and add a note in src/squid-config.ts confirming this assumption.
📈 Security Metrics
Metric
Value
Security-critical files analyzed
8
Total lines of security-critical code
6,579
Attack surfaces identified
14
STRIDE threats modeled
9
Critical findings
0
High findings
0
Medium findings
3
Low findings
4
npm vulnerabilities (critical/high)
0
npm vulnerabilities (moderate)
4 (dev deps only)
🏆 Notable Security Strengths
Init container pattern for iptables: NET_ADMIN never granted to agent container — excellent design
One-shot token LD_PRELOAD library: Credentials zeroed after first read — innovative defense against credential exfiltration
tmpfs overlays: Hides docker-compose.yml (containing secrets) from agent filesystem view
ReDoS protection: Wildcard patterns use character class [a-zA-Z0-9.-]* instead of .*
Granular volume mounts: SECURITY FIX noted at src/docker-manager.ts:596 replacing blanket HOME mount with selective subdirectory mounts
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-firewall codebase conducted on 2026-03-17. The system implements a well-architected defense-in-depth approach combining host-level iptables, container NAT redirection, Squid domain-filtering proxy, capability dropping, seccomp, and one-shot credential protection.
Overall security posture: GOOD — No critical vulnerabilities were found. Three medium-severity issues require attention, primarily around the seccomp profile allowing dangerous syscalls without argument-level filtering and the AppArmor unconfined configuration.
Lines of security-critical code analyzed: 6,579 across 8 core files.
🔍 Findings from Firewall Escape Test
🛡️ Architecture Security Analysis
Network Security Assessment — Strong
Evidence gathered:
The traffic flow implements three enforcing layers:
src/host-iptables.ts): DOCKER-USER chain → FW_WRAPPER chain; allows only Squid+DNS, rejects everything else including UDP and multicastsetup-iptables.sh): DNAT rules redirect port 80/443 to Squid; dangerous ports explicitlyRETURNbefore DNAT so filter chain's DROP catches themsrc/squid-config.ts): Domain allowlist, blocked domain list, IP-direct blocking, dangerous port ACLIPv4 direct-IP blocking in Squid (
src/squid-config.ts:551-554):EXCLUDED_ENV_VARS list (--env-all gap)
Assessment: These are workflow/dev tooling dependencies. The core production security path (execa, commander, chalk, js-yaml production usage) does not include these packages.
IPv6 fallback — non-fatal failure path
Same pattern exists in
src/host-iptables.ts:71-76.✅ Recommendations
🔴 Medium — Restrict
clonesyscall to prevent user namespace creationFile:
containers/agent/seccomp-profile.jsonDocker's default seccomp profile restricts
clonewith argument filtering to blockCLONE_NEWUSER(user namespace creation). The custom profile allowsclone/clone3/unsharewithout argument restrictions, enabling potential namespace-based privilege escalation.Recommended fix: Add an argument-filtered entry that blocks
CLONE_NEWUSER(flag value0x10000000) in the seccomp profile, following the Docker default seccomp pattern for namespace restriction. Alternatively, split thecloneentry into: (a) allowclonewith argCLONE_NEWUSER=0, (b) blockclonewithCLONE_NEWUSERset.🔴 Medium — Document and reduce the AppArmor unconfined + SYS_ADMIN window
File:
src/docker-manager.ts:1012-1028The combination of
apparmor:unconfined,SYS_ADMIN, and custom seccomp (which allowsmount) during the container startup phase creates a window with fewer restrictions than intended. WhilecapshdropsSYS_ADMINbefore user code executes, the startup phase (entrypoint.sh) runs with full capabilities.Recommended mitigations:
capsh --drop=cap_sys_admin/host/proc) rather than fullunconfined🟡 Medium — Document
--env-allcredential leakage risk; consider expanding EXCLUDED_ENV_VARSFile:
src/docker-manager.ts:375-395The
--env-allflag passes all host environment variables to the container, butEXCLUDED_ENV_VARSonly filters out shell metadata variables. Cloud provider credentials (AWS_SECRET_ACCESS_KEY,AZURE_CLIENT_SECRET,GOOGLE_APPLICATION_CREDENTIALS,VAULT_TOKEN,DATABASE_URL) are passed through when present.Recommended actions:
EXCLUDED_ENV_VARSto include well-known cloud provider credential variable names--env-all🟢 Low — Abort if both ip6tables and sysctl IPv6 disable fail
Files:
containers/agent/setup-iptables.sh:36-37,src/host-iptables.ts:71-76If
ip6tablesis unavailable ANDsysctlfails to disable IPv6, the script logs a warning but continues. This could leave IPv6 traffic unfiltered.Recommended fix: Change the warning to an error and
exit 1if both fallbacks fail, preventing the agent from running with unfiltered IPv6.🟢 Low — Upgrade markdownlint-cli2 to address moderate npm vulnerabilities
File:
package.jsonFour moderate vulnerabilities in
markdownlint-cli2and its dependencies. These appear to be dev/workflow tooling, not the production security path.Recommended action: Evaluate the major version bump (
0.21.0) and upgrade, or accept the risk if markdownlint-cli2 is not used in security-sensitive contexts.🟢 Low — Verify Squid handles alternative IP notations
File:
src/squid-config.ts:551The
dst_ipv4ACL regex^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$matches standard dotted-decimal notation. Alternative IPv4 representations (hex0x7f000001, pure decimal2130706433, octal0177.0.0.1) are not explicitly covered.Recommended action: Verify that Squid's internal URL normalization converts all IPv4 representations to standard form before ACL matching (it likely does), and add a note in
src/squid-config.tsconfirming this assumption.📈 Security Metrics
🏆 Notable Security Strengths
[a-zA-Z0-9.-]*instead of.*src/docker-manager.ts:596replacing blanket HOME mount with selective subdirectory mountsno-new-privileges:true: Applied to all containers consistentlyBeta Was this translation helpful? Give feedback.
All reactions