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
Evidence: src/host-iptables.ts:301-416
- Creates FW_WRAPPER chain inserted into DOCKER-USER
- Allows only 172.30.0.0/24 (awf-net) traffic outbound
- Drops all other Docker container traffic by default
Container-level DNAT (setup-iptables.sh):
# Key rules observed:
iptables -t nat -A OUTPUT -d "$SQUID_IP" -j RETURN # Skip DNAT for Squid itself
iptables -t nat -A OUTPUT -o lo -j RETURN # Loopback allowed
iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN # Localhost allowed# Docker embedded DNS preserved (127.0.0.11 DNAT rules restored after flush)# DNS restricted to AWF_DNS_SERVERS only# DNAT port 80/443 → Squid:3128 as defence-in-depth fallback```**IPv6 handling:**```
Evidence: src/host-iptables.ts:348, setup-iptables.sh grep "disable_ipv6"
- If ip6tables available: FW_WRAPPER_V6 chain created, IPv6 egress filtered
- If ip6tables unavailable: IPv6 disabled via sysctl (net.ipv6.conf.all.disable_ipv6=1)
- Both paths prevent unfiltered IPv6 bypass — defence-in-depth applied
Dangerous ports blocked in Squid config:
// src/squid-config.ts:14-31constDANGEROUS_PORTS=[22,23,25,110,143,445,1433,1521,3306,3389,5432,5984,6379,6984,8086,8088,9200,9300,27017,27018,28017];```**Raw IP access blocked:**```// src/squid-config.ts:590,707,716
# Preventsbypassingdomain-basedfilteringviadirectIPconnections
# DeniesrequeststorawIPv4/IPv6addresses
Assessment: Network architecture is layered (L3 iptables + L7 Squid) and covers known bypass vectors including raw IP access, IPv6 tunneling, and DNS exfiltration.
// Evidence: src/docker-manager.ts:1385-1393// SECURITY: NET_ADMIN is NOT granted to the agent container.// SYS_ADMIN is required to mount procfs at /host/proc// Security: SYS_CHROOT and SYS_ADMIN are dropped before running user commands
cap_add: ['SYS_CHROOT','SYS_ADMIN'],```**Seccomp profile:**```
Evidence: containers/agent/seccomp-profile.json
Default action: SCMP_ACT_ERRNO(deny-by-default)Totalsyscall rules: 371
SCMP_ACT_ALLOW: 343(explicitallowlist)
SCMP_ACT_ERRNO: 28(explicitblocks—defence-in-depth)
Explicitly blocked:
ptrace,process_vm_readv,process_vm_writev(processinspection)kexec_load,kexec_file_load,reboot,init_module,finit_module(kerneltampering)pivot_root,umount,umount2(namespace/mountmanipulation)add_key,request_key,keyctl(kernelkeyring)
no-new-privileges:
// Evidence: src/docker-manager.ts (confirmed in tests at lines 1252-1253, 2249, 2907, 3089)
security_opt: ['no-new-privileges:true','seccomp=<profile>']
Docker socket access:
// Evidence: src/docker-manager.ts:1176-1180// Hide Docker socket to prevent firewall bypass via 'docker run'agentVolumes.push('/dev/null:/host/var/run/docker.sock:ro');agentVolumes.push('/dev/null:/host/run/docker.sock:ro');// Both paths masked — covers both canonical path and symlink
UID/GID validation:
// Evidence: containers/agent/entrypoint.sh, src/docker-manager.ts:109// Prevent setting UID/GID to 0 (root) which defeats the privilege dropifUID==0: rejectwitherror
Assessment: Container hardening is rigorous. The separation of NET_ADMIN into a dedicated short-lived init container is a strong design decision that eliminates the window during which the agent holds network admin privileges.
// Evidence: src/domain-patterns.ts:27exportconstSQUID_DANGEROUS_CHARS=/[\s\0"'`;#]/;// Evidence: src/squid-config.ts (assertSafeForSquidConfig function)functionassertSafeForSquidConfig(value: string): string{if(SQUID_DANGEROUS_CHARS.test(value)){thrownewError(`SECURITY: Domain or pattern contains characters unsafe for Squid config...`);}returnvalue;}```**Wildcard restrictions (from tests at src/domain-patterns.test.ts:212-231):**```'*.'→throws"too broad"'.*'→throws"too broad"'*'→throws"matches all domains"'*.*'→throws"too broad"'*.*.*'→throws"too broad"'*.*.com'→throws"too many wildcard segments"```**Config injection tests:**```'evil.com\nhttp_access allow all'→throws"contains invalid character"'evil.com\rhttp_access allow all'→throws"contains invalid character"
Observation: Backslash (\) is not included in SQUID_DANGEROUS_CHARS but is checked separately in validateDomainOrPattern() per the comment at domain-patterns.ts:25. Defense is applied at two independent validation layers.
Input Validation Assessment
Shell argument escaping:
// Evidence: src/cli.ts:1050-1068exportfunctionescapeShellArg(arg: string): string{if(/^[a-zA-Z0-9_\-./=:]+$/.test(arg)){returnarg;}return`'\$\{arg.replace(/'/g, "'\\''")}'`;}```This is a correct implementation of POSIX single-quote escaping (CERT-C MSC41-C compliant).**execa usage — no shell: true:**```
Evidence: grep-rn"shell.*true"src/→0results
All execa calls use array-form arguments (not string-form), so no shell metacharacter injection is possible via exec arguments.
no-unsafe-execa suppression (ssl-bump.ts:216):
// Evidence: src/ssl-bump.ts:210-220awaitexeca('openssl',['req','-new','-newkey','rsa:2048', ...
// eslint-disable-next-line local/no-unsafe-execa'-subj',`/CN=\$\{commonName}`,```The `commonName` value is the SSL Bump CA common name (defaults to `'AWF Session CA'`). This suppresses the local lint rule but the value is not user-controlled — it is hardcoded in `ssl-bump.ts:42`. The risk is **Low**.---## ⚠️ Threat Model (STRIDE)| # | Category | Threat | Likelihood | Impact | Evidence ||---|---|---|---|---|---|| T1 | Spoofing | Agent spoofs Squid proxy via HTTPS CONNECT to unauthorized domain | Low | High | Squid ACL enforces domain allow-list; raw IP blocked || T2 | Tampering | Agent modifies iptables rules inside container | Very Low | Critical | iptables-init is separate container; agent never holds NET_ADMIN || T3 | Tampering | Agent modifies /etc/resolv.conf to use rogue DNS | Low | High | /etc written by entrypoint before cap drop; noexec procfs || T4 | Repudiation | Agent actions not attributable (no per-PID audit trail) | Medium | Medium | iptables LOG rules with `--log-uid`; Squid logs client IP; no per-PID correlation || T5 | Info Disclosure | Token exfiltration via /proc/1/environ | Low | Critical | `unset_sensitive_tokens()` clears env post-startup; one-shot-token LD_PRELOAD || T6 | Info Disclosure | Token re-read by malicious subprocess before unset | Low | Critical | one-shot-token LD_PRELOAD caches and zeroes token on first read (~100ms window) || T7 | Info Disclosure | LD_PRELOAD bypass via statically-linked binary | Low | High | Seccomp blocks dangerous syscalls; capability drop limits what can be exec'd || T8 | Info Disclosure | SSL Bump CA key exposed in workDir | Low | Medium | Key chmod 0o600 (ssl-bump.ts); workDir on host requires root or awfuser access || T9 | DoS | Subnet pool exhaustion from leaked Docker networks | Medium | Medium | Pre-test cleanup + `if: always()` CI cleanup guards; scripts/ci/cleanup.sh || T10 | DoS | Memory exhaustion via agent container | Low | Medium | Memory limits configurable via `--memory`; default depends on Docker daemon || T11 | Elevation | Docker socket access for container escape | Very Low | Critical | Socket masked with /dev/null by default; `--enable-dind` explicitly warned || T12 | Elevation | Pivot via DinD to spawn unrestricted container | Low | Critical | `--enable-dind` documented as firewall bypass; user must opt in || T13 | Elevation | chroot escape via SYS_CHROOT before cap drop | Very Low | High | SYS_CHROOT dropped immediately after chroot(); seccomp blocks pivot_root || T14 | Elevation | IPv6 egress bypass when ip6tables unavailable | Very Low | High | Mitigated: IPv6 disabled via sysctl when ip6tables absent || T15 | Elevation | Custom agent base image supply chain attack | Low | Critical | `validateAgentImage()` allowlist: ubuntu:*, catthehacker/* with optional `@sha256` |---## 🎯 Attack Surface Map| Surface | Entry Point | Current Protection | Risk ||---|---|---|---|| Squid domain ACL | `--allow-domains` CLI arg | `validateDomainOrPattern()` + `assertSafeForSquidConfig()` + two-layer injection prevention | Low || Host iptables | `src/host-iptables.ts` DOCKER-USER chain | FW_WRAPPER chain; IPv6 fallback to sysctl | Low || Container iptables | `setup-iptables.sh` | Separate init container; agent never has NET_ADMIN | Low || Docker socket | `/var/run/docker.sock` | Masked to /dev/null by default; DinD requires explicit opt-in with warning | Low || Agent base image | `--agent-image` CLI arg | `SAFE_BASE_IMAGE_PATTERNS` regex allowlist in cli.ts | Low || Credential env vars | Agent environment | `unset_sensitive_tokens()` + one-shot-token LD_PRELOAD | Low || Shell arg injection | User command string | `escapeShellArg()` + execa array-form | Low || DinD / `--enable-dind` | Explicit flag | Warning logged; documented firewall bypass | **Medium** || `--allow-host-service-ports` | Explicit flag | Bypasses dangerous port restrictions; host-gateway-only | **Medium** || `--enable-host-access` default ports | Implicit when localhost used | Broad default port list (12 ports auto-added) | **Medium** || SSL Bump CA | `--ssl-bump` flag | Per-session CA; key chmod 0o600; user warned | Low || `no-unsafe-execa` suppression | `src/ssl-bump.ts:216` | commonName is hardcoded, not user-controlled | Low || DIFC proxy external TLS | `--difc-proxy-host` | CA cert validated; TCP tunnel for SAN matching | Low |---## 📋 Evidence Collection<details><summary>Command: npm audit</summary>```$npmaudit--json|python3-c"..."NovulnerabilitiesfoundTotal: {'info': 0,'low': 0,'moderate': 0,'high': 0,'critical': 0,'total': 0}```</details><details><summary>Command: seccomp profile analysis</summary>```$python3-c"import json; d=json.load(open('containers/agent/seccomp-profile.json')); ..."Totalsyscallrules: 371SCMP_ACT_ALLOW: 343SCMP_ACT_ERRNO: 28Defaultaction: SCMP_ACT_ERRNOExplicitlyblocked: ptrace,process_vm_readv,process_vm_writev,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,umount,umount2```</details><details><summary>Command: shell:true usage search</summary>```$grep-rn"shell.*true\|{shell}"src/--include="*.ts"(0results—noshell:truein any execacall)```</details><details><summary>Command: Docker socket handling</summary>```$grep-n"docker.sock"src/docker-manager.ts1171: agentVolumes.push('/var/run/docker.sock:/host/var/run/docker.sock:rw');[DinDonly]1178: agentVolumes.push('/dev/null:/host/var/run/docker.sock:ro');[default]1179: agentVolumes.push('/dev/null:/host/run/docker.sock:ro');[symlink]```</details><details><summary>Command: Domain injection protection</summary>```$grep-n"SQUID_DANGEROUS_CHARS"src/domain-patterns.ts27: exportconstSQUID_DANGEROUS_CHARS=/[\s\0"'`;#]/;Newline/CRinjectiontestsallthrow—confirmedindomain-patterns.test.ts:237-245```</details><details><summary>Command: Security-critical source file sizes</summary>```$wc-lsrc/host-iptables.tssrc/docker-manager.tssrc/cli.tssrc/squid-config.ts \
src/domain-patterns.tscontainers/agent/entrypoint.shcontainers/agent/setup-iptables.sh703src/host-iptables.ts2934src/docker-manager.ts2344src/cli.ts855src/squid-config.ts343src/domain-patterns.ts911containers/agent/entrypoint.sh460containers/agent/setup-iptables.sh8550total
✅ Recommendations
🔴 Critical
None identified.
🟠 High
H1 — Document and gate --enable-dind more aggressively src/cli.ts:1454 has a comment noting firewall bypass is possible. Consider adding a required confirmation flag (e.g. --enable-dind-i-understand-firewall-bypass) or an explicit --allow-network-bypass acknowledgement to prevent accidental use in CI pipelines where --enable-dind might be passed silently.
H2 — one-shot-token: statically-linked binary gap
The LD_PRELOAD one-shot-token protection cannot intercept execve into statically-linked binaries (e.g., Go binaries, musl-static builds). If an agent spawns a static binary before the unset window completes, that binary can read /proc/1/environ directly. Mitigation: confirm that unset_sensitive_tokens() executes before any subprocess is spawned, and document the remaining risk.
🟡 Medium
M1 — --enable-host-access default port list is broad
When the user specifies localhost in --allow-domains, 12 dev-server ports are automatically added (3000,3001,4000,4200,5000,5173,8000,8080,8081,8888,9000,9090) via src/cli.ts:1041. If the host runs sensitive services on any of these ports, they become reachable from the agent. Recommend logging the auto-added port list at warn level and allowing users to set --allow-host-ports none to suppress defaults.
M2 — --allow-host-service-ports documents dangerous-port bypass without rate-limiting src/cli.ts:833: "bypasses dangerous port restrictions for host-local traffic." This is intentional but could allow SSH (22) or database ports to be reached on the host gateway if a user explicitly sets them. Confirm the warning is surfaced clearly in --help output and consider a secondary confirmation mechanism.
M3 — Repudiation gap: no per-process attribution in logs
iptables LOG uses --log-uid (UID) but not PID. Squid logs client IP but not process name. For forensic investigations (e.g., "which subprocess made this blocked connection?"), there is no way to correlate network events to specific process trees. Consider adding auditd or eBPF-based process-to-socket attribution as a future enhancement.
🟢 Low
L1 — SSL Bump CA key in workDir on host filesystem ssl-bump.ts sets chmod 0o600 on the key, but the workDir /tmp/awf-<ts> is owned by the invoking user. On shared systems (multi-user hosts), if the invoking user is root, the key is adequately protected. On systems where awf runs as a non-root user, the key inherits that user's protections. No change required; document this assumption.
L2 — no-unsafe-execa suppressed in ssl-bump.ts:216
The suppressed call passes -subj /CN=\$\{commonName} where commonName is a hardcoded constant ('AWF Session CA'). The suppression is safe. However, if commonName is ever made configurable, ensure it is validated before use. Add an inline comment explaining this constraint.
L3 — Domain file parsing allows comments but no schema validation parseDomainsFile() (src/cli.ts) strips # comments but does not enforce any maximum domain count or line length. A very large domains file would not fail gracefully. Add a configurable maximum (e.g., 1000 domains) and document the limit.
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
Review date: 2026-04-13
Reviewer: Automated security agent (Claude Sonnet 4.6)
Scope: Full codebase — network controls, container hardening, input validation, credential handling, supply-chain, dependency health
Security posture: Good — defence-in-depth is well-implemented. No critical vulnerabilities found. Several medium/low items warrant attention.
🔍 Findings from Prior Firewall Escape Test (Secret Digger — Copilot)
Workflow run:
secret-digger-copilot@ 2026-04-11T03:25Conclusion:
success— agent produced onlynoopsafe outputsSecret verification result:
successThe Secret Digger agent was unable to exfiltrate secrets or find exposed credentials, confirming that:
one-shot-tokenLD_PRELOAD +unset_sensitive_tokens) is functioning🛡️ Architecture Security Analysis
Network Security Assessment
Evidence gathered from:
src/host-iptables.ts(703 lines),containers/agent/setup-iptables.sh(460 lines),src/squid-config.ts(855 lines)Host-level (DOCKER-USER chain / FW_WRAPPER):
Container-level DNAT (setup-iptables.sh):
Dangerous ports blocked in Squid config:
Assessment: Network architecture is layered (L3 iptables + L7 Squid) and covers known bypass vectors including raw IP access, IPv6 tunneling, and DNS exfiltration.
Container Security Assessment
Evidence gathered from:
src/docker-manager.ts(2934 lines),containers/agent/entrypoint.sh(911 lines),containers/agent/seccomp-profile.jsonCapability model:
no-new-privileges:
Docker socket access:
UID/GID validation:
Assessment: Container hardening is rigorous. The separation of NET_ADMIN into a dedicated short-lived init container is a strong design decision that eliminates the window during which the agent holds network admin privileges.
Domain Validation Assessment
Evidence gathered from:
src/domain-patterns.ts(343 lines),src/squid-config.tsInjection prevention:
Observation: Backslash (
\) is not included inSQUID_DANGEROUS_CHARSbut is checked separately invalidateDomainOrPattern()per the comment atdomain-patterns.ts:25. Defense is applied at two independent validation layers.Input Validation Assessment
Shell argument escaping:
All
execacalls use array-form arguments (not string-form), so no shell metacharacter injection is possible via exec arguments.no-unsafe-execa suppression (ssl-bump.ts:216):
✅ Recommendations
🔴 Critical
None identified.
🟠 High
H1 — Document and gate
--enable-dindmore aggressivelysrc/cli.ts:1454has a comment noting firewall bypass is possible. Consider adding a required confirmation flag (e.g.--enable-dind-i-understand-firewall-bypass) or an explicit--allow-network-bypassacknowledgement to prevent accidental use in CI pipelines where--enable-dindmight be passed silently.H2 — one-shot-token: statically-linked binary gap
The LD_PRELOAD one-shot-token protection cannot intercept
execveinto statically-linked binaries (e.g., Go binaries, musl-static builds). If an agent spawns a static binary before the unset window completes, that binary can read/proc/1/environdirectly. Mitigation: confirm thatunset_sensitive_tokens()executes before any subprocess is spawned, and document the remaining risk.🟡 Medium
M1 —
--enable-host-accessdefault port list is broadWhen the user specifies
localhostin--allow-domains, 12 dev-server ports are automatically added (3000,3001,4000,4200,5000,5173,8000,8080,8081,8888,9000,9090) viasrc/cli.ts:1041. If the host runs sensitive services on any of these ports, they become reachable from the agent. Recommend logging the auto-added port list atwarnlevel and allowing users to set--allow-host-ports noneto suppress defaults.M2 —
--allow-host-service-portsdocuments dangerous-port bypass without rate-limitingsrc/cli.ts:833: "bypasses dangerous port restrictions for host-local traffic." This is intentional but could allow SSH (22) or database ports to be reached on the host gateway if a user explicitly sets them. Confirm the warning is surfaced clearly in--helpoutput and consider a secondary confirmation mechanism.M3 — Repudiation gap: no per-process attribution in logs
iptables LOG uses
--log-uid(UID) but not PID. Squid logs client IP but not process name. For forensic investigations (e.g., "which subprocess made this blocked connection?"), there is no way to correlate network events to specific process trees. Consider adding auditd or eBPF-based process-to-socket attribution as a future enhancement.🟢 Low
L1 — SSL Bump CA key in workDir on host filesystem
ssl-bump.tssetschmod 0o600on the key, but the workDir/tmp/awf-<ts>is owned by the invoking user. On shared systems (multi-user hosts), if the invoking user is root, the key is adequately protected. On systems where awf runs as a non-root user, the key inherits that user's protections. No change required; document this assumption.L2 —
no-unsafe-execasuppressed in ssl-bump.ts:216The suppressed call passes
-subj /CN=\$\{commonName}wherecommonNameis a hardcoded constant ('AWF Session CA'). The suppression is safe. However, ifcommonNameis ever made configurable, ensure it is validated before use. Add an inline comment explaining this constraint.L3 — Domain file parsing allows comments but no schema validation
parseDomainsFile()(src/cli.ts) strips#comments but does not enforce any maximum domain count or line length. A very large domains file would not fail gracefully. Add a configurable maximum (e.g., 1000 domains) and document the limit.📈 Security Metrics
shell: trueexec callsBeta Was this translation helpful? Give feedback.
All reactions