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 analyzed 5,553 lines of security-critical code across 7 core files, covering the full defense-in-depth stack: host-level iptables, container-level iptables, Squid proxy ACL generation, domain pattern validation, container hardening, and credential protection. No firewall-escape-test workflow exists; analysis is based entirely on static code review with full command evidence.
Overall security posture: GOOD — multiple independent defense layers are correctly implemented. Findings are primarily gaps in the inner-most (container-level) layer that are already compensated by the outer layer, plus one conditional IPv6 bypass that depends on the host Docker configuration.
🔍 Phase 1: Previous Escape Test Context
No firewall-escape-test workflow was found in the repository. The closest equivalents are secret-digger-claude, secret-digger-codex, and secret-digger-copilot (hourly scheduled), and security-guard (PR gating). This review stands alone as primary threat intelligence.
🛡️ Phase 2: Architecture Security Analysis
Network Security — Two-Layer Firewall
The system correctly implements defense-in-depth with two independent enforcement points:
Layer 1 — Container-level (setup-iptables.sh)
# Command run to discover this:
grep -n "iptables -A OUTPUT" containers/agent/setup-iptables.sh
# Key results:
79: iptables -A OUTPUT -p tcp -d "$AGENT_IP" -j ACCEPT
272: iptables -A OUTPUT -o lo -j ACCEPT
276: iptables -A OUTPUT -p udp -d "$dns_server" --dport 53 -j ACCEPT
285: iptables -A OUTPUT -p tcp -d "$SQUID_IP" -j ACCEPT
295: iptables -A OUTPUT -p tcp -j DROP # <-- only TCP dropped
Observation: The final DROP rule only blocks TCP. Non-DNS UDP and ICMPv4 outbound are not blocked at the container level.
Layer 2 — Host-level (src/host-iptables.ts)
# Command run to discover this:
sed -n '526,555p' src/host-iptables.ts
# Key results:# Rule 7: Block all UDP# Rule 8: Default-deny everything else via REJECT# FW_WRAPPER chain inserted into DOCKER-USER at position 1
The host chain correctly blocks UDP and has a default-deny final rule. This fully compensates for Layer 1's gap — but only for IPv4.
Container Hardening
# Command run:
grep -n "cap_drop\|capabilities\|NET_ADMIN\|NET_RAW" src/docker-manager.ts
# Squid container drops:
cap_drop: [NET_RAW, CHOWN, SETUID, SETGID, SETFCAP]
# Agent entrypoint drops:
CAPS_TO_DROP="cap_net_admin,cap_sys_chroot,cap_sys_admin"# chroot mode# or just cap_net_admin in normal mode
Correctly implemented: CAP_NET_ADMIN is dropped before user code executes, preventing iptables bypass.
Domain Validation
# Command run:
cat src/domain-patterns.ts | grep -A5 "validateDomainOrPattern"# Validates:# - Rejects '*' (match-all)# - Rejects '*.*' (too broad)# - Rejects patterns where ≥50% segments are pure wildcards# - Uses [a-zA-Z0-9.-]* instead of .* to prevent ReDoS
Correctly implemented: The anti-ReDoS wildcard replacement is good. Pattern anchoring prevents partial matches.
Credential Protection (One-Shot Token)
# Command run:
head -80 containers/agent/one-shot-token/one-shot-token.c
# Mechanism:# - LD_PRELOAD hooks getenv()# - First call: returns value, then calls unsetenv() to clear from environ# - Token names XOR-obfuscated (key=0x5A) to prevent trivial strings(1) discovery
Correctly implemented with acknowledged limitations (XOR is not cryptographic; by-design for binary string hiding, not crypto protection).
⚠️ Threat Model (STRIDE)
#
STRIDE
Threat
Severity
Evidence
T1
Elevation
IPv6 bypass when only IPv4 DNS configured
High
host-iptables.ts:363 — FW_WRAPPER_V6 only created if ipv6DnsServers.length > 0
T2
Elevation
Container UDP/ICMP not blocked at container layer
Medium
setup-iptables.sh:295 — only iptables -A OUTPUT -p tcp -j DROP
T3
Tampering
Seccomp profile is default-ALLOW
Medium
seccomp-profile.json — defaultAction: SCMP_ACT_ALLOW, only 3 deny groups
T4
Tampering
URL pattern newline injection into Squid config
Low
squid-config.ts:119 — url_regex \$\{pattern} without newline sanitization
T5
Info Disclosure
Chroot mode mounts host /dev read-only
Low
docker-manager.ts:541 — '/dev:/host/dev:ro'
T6
Repudiation
Container-level iptables has no LOG rules for UDP/ICMP
Low
setup-iptables.sh — no LOG rules in OUTPUT filter chain
T7
DoS
Port range O(n) validation (1-65535 = 65535 iterations)
Informational
squid-config.ts:458 — for (let p = start; p <= end; p++)
entrypoint.sh:269 — global safe.directory wildcard
🎯 Attack Surface Map
Entry Point
Location
Purpose
Current Protections
Weakness
--allow-domains CLI flag
cli.ts:669
Domain whitelist input
validateDomainOrPattern(), anti-ReDoS regex
Intentionally permissive for subdomains
--allow-urls CLI flag
cli.ts:996
SSL Bump URL patterns
Dangerous pattern checklist, path requirement
Newlines not sanitized before Squid config
--allow-host-ports CLI flag
cli.ts:778
Host access ports
DANGEROUS_PORTS blocklist
Valid
--dns-servers CLI flag
cli.ts:762
DNS server override
IPv4/IPv6 regex validation
Valid
--env CLI flag
cli.ts:950
Pass env vars to agent
KEY=VALUE format check
Passed to containers; broad access
--mount CLI flag
cli.ts:964
Custom volume mounts
Format validation
No path escape check documented
AWF_USER_UID env var
entrypoint.sh:9
Container user mapping
Rejects UID 0
Valid
URL pattern → Squid ACL
squid-config.ts:119
Content filtering
Pattern escaping
Newline injection possible
One-shot token interception
one-shot-token.c
Credential protection
LD_PRELOAD hook + unsetenv
XOR obfuscation (not crypto)
chroot /dev mount
docker-manager.ts:541
Runtime compatibility
Read-only
Exposes device nodes
📋 Evidence Collection
Finding T1: IPv6 bypass — FW_WRAPPER_V6 only installed when IPv6 DNS servers configured
# Command:
sed -n '360,370p' src/host-iptables.ts
# Output:if (ipv6DnsServers.length > 0) {
if (!ip6tablesAvailable) {
logger.warn('...');
} else {
// Set up IPv6 chain if we have IPv6 DNS servers
await setupIpv6Chain(bridgeName);
Analysis: With default config (--dns-servers 8.8.8.8,8.8.4.4 — all IPv4), ipv6DnsServers.length === 0, so FW_WRAPPER_V6 is never installed in ip6tables DOCKER-USER. If Docker has IPv6 enabled globally, containers on awf-net may have IPv6 addresses and could initiate unfiltered outbound IPv6 connections.
Partial mitigation: awf-net is created without --ipv6, so IPv6 routing through that bridge is typically unavailable. The risk is low in most environments but non-zero on Docker installations with ip6tables: true in daemon.json.
Finding T2: Container UDP/ICMP not blocked at container layer
# Command:
grep -n "iptables -A OUTPUT" containers/agent/setup-iptables.sh
# Full OUTPUT filter chain rules:
Line 79: iptables -A OUTPUT -p tcp -d "$AGENT_IP" -j ACCEPT # self-traffic
Line 163: iptables -A OUTPUT -p tcp -d "$HOST_GW" --dport 80 -j ACCEPT # (if host access)
Line 272: iptables -A OUTPUT -o lo -j ACCEPT # loopback
Line 276: iptables -A OUTPUT -p udp -d "$dns_server" --dport 53 -j ACCEPT # DNS
Line 285: iptables -A OUTPUT -p tcp -d "$SQUID_IP" -j ACCEPT # Squid
Line 295: iptables -A OUTPUT -p tcp -j DROP # DROP TCP only
Analysis: No rule blocks -p udp (non-DNS) or -p icmp outbound. The container's OUTPUT filter chain default policy is ACCEPT, so non-DNS UDP and ICMP packets exit the container unimpeded at the iptables level.
Full mitigation exists at host level: host-iptables.ts FW_WRAPPER chain at the DOCKER-USER hook drops all UDP (except trusted DNS) and has default-deny. The container-level gap is compensated, but the defense relies on a single (outer) enforcement point for these protocols.
Finding T3: Seccomp profile is default-ALLOW
# Command:
python3 -c "import json,sysd=json.load(open('containers/agent/seccomp-profile.json'))print('Default action:', d.get('defaultAction'))blocked=[s for s in d.get('syscalls',[]) if s.get('action')=='SCMP_ACT_ERRNO']print('Blocked groups:', len(blocked))for s in blocked: print(' -', s['names'][:5], '...')"# Output:
Default action: SCMP_ACT_ALLOW
Blocked groups: 3
- ['ptrace', 'process_vm_readv', 'process_vm_writev'] ...
- ['kexec_load', 'kexec_file_load', 'reboot', 'init_module', ...] ...
- ['umount', 'umount2'] ...
Analysis: The custom profile blocks ptrace (memory inspection), kernel module ops, and umount. Docker's built-in default seccomp profile blocks ~47+ syscalls including unshare, clone with new namespaces, name_to_handle_at, etc. The custom profile is notably missing blocks for:
unshare — create new namespace (combined with lack of CLONE_NEWUSER block)
Mitigation: CAP_NET_ADMIN and CAP_SYS_ADMIN are dropped via capsh before user code runs. These drops make most namespace escape attempts fail regardless of seccomp policy.
Finding T4: URL pattern newline injection into Squid config
# Command:
sed -n '113,125p' src/squid-config.ts
# Output:if (urlPatterns && urlPatterns.length > 0) {
const urlAcls = urlPatterns
.map((pattern, i) => `acl allowed_url_\$\{i} url_regex \$\{pattern}`) .join('\n');# Also check parseUrlPatterns in ssl-bump.ts:grep -n "newline\|\\\\n\|\\.replace.*\\\\r" src/ssl-bump.ts# (no results — newlines not sanitized)
Analysis: If a URL pattern contained a literal newline (e.g., from reading --urls-file), the pattern would be split across Squid config lines. The second line could be an arbitrary Squid directive.
Example: A file containing (example.com/redacted) allow all would inject http_access allow all into the Squid config, bypassing domain restrictions.
Existing mitigations: (1) --allow-urls requires --ssl-bump, which is an advanced/unusual mode. (2) The parseDomains() function splits on commas, but --urls-file reads raw lines. (3) A HTTPS-scheme check runs but does not scan for embedded newlines.
✅ Recommendations
🔴 High — Address Soon
H1: Always install FW_WRAPPER_V6 chain when ip6tables is available, regardless of DNS config
Currently, the IPv6 DOCKER-USER chain is only set up when IPv6 DNS servers are explicitly configured. This means IPv6 filtering is absent in the default configuration.
// src/host-iptables.ts — change condition from:if(ipv6DnsServers.length>0){
...awaitsetupIpv6Chain(bridgeName);// to:if(ip6tablesAvailable){awaitsetupIpv6Chain(bridgeName);// Then within the chain, add DNS rules only if ipv6DnsServers.length > 0// Otherwise just default-deny all IPv6 traffic}
🟠 Medium — Plan to Address
M1: Add UDP and ICMP DROP rules in container-level OUTPUT chain
The container's setup-iptables.sh should block all non-DNS UDP and ICMP outbound to provide defense-in-depth independent of the host firewall.
# Add before the final TCP drop rule (setup-iptables.sh ~line 293):echo"[iptables] Drop all non-DNS UDP traffic..."
iptables -A OUTPUT -p udp -j DROP
echo"[iptables] Drop all ICMP traffic..."
iptables -A OUTPUT -p icmp -j DROP
M2: Harden seccomp profile to block additional dangerous syscalls
The custom seccomp profile should be extended to match or exceed Docker's built-in default seccomp coverage. At minimum, add blocks for:
L1: Sanitize newlines in URL patterns before Squid config generation
// src/ssl-bump.ts — in parseUrlPatterns():exportfunctionparseUrlPatterns(patterns: string[]): string[]{returnpatterns.map(pattern=>{// Strip any newline/carriage-return characters to prevent config injectionletp=pattern.replace(/[\r\n]/g,'');// ... rest of existing logic
L2: Consider whether host /dev needs to be mounted in chroot mode
Review whether all device nodes in /dev are necessary. If only /dev/null, /dev/zero, /dev/random, /dev/urandom, and /dev/pts are needed, prefer mounting a minimal tmpfs devtmpfs instead of bind-mounting the full host /dev.
L3: Add LOG rules for UDP/ICMP drops in container iptables
For forensic completeness, add LOG rules before the final DROP rules in the container's OUTPUT chain (mirroring the host-level [FW_BLOCKED_UDP] and [FW_BLOCKED_OTHER] prefix logging).
ℹ️ Informational
I1: Port range validation loop is O(range_size) — squid-config.ts:458 iterates every port in a user-specified range to check against DANGEROUS_PORTS. For range 1000-60000, this is ~59K iterations. Consider using a range intersection check instead.
I2: XOR obfuscation in one-shot-token is not cryptographic — Acknowledged in the code comments. No action needed; the mechanism correctly clears env vars from /proc/self/environ after first read.
I3: git config safe.directory '*' — Set globally for awfuser in entrypoint.sh. This is intentional for workspace compatibility but disables git ownership verification. Document this explicitly in security posture documentation.
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 analyzed 5,553 lines of security-critical code across 7 core files, covering the full defense-in-depth stack: host-level iptables, container-level iptables, Squid proxy ACL generation, domain pattern validation, container hardening, and credential protection. No firewall-escape-test workflow exists; analysis is based entirely on static code review with full command evidence.
Overall security posture: GOOD — multiple independent defense layers are correctly implemented. Findings are primarily gaps in the inner-most (container-level) layer that are already compensated by the outer layer, plus one conditional IPv6 bypass that depends on the host Docker configuration.
🔍 Phase 1: Previous Escape Test Context
No
firewall-escape-testworkflow was found in the repository. The closest equivalents aresecret-digger-claude,secret-digger-codex, andsecret-digger-copilot(hourly scheduled), andsecurity-guard(PR gating). This review stands alone as primary threat intelligence.🛡️ Phase 2: Architecture Security Analysis
Network Security — Two-Layer Firewall
The system correctly implements defense-in-depth with two independent enforcement points:
Layer 1 — Container-level (setup-iptables.sh)
Observation: The final DROP rule only blocks TCP. Non-DNS UDP and ICMPv4 outbound are not blocked at the container level.
Layer 2 — Host-level (src/host-iptables.ts)
The host chain correctly blocks UDP and has a default-deny final rule. This fully compensates for Layer 1's gap — but only for IPv4.
Container Hardening
Correctly implemented:
CAP_NET_ADMINis dropped before user code executes, preventing iptables bypass.Domain Validation
Correctly implemented: The anti-ReDoS wildcard replacement is good. Pattern anchoring prevents partial matches.
Credential Protection (One-Shot Token)
Correctly implemented with acknowledged limitations (XOR is not cryptographic; by-design for binary string hiding, not crypto protection).
host-iptables.ts:363— FW_WRAPPER_V6 only created ifipv6DnsServers.length > 0setup-iptables.sh:295— onlyiptables -A OUTPUT -p tcp -j DROPseccomp-profile.json—defaultAction: SCMP_ACT_ALLOW, only 3 deny groupssquid-config.ts:119—url_regex \$\{pattern}without newline sanitization/devread-onlydocker-manager.ts:541—'/dev:/host/dev:ro'setup-iptables.sh— noLOGrules in OUTPUT filter chainsquid-config.ts:458—for (let p = start; p <= end; p++)git config safe.directory '*'disables ownership checkentrypoint.sh:269— global safe.directory wildcard🎯 Attack Surface Map
--allow-domainsCLI flagcli.ts:669validateDomainOrPattern(), anti-ReDoS regex--allow-urlsCLI flagcli.ts:996--allow-host-portsCLI flagcli.ts:778--dns-serversCLI flagcli.ts:762--envCLI flagcli.ts:950--mountCLI flagcli.ts:964AWF_USER_UIDenv varentrypoint.sh:9squid-config.ts:119one-shot-token.cdocker-manager.ts:541📋 Evidence Collection
Finding T1: IPv6 bypass — FW_WRAPPER_V6 only installed when IPv6 DNS servers configured
Analysis: With default config (
--dns-servers 8.8.8.8,8.8.4.4— all IPv4),ipv6DnsServers.length === 0, soFW_WRAPPER_V6is never installed in ip6tables DOCKER-USER. If Docker has IPv6 enabled globally, containers onawf-netmay have IPv6 addresses and could initiate unfiltered outbound IPv6 connections.Partial mitigation:
awf-netis created without--ipv6, so IPv6 routing through that bridge is typically unavailable. The risk is low in most environments but non-zero on Docker installations withip6tables: truein daemon.json.Finding T2: Container UDP/ICMP not blocked at container layer
Analysis: No rule blocks
-p udp(non-DNS) or-p icmpoutbound. The container's OUTPUT filter chain default policy is ACCEPT, so non-DNS UDP and ICMP packets exit the container unimpeded at the iptables level.Full mitigation exists at host level:
host-iptables.tsFW_WRAPPER chain at the DOCKER-USER hook drops all UDP (except trusted DNS) and has default-deny. The container-level gap is compensated, but the defense relies on a single (outer) enforcement point for these protocols.Finding T3: Seccomp profile is default-ALLOW
Analysis: The custom profile blocks ptrace (memory inspection), kernel module ops, and umount. Docker's built-in default seccomp profile blocks ~47+ syscalls including
unshare,clonewith new namespaces,name_to_handle_at, etc. The custom profile is notably missing blocks for:unshare— create new namespace (combined with lack ofCLONE_NEWUSERblock)name_to_handle_at/open_by_handle_at— filesystem handle bypasssetns— join another namespaceMitigation:
CAP_NET_ADMINandCAP_SYS_ADMINare dropped viacapshbefore user code runs. These drops make most namespace escape attempts fail regardless of seccomp policy.Finding T4: URL pattern newline injection into Squid config
Analysis: If a URL pattern contained a literal newline (e.g., from reading
--urls-file), the pattern would be split across Squid config lines. The second line could be an arbitrary Squid directive.Example: A file containing
(example.com/redacted) allow allwould injecthttp_access allow allinto the Squid config, bypassing domain restrictions.Existing mitigations: (1)
--allow-urlsrequires--ssl-bump, which is an advanced/unusual mode. (2) TheparseDomains()function splits on commas, but--urls-filereads raw lines. (3) A HTTPS-scheme check runs but does not scan for embedded newlines.✅ Recommendations
🔴 High — Address Soon
H1: Always install FW_WRAPPER_V6 chain when ip6tables is available, regardless of DNS config
Currently, the IPv6 DOCKER-USER chain is only set up when IPv6 DNS servers are explicitly configured. This means IPv6 filtering is absent in the default configuration.
🟠 Medium — Plan to Address
M1: Add UDP and ICMP DROP rules in container-level OUTPUT chain
The container's
setup-iptables.shshould block all non-DNS UDP and ICMP outbound to provide defense-in-depth independent of the host firewall.M2: Harden seccomp profile to block additional dangerous syscalls
The custom seccomp profile should be extended to match or exceed Docker's built-in default seccomp coverage. At minimum, add blocks for:
{ "names": ["unshare", "setns", "name_to_handle_at", "open_by_handle_at", "clone3", "move_mount", "fsopen", "fsmount", "fsconfig", "fspick", "open_tree"], "action": "SCMP_ACT_ERRNO" }🟡 Low — Nice to Have
L1: Sanitize newlines in URL patterns before Squid config generation
L2: Consider whether host
/devneeds to be mounted in chroot modeReview whether all device nodes in
/devare necessary. If only/dev/null,/dev/zero,/dev/random,/dev/urandom, and/dev/ptsare needed, prefer mounting a minimal tmpfs devtmpfs instead of bind-mounting the full host/dev.L3: Add LOG rules for UDP/ICMP drops in container iptables
For forensic completeness, add LOG rules before the final DROP rules in the container's OUTPUT chain (mirroring the host-level
[FW_BLOCKED_UDP]and[FW_BLOCKED_OTHER]prefix logging).ℹ️ Informational
I1: Port range validation loop is O(range_size) —
squid-config.ts:458iterates every port in a user-specified range to check against DANGEROUS_PORTS. For range1000-60000, this is ~59K iterations. Consider using a range intersection check instead.I2: XOR obfuscation in one-shot-token is not cryptographic — Acknowledged in the code comments. No action needed; the mechanism correctly clears env vars from
/proc/self/environafter first read.I3:
git config safe.directory '*'— Set globally for awfuser inentrypoint.sh. This is intentional for workspace compatibility but disables git ownership verification. Document this explicitly in security posture documentation.📈 Security Metrics
Strengths worth calling out:
[a-zA-Z0-9.-]*instead of.*in wildcardsBeta Was this translation helpful? Give feedback.
All reactions