-
Notifications
You must be signed in to change notification settings - Fork 11
Description
π Executive Summary
Conducted a comprehensive evidence-based security review of the gh-aw-firewall codebase, analyzing 4,101 lines of security-critical code across 8 core files. The overall security posture is good β the architecture applies multiple overlapping controls (iptables at two layers, Squid ACLs, capability dropping, seccomp, one-shot token LD_PRELOAD, tmpfs overlays, /dev/null credential hiding). Three genuine security gaps were identified, the most significant being an IPv6 bypass path under the default configuration.
| Category | Status |
|---|---|
| Network egress filtering (IPv4) | β Strong |
| Container privilege dropping | β Strong |
| Credential file protection | β Strong (minor gap) |
| IPv6 egress filtering | |
| Squid DNS config | |
| UDP filtering (container-level) | |
| Seccomp profile | |
| Dependency vulns | βΉοΈ 1 moderate (indirect) |
π Findings from Firewall Escape Test
The security-review workflow logs were unavailable for download in this run, so this section draws on direct code analysis. The escape vectors analyzed below are the most likely candidates for an escape test agent to attempt.
π‘οΈ Architecture Security Analysis
Network Security Assessment
The firewall uses a two-layer egress filtering approach:
-
Host-level (DOCKER-USER chain) β
src/host-iptables.ts: Creates the customFW_WRAPPERchain and inserts it into Docker'sDOCKER-USERchain, filtered to traffic from thefw-bridgeinterface. Rules order: Squid allowed unrestricted β established/related β localhost β DNS allowlist β Squid port allow β UDP block β default deny. -
Container-level (NAT + OUTPUT chain) β
containers/agent/setup-iptables.sh: Redirects TCP port 80/443 via DNAT to Squid, then drops all non-redirected TCP in the OUTPUT filter chain. -
Squid ACL layer β
src/squid-config.ts: Domain allowlisting withdstdomain(plain domains) anddstdom_regex(wildcard patterns).
Rule ordering is correct β deny rules follow allow rules properly in both iptables chains and Squid ACLs. The Squid blocklist precedes the allowlist (lines ~290-310 in squid-config.ts).
DNS exfiltration is well-mitigated: DNS traffic restricted to trusted servers at both host-level (host-iptables.ts:305-329) and container-level (setup-iptables.sh:101-133). Docker's embedded DNS 127.0.0.11 is also allowed for service name resolution.
ICMP/raw sockets: NET_RAW capability is dropped from both Squid and agent containers (docker-manager.ts:285, 869). This prevents raw socket creation that could be used for ICMP tunneling. Host-level UDP blocking ([FW_BLOCKED_UDP] REJECT) catches non-DNS UDP.
Container Security Assessment
Capability model: Agent container receives NET_ADMIN, SYS_CHROOT, SYS_ADMIN at startup (required for iptables setup and procfs mount), then drops all three via capsh --drop=cap_net_admin,cap_sys_chroot,cap_sys_admin before user code runs (entrypoint.sh:~257). NET_RAW, SYS_PTRACE, SYS_MODULE, SYS_RAWIO, MKNOD are also explicitly cap_drop'd at the Docker level (docker-manager.ts:868-874).
Seccomp profile (containers/agent/seccomp-profile.json): Default action is SCMP_ACT_ALLOW. Only 3 deny rules blocking: ptrace/process_vm_readv/process_vm_writev, kernel load/reboot/module syscalls, and umount/umount2. This is permissive β many potentially dangerous syscalls like unshare, clone with namespace flags, mount are allowed. However, namespace escape via unshare requires CAP_SYS_ADMIN which is dropped.
no-new-privileges: Applied (security_opt: ['no-new-privileges:true']). Prevents privilege escalation via setuid binaries after container start.
AppArmor unconfined: apparmor:unconfined is set for the agent container (docker-manager.ts:880) to allow procfs mounting. This removes the AppArmor layer of defense. Mitigated by capability drop before user code.
Resource limits: 4GB memory limit, 1000 PID limit, default CPU shares (docker-manager.ts:887-889).
Tmpfs overlays: workDir is hidden behind tmpfs to prevent reading docker-compose.yml which contains environment variables in plaintext. MCP logs also hidden. (docker-manager.ts:824-853)
One-shot-token LD_PRELOAD: Custom C library caches sensitive tokens on first getenv() call and clears them from /proc/self/environ, preventing /proc inspection attacks.
Git credential hiding: .gitconfig, .git-credentials, etc. mounted from /dev/null to block credential access.
Domain Validation Assessment
src/domain-patterns.ts validates and sanitizes domain inputs:
- Explicit checks for
*,*.*, pure-wildcard patterns throw errors - Double dots rejected
- Wildcard segments limited:
>1wildcard in TLD position rejected - ReDoS mitigated:
[a-zA-Z0-9.-]*character class used instead of.*for wildcard expansion - Domain length cap (512 chars) before regex matching in
isDomainMatchedByPattern()
Input Validation Assessment
- Port validation in
squid-config.ts: Integer parsing, range check (1-65535), DANGEROUS_PORTS blocklist checked before adding to Safe_ports ACL - Port sanitization:
port.replace(/[^0-9-]/g, '')defense-in-depth after integer validation - AWF_USER_UID/GID validated as numeric + non-zero in
entrypoint.sh:18-28 redact-secrets.ts: Minimal function redacting Authorization headers, env var patterns, GitHub tokens from log output
β οΈ Threat Model
| # | Threat (STRIDE) | Attack Vector | Evidence | Likelihood | Impact | Severity |
|---|---|---|---|---|---|---|
| T1 | Information Disclosure β IPv6 Bypass | Agent sends TCP/UDP over IPv6 to bypass domain filtering | host-iptables.ts:313 β IPv6 chain only created when ipv6DnsServers.length > 0; setup-iptables.sh β no ip6tables OUTPUT -p tcp -j DROP |
Medium (requires host IPv6) | High | Medium |
| T2 | Information Disclosure β Squid DNS misconfiguration | Squid uses hardcoded 8.8.8.8 8.8.4.4 regardless of --dns-servers; custom DNS blocks Google DNS at iptables |
squid-config.ts:551 β dns_nameservers 8.8.8.8 8.8.4.4 hardcoded; not dynamic from config.dnsServers |
Low (only when custom DNS) | Medium | Low |
| T3 | Information Disclosure β GCP ADC credential gap | ~/.config/gcloud/application_default_credentials.json not blocked |
docker-manager.ts:770,802 β only credentials.db blocked, not application_default_credentials.json |
Low | Medium | Low |
| T4 | Information Disclosure β UDP passes container OUTPUT chain | Non-DNS UDP from container is only blocked at host level, not container-level | setup-iptables.sh:290 β only iptables -A OUTPUT -p tcp -j DROP, no UDP rule |
Low (host-level catches it) | Low | Low |
| T5 | Elevation of Privilege β Permissive seccomp | SCMP_ACT_ALLOW default; many syscalls allowed |
seccomp-profile.json β defaultAction ALLOW, only 3 deny rules |
Low (capabilities mitigate) | Medium | Low |
| T6 | Tampering β AppArmor unconfined | No AppArmor profile enforcement on agent container | docker-manager.ts:880 β apparmor:unconfined |
Low (capability drop before user code) | Medium | Low |
| T7 | Information Disclosure β ajv ReDoS via $data |
Indirect ajv dependency vulnerable to ReDoS when $data option used |
npm advisory GHSA-2g4f-4pwh-qvx6, npm audit output |
Low (not directly used) | Low | Low |
π― Attack Surface Map
| Entry Point | File | Protections | Risk |
|---|---|---|---|
--allow-domains input |
domain-patterns.ts:validate... |
Empty check, wildcard validation, ReDoS mitigation via char class | Low |
--allow-host-ports input |
squid-config.ts:318-360 |
Integer parse, 1-65535 check, DANGEROUS_PORTS blocklist | Low |
| Agent command execution | docker-manager.ts:895 |
Shell escape (replace(/\$/g, '$$$$')), runs as non-root awfuser |
Low |
| Container environment vars | docker-manager.ts:341-448 |
EXCLUDED_ENV_VARS set, credential isolation for API proxy | Low |
| IPv4 TCP egress | setup-iptables.sh + squid-config.ts |
DNAT β Squid β domain ACL, OUTPUT chain TCP DROP | Low |
| IPv6 TCP egress | host-iptables.ts:313 |
No default-deny when IPv4-only DNS | Medium |
| UDP egress | host-iptables.ts:483-489 |
Host-level REJECT via FW_WRAPPER chain | Low |
| DNS queries | setup-iptables.sh:101-122 |
Allowlisted servers only via NAT RETURN | Low |
/proc inspection |
entrypoint.sh, one-shot-token lib |
Tokens unset from env via LD_PRELOAD after 5s sleep | Low |
| Docker socket | containers/agent/docker-stub.sh |
Replaced with stub that errors on exec | Low |
| Credential files | docker-manager.ts:757-803 |
/dev/null mounts + selective home mounting | Low |
| Squid proxy | containers/squid/ |
cap_drop, domain ACL, healthcheck | Low |
π Evidence Collection
T1: IPv6 Bypass β Code Evidence
# host-iptables.ts:313 β setupIpv6Chain only called inside this block:
if (ipv6DnsServers.length > 0) {
// ...
await setupIpv6Chain(bridgeName);
// ...IPv6 deny rules...
}
// With default --dns-servers 8.8.8.8,8.8.4.4: ipv6DnsServers = [] β NO IPv6 chain setup
# setup-iptables.sh final OUTPUT rule:
iptables -A OUTPUT -p tcp -j DROP # IPv4-only β no ip6tables equivalent
# No: ip6tables -A OUTPUT -p tcp -j DROP
````
With `ip6tablesAvailable=true` on a host with IPv6, an agent can send IPv6 TCP traffic (e.g., `curl -6 https://[2001:4860:4860::8888]/...`) that bypasses Squid domain filtering entirely.
</details>
<details>
<summary>T2: Squid DNS Hardcoded β Code Evidence</summary>
````
# squid-config.ts:551 (hardcoded, never uses config.dnsServers):
dns_nameservers 8.8.8.8 8.8.4.4
# types.ts:326 shows dnsServers field exists in SquidConfig but is never consumed:
dnsServers?: string[];
# Impact: user runs: awf --dns-servers 1.1.1.1,1.0.0.1 ...
# β iptables blocks 8.8.8.8 (not in trusted list)
# β Squid tries to query 8.8.8.8 β blocked β DNS resolution fails
# β All proxied requests fail
````
</details>
<details>
<summary>T3: GCP ADC Credential Gap β Code Evidence</summary>
````
# docker-manager.ts:770 β BLOCKED:
`\$\{effectiveHome}/.config/gcloud/credentials.db`
# NOT BLOCKED:
# ~/.config/gcloud/application_default_credentials.json β OAuth2 tokens for gcloud CLI
# ~/.config/gcloud/legacy_credentials/*/adc.json β Legacy ADC tokens
# ~/.config/gcloud/access_tokens.db β Access token cache
````
</details>
<details>
<summary>T4: UDP Container-Level Gap</summary>
````
# setup-iptables.sh:290 β final rule only drops TCP:
iptables -A OUTPUT -p tcp -j DROP
# No matching: ip6tables or iptables -A OUTPUT -p udp -j DROP
# MITIGATED at host level (host-iptables.ts:483-489):
'-p', 'udp', '-j', 'LOG', '--log-prefix', '[FW_BLOCKED_UDP] '
'-p', 'udp', '-j', 'REJECT', '--reject-with', 'icmp-port-unreachable'
# Defense-in-depth gap: relies only on host-level filtering for UDP
Seccomp Profile Summary
{
"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",
"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"]},
{"action": "SCMP_ACT_ERRNO", "names": ["umount", "umount2"]}
]
}
````
`SCMP_ACT_ALLOW` default means all other syscalls are permitted. `unshare`, `clone`, `mount` (with namespace flags) are allowed β namespace escape requires `CAP_SYS_ADMIN` which is capability-dropped before user code.
</details>
<details>
<summary>npm audit output</summary>
````
Package: ajv (indirect dependency)
Severity: moderate
Advisory: GHSA-2g4f-4pwh-qvx6 β ReDoS when using $data option
Direct: falseβ Recommendations
π΄ Medium Priority
M1 β Establish IPv6 default-deny at both host and container levels
Even without IPv6 DNS servers configured, the host-level DOCKER-USER should have a default-deny IPv6 rule, and the container-level OUTPUT chain should drop non-DNS IPv6 TCP:
// host-iptables.ts: Always set up IPv6 chain (not only when IPv6 DNS configured)
// Add default-deny rules even when no IPv6 DNS servers specified
await setupIpv6Chain(bridgeName); // Always called
// Then in the chain: deny all IPv6 TCP/UDP unless it's to a trusted IPv6 DNS server# setup-iptables.sh: Add default IPv6 TCP deny alongside IPv4
if [ "$IP6TABLES_AVAILABLE" = true ]; then
ip6tables -A OUTPUT -p tcp -j DROP
ip6tables -A OUTPUT -p udp -j DROP
fiπ‘ Low Priority
L1 β Fix Squid dns_nameservers to use configured DNS servers
squid-config.ts:551 hardcodes dns_nameservers 8.8.8.8 8.8.4.4. Squid should use the configured DNS servers to match what iptables allows. The SquidConfig type has dnsServers?: string[] (unused):
// squid-config.ts β replace hardcoded line:
// dns_nameservers 8.8.8.8 8.8.4.4
// with:
const dnsServersLine = config.dnsServers && config.dnsServers.length > 0
? `dns_nameservers \$\{config.dnsServers.join(' ')}`
: 'dns_nameservers 8.8.8.8 8.8.4.4';And pass dnsServers when calling generateSquidConfig() in docker-manager.ts.
L2 β Extend GCP credential blocking to ADC and access token files
Add to credentialFiles array in docker-manager.ts:
`\$\{effectiveHome}/.config/gcloud/application_default_credentials.json`,
`\$\{effectiveHome}/.config/gcloud/access_tokens.db`,
`\$\{effectiveHome}/.config/gcloud/legacy_credentials`, // directory, not file β needs special handlingNote: Blocking an entire directory requires a different approach (tmpfs overlay or not mounting ~/.config).
L3 β Add container-level UDP default-deny
setup-iptables.sh drops TCP but not UDP. While the host-level chain blocks UDP, adding defense-in-depth:
# setup-iptables.sh: After TCP DROP rule, add UDP DROP
iptables -A OUTPUT -p tcp -j DROP
# Add: block non-DNS UDP at container level too (defense-in-depth)
iptables -A OUTPUT -p udp -j DROPL4 β Harden seccomp profile to allowlist rather than denylist
Consider switching from SCMP_ACT_ALLOW default with a denylist to a SCMP_ACT_ERRNO default with an explicit allowlist of needed syscalls. This is the CIS Docker Benchmark recommendation. Docker's default seccomp profile is a good starting point to derive an allowlist from.
L5 β Update ajv indirect dependency (minor)
npm audit fix # or pin to a patched versionπ Security Metrics
| Metric | Value |
|---|---|
| Security-critical code lines analyzed | 4,101 |
| Attack surfaces identified | 14 |
| STRIDE threats modeled | 7 |
| Critical findings | 0 |
| Medium findings | 1 (T1: IPv6 bypass) |
| Low findings | 6 |
| False positives | 0 |
| Defense layers (network) | 3 (host iptables + container iptables + Squid ACL) |
| Credential files explicitly protected | 14 per docker-manager.ts:757-803 |
Note: This was intended to be a discussion, but discussions could not be created due to permissions issues. This issue was created as a fallback.
Tip: Discussion creation may fail if the specified category is not announcement-capable. Consider using the "Announcements" category or another announcement-capable category in your workflow configuration.
Generated by Daily Security Review and Threat Modeling
- expires on Feb 28, 2026, 1:41 PM UTC