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 comprehensive security analysis of the AWF (Agentic Workflow Firewall) codebase conducted on 2026-03-31. The system implements defense-in-depth with multiple layers: host-level iptables, Squid L7 proxy, container capability restriction, seccomp profiling, and LD_PRELOAD credential isolation. The overall security posture is strong, with one Critical finding and several medium/low observations.
Metric
Value
Critical findings
1
High findings
2
Medium findings
3
Low findings
3
npm audit vulnerabilities
0
Security-critical files analyzed
12
🔍 Phase 1: Firewall Escape Test Status
The security-review workflow is compiled and active. No firewall-escape-test workflow was found in the workflow registry. The existing secret-digger-* workflows (claude, codex, copilot variants) run hourly and cover credential exfiltration scenarios.
🛡️ Architecture Security Analysis
Network Security Assessment
✅ Strengths:
The iptables architecture is well-layered:
Host-level DOCKER-USER chain (src/host-iptables.ts) — creates a dedicated FW_WRAPPER chain that restricts egress from the Docker bridge. The rule ordering is correct: allow Squid → allow DNS → allow API proxy → block UDP → default-deny. Evidence:
# src/host-iptables.ts:521
// 8. Default deny all other traffic
await execa('iptables', ['-t', 'filter', '-A', CHAIN_NAME, '-j', 'REJECT', ...]);
Container NAT DNAT redirect (containers/agent/setup-iptables.sh) — all port 80/443 traffic is force-redirected to Squid at 172.30.0.10:3128 before the agent starts, covering proxy-unaware tools.
IPv6 hardening — when ip6tables is unavailable, IPv6 is disabled via sysctl to prevent an unfiltered bypass path (src/host-iptables.ts:119-129).
DNS exfiltration prevention — DNS queries are restricted to configured upstream servers only; non-configured servers are rejected by the DOCKER-USER chain (src/host-iptables.ts:363-388).
⚠️ Observation — No private RFC-1918 egress restriction at the host firewall level:
The FW_WRAPPER chain allows any TCP traffic that exits through Squid. However, Squid only allows traffic to whitelisted domains by name. If an agent resolves an internal RFC-1918 IP address through a domain on the allow list, Squid's domain ACL enforcement by hostname means the IP-level traffic would depend on Squid's DNS resolving correctly. This is an accepted trade-off in the current architecture.
Container Security Assessment
✅ Strengths:
NET_ADMIN is never granted to the agent container; it is isolated to the short-lived awf-iptables-init init container that exits after iptables setup.
NET_RAW is explicitly dropped from the agent (src/docker-manager.ts:1172), preventing raw socket creation that could bypass DNAT rules.
no-new-privileges:true is applied to all containers (src/docker-manager.ts:1183).
Memory and PID limits are set: agent gets 6g / 1000 PIDs, Squid/init get 128m / 50 PIDs (src/docker-manager.ts:1191, 1325).
SYS_CHROOT and SYS_ADMIN are dropped via capsh before user code runs (containers/agent/entrypoint.sh:353).
docker-stub.sh blocks Docker-in-Docker by replacing the docker binary with an error stub.
⚠️ Finding (High) — Agent container runs with apparmor:unconfined:
The agent container explicitly disables AppArmor confinement. This removes a key kernel-level MAC layer that would otherwise prevent privilege escalation techniques like mount namespace manipulation. Combined with SYS_ADMIN being temporarily present (dropped just before user code), this represents a window of risk.
⚠️ Finding (Medium) — unshare, mount, and setns allowed in seccomp profile:
# Evidence from analysis
Status of security-sensitive syscalls:
unshare: ALLOWED
mount: ALLOWED
setns: ALLOWED
ptrace: BLOCKED
process_vm_readv: BLOCKED
mount is intentionally allowed for procfs mounting in chroot mode. However, unshare (create new namespace) combined with setns (join another namespace) are allowed. Without AppArmor, a process with sufficient capabilities could use these to attempt network namespace manipulation during the window when SYS_ADMIN is present.
The seccomp profile (containers/agent/seccomp-profile.json) has defaultAction: SCMP_ACT_ERRNO (allow-list model) which is correct. However, mount, unshare, and setns are all in the allow list.
Domain Validation Assessment — 🚨 CRITICAL FINDING
CRITICAL — Squid configuration injection via newline-embedded domain names:
validateDomainOrPattern in src/domain-patterns.ts does not reject domain strings containing newline characters (\n, \r\n) or other whitespace. Since domains are interpolated directly into Squid's configuration file, an attacker who controls the --allow-domains argument can inject arbitrary Squid directives.
Impact: An http_access allow all line injected before the http_access deny all terminal rule causes Squid to allow traffic to any domain, completely bypassing the whitelist. The same injection works via --block-domains.
Attack vector: This is exploitable when:
awf is invoked from a CI/CD pipeline or script where the domain list is sourced from an untrusted input (e.g., an issue body, PR description, environment variable set by a workflow trigger, or agentic task parameter).
A prompt-injected agent task passes a malicious domain string to the awf invocation.
Proof of exploitability (src/docker-manager.ts:1744): The Squid config is base64-encoded and injected as an environment variable — the malicious newline survives encoding/decoding intact.
Files affected:src/domain-patterns.ts (missing newline check in validateDomainOrPattern), src/squid-config.ts (no sanitization on domain interpolation).
Input Validation Assessment
✅ Strengths:
Port specifications validated with isValidPortSpec() preventing numeric injection.
Wildcard patterns use character class [a-zA-Z0-9.-]* instead of .* to prevent ReDoS (src/domain-patterns.ts:87).
Domain length capped at 512 chars for regex matching (src/domain-patterns.ts:246).
UID/GID 0 rejected in entrypoint (containers/agent/entrypoint.sh:25-31).
⚠️ Finding (High) — DLP only inspects URLs, not request bodies:
The DLP system (src/dlp.ts) uses Squid url_regex ACLs, which match only the URL of HTTP requests. Credential values embedded in POST body JSON (e.g., {"token": "ghp_xxxxx..."}) are not inspected. An agent that exfiltrates credentials through a POST body to an allowed domain (e.g., a GitHub API endpoint) would not be detected by DLP.
# src/squid-config.ts:154
.map((pattern, i) => `acl allowed_url_\$\{i} url_regex \$\{pattern}`)# url_regex matches URL only — no body inspection````This requires SSL Bump to be enabled for URL inspection to work at all for HTTPS, and even then only URLs (not bodies) are checked.**Note:** Full request body inspection would require SSL Bump mode and an ICAP service, which is architecturally complex. This is a known limitation of the current approach.---## ⚠️ Threat Model (STRIDE)| # | Category | Threat | Likelihood | Impact | Severity ||---|----------|--------|-----------|--------|----------|| T1 | Tampering | Squid config injection via newline in `--allow-domains` | Medium | Critical | **CRITICAL** || T2 | EoP | `SYS_ADMIN` + `unshare`/`setns` + `apparmor:unconfined` window | Low | High | **High** || T3 | Info Disclosure | Credential exfiltration via POST body to allowed domain (DLP gap) | Medium | High | **High** || T4 | Spoofing | API proxy accepts unauthenticated requests from any container-network process | Low | Medium | **Medium** || T5 | Repudiation | Squid logs use Unix timestamps; no tamper-evident log chain | Low | Medium | **Medium** || T6 | DoS | No rate-limiting at the iptables or Squid level for egress connections | Low | Medium | **Medium** || T7 | Info Disclosure | `docker-compose.yml` containing tokens world-readable briefly before `chmod 0700` | Very Low | Low | **Low** || T8 | Tampering | DNS response poisoning if upstream DNS servers are compromised | Very Low | Medium | **Low** || T9 | EoP | `mount` syscall allowed; could mount `/proc` again after `SYS_ADMIN` drop if combined with namespace tricks | Very Low | Low | **Low** |---## 🎯 Attack Surface Map| Entry Point | File:Line | Current Protections | Risk ||-------------|-----------|---------------------|------|| `--allow-domains` CLI arg | `src/domain-patterns.ts:validateDomainOrPattern` | Pattern/length validation, no newline check | **CRITICAL** || `--block-domains` CLI arg | `src/squid-config.ts:355` | Same validation path | **CRITICAL** || Agent command string | `src/cli.ts:932 escapeShellArg()` | Single-quote escape | Low || Port spec arguments | `src/cli.ts:isValidPortSpec()` | Numeric regex validation | Low || API proxy (port 10001) | `containers/api-proxy/server.js` | Rate limiting, credential stripping, no caller auth | Medium || Docker Compose YAML generation | `src/docker-manager.ts:generateDockerCompose()` | js-yaml serialization, `lineWidth:-1` | Low || LD_PRELOAD one-shot tokens | `containers/agent/entrypoint.sh:756` | Cached in process memory, env var cleared | Low || AWF_SQUID_CONFIG_B64 env var | `src/docker-manager.ts:431` | Base64 encoding (not security boundary) | Medium (via T1) || Seccomp profile path | `src/cli.ts:1684` | Copied from dist, owner-only workDir | Low |---## 📋 Evidence Collection<details><summary>Domain injection proof-of-concept output</summary>````$ node -e "const { generateSquidConfig } = require('./dist/squid-config.js');const cfg = generateSquidConfig({ domains: ['github.com\nhttp_access allow all\n# '], blockedDomains: [], port: 3128, dnsServers: ['8.8.8.8'],});if (cfg.includes('http_access allow all')) console.log('INJECTION SUCCEEDED');
const lines = cfg.split('\n');
const idx = lines.findIndex(l => l.includes('allow all'));
console.log(lines.slice(idx-1, idx+3).join('\n'));"INJECTION SUCCEEDEDacl allowed_domains dstdomain .github.comhttp_access allow all#````</details><details><summary>Security-sensitive syscall status from seccomp profile</summary>````unshare: ALLOWEDmount: ALLOWEDsetns: ALLOWEDptrace: BLOCKEDprocess_vm_readv: BLOCKEDprocess_vm_writev: BLOCKEDdefaultAction: SCMP_ACT_ERRNO (allowlist model — correct)Total allow entries: ~250 standard syscalls````</details><details><summary>npm audit results</summary>````Total vulnerabilities: 0Critical/High: 0````</details><details><summary>AppArmor configuration</summary>````# src/docker-manager.ts:1182-1185security_opt: [ 'no-new-privileges:true',`seccomp=\$\{config.workDir}/seccomp-profile.json`, 'apparmor:unconfined', ← explicitly unconfined],
✅ Recommendations
🔴 Critical — Fix Immediately
C1: Reject newlines and whitespace in domain names (src/domain-patterns.ts)
Add a check in validateDomainOrPattern before the existing checks:
// In validateDomainOrPattern(), before existing checks:if(/[\r\n\t\s]/.test(trimmed)){thrownewError(`Invalid domain '\$\{input}': contains whitespace or newline characters`);}
Additionally, add a sanitization guard in generateSquidConfig as defense-in-depth — escape or reject any domain that would produce a multi-line Squid ACL entry when interpolated.
🟠 High — Fix Soon
H1: Enable AppArmor or provide a custom profile for the agent container
Rather than apparmor:unconfined, consider using Docker's default AppArmor profile (docker-default) or a custom profile that restricts mount and namespace syscalls. The current justification for unconfined should be documented if it is intentional. If unconfined is required for chroot to work on all host distributions (some AppArmor profiles block chroot), document this and add a compensating control.
H2: Extend DLP to inspect request body content via ICAP or Squid response_body_max_size
The current DLP implementation only inspects URLs. Adding an ICAP adapter (e.g., C-ICAP) to Squid would allow credential pattern detection in POST bodies. Alternatively, document this limitation prominently so operators understand that --enable-dlp does not protect against body-level exfiltration.
🟡 Medium — Plan to Address
M1: Add caller authentication to the API proxy sidecar
The API proxy (containers/api-proxy/server.js) accepts any unauthenticated HTTP request from the 172.30.0.0/24 subnet. While the Squid firewall restricts which endpoints the proxy can reach, a compromised agent could still make arbitrary API calls through it. Adding a shared secret (e.g., a UUID injected as an environment variable and required as a bearer token) would prevent other processes that might gain access to the network subnet from using the proxy.
M2: Restrict unshare and setns in the seccomp profile
These syscalls are not needed by the agent's user workloads. Blocking them in containers/agent/seccomp-profile.json reduces the attack surface during the SYS_ADMIN capability window without breaking any documented functionality.
M3: Document the window between startContainers and iptables-init completion
The init container writes /tmp/awf-init/ready and the agent's entrypoint.sh waits for it before executing the user command. This architecture correctly ensures the agent cannot run before iptables is set up. However, this guarantee should be documented in AGENTS.md as it is a critical security invariant.
🟢 Low — Nice to Have
L1: Consider structured logging with log integrity (append-only, signed)
Squid logs currently use Unix timestamps and are written to a shared volume. Adding log signing or shipping to an append-only store would improve forensic non-repudiation (STRIDE: Repudiation).
L2: Add rate-limiting at the Squid layer in addition to the API proxy layer
The API proxy has per-provider rate limits, but the Squid proxy itself has no connection or bandwidth limits. A compromised agent could flood outbound connections to allowed domains. Squid's delay_pools or max_filedesc can be used for this.
L3: Document apparmor:unconfined as a known trade-off
Add a comment in src/docker-manager.ts:1185 explaining why AppArmor is disabled (e.g., cross-distro compatibility with chroot operations) and what compensating controls exist (seccomp, capability drop, no-new-privs).
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 comprehensive security analysis of the AWF (Agentic Workflow Firewall) codebase conducted on 2026-03-31. The system implements defense-in-depth with multiple layers: host-level iptables, Squid L7 proxy, container capability restriction, seccomp profiling, and LD_PRELOAD credential isolation. The overall security posture is strong, with one Critical finding and several medium/low observations.
🔍 Phase 1: Firewall Escape Test Status
The
security-reviewworkflow is compiled and active. Nofirewall-escape-testworkflow was found in the workflow registry. The existingsecret-digger-*workflows (claude, codex, copilot variants) run hourly and cover credential exfiltration scenarios.🛡️ Architecture Security Analysis
Network Security Assessment
✅ Strengths:
The iptables architecture is well-layered:
src/host-iptables.ts) — creates a dedicatedFW_WRAPPERchain that restricts egress from the Docker bridge. The rule ordering is correct: allow Squid → allow DNS → allow API proxy → block UDP → default-deny. Evidence:Container NAT DNAT redirect (
containers/agent/setup-iptables.sh) — all port 80/443 traffic is force-redirected to Squid at172.30.0.10:3128before the agent starts, covering proxy-unaware tools.IPv6 hardening — when
ip6tablesis unavailable, IPv6 is disabled via sysctl to prevent an unfiltered bypass path (src/host-iptables.ts:119-129).DNS exfiltration prevention — DNS queries are restricted to configured upstream servers only; non-configured servers are rejected by the DOCKER-USER chain (
src/host-iptables.ts:363-388).Metadata endpoint blocking — link-local
169.254.0.0/16(AWS/GCP metadata service) is explicitly blocked (src/host-iptables.ts:499-503).The FW_WRAPPER chain allows any TCP traffic that exits through Squid. However, Squid only allows traffic to whitelisted domains by name. If an agent resolves an internal RFC-1918 IP address through a domain on the allow list, Squid's domain ACL enforcement by hostname means the IP-level traffic would depend on Squid's DNS resolving correctly. This is an accepted trade-off in the current architecture.
Container Security Assessment
✅ Strengths:
NET_ADMINis never granted to the agent container; it is isolated to the short-livedawf-iptables-initinit container that exits after iptables setup.NET_RAWis explicitly dropped from the agent (src/docker-manager.ts:1172), preventing raw socket creation that could bypass DNAT rules.no-new-privileges:trueis applied to all containers (src/docker-manager.ts:1183).6g/ 1000 PIDs, Squid/init get128m/ 50 PIDs (src/docker-manager.ts:1191, 1325).SYS_CHROOTandSYS_ADMINare dropped viacapshbefore user code runs (containers/agent/entrypoint.sh:353).docker-stub.shblocks Docker-in-Docker by replacing thedockerbinary with an error stub.apparmor:unconfined:The agent container explicitly disables AppArmor confinement. This removes a key kernel-level MAC layer that would otherwise prevent privilege escalation techniques like
mountnamespace manipulation. Combined withSYS_ADMINbeing temporarily present (dropped just before user code), this represents a window of risk.unshare,mount, andsetnsallowed in seccomp profile:# Evidence from analysis Status of security-sensitive syscalls: unshare: ALLOWED mount: ALLOWED setns: ALLOWED ptrace: BLOCKED process_vm_readv: BLOCKEDmountis intentionally allowed for procfs mounting in chroot mode. However,unshare(create new namespace) combined withsetns(join another namespace) are allowed. Without AppArmor, a process with sufficient capabilities could use these to attempt network namespace manipulation during the window whenSYS_ADMINis present.The seccomp profile (
containers/agent/seccomp-profile.json) hasdefaultAction: SCMP_ACT_ERRNO(allow-list model) which is correct. However,mount,unshare, andsetnsare all in the allow list.Domain Validation Assessment — 🚨 CRITICAL FINDING
CRITICAL — Squid configuration injection via newline-embedded domain names:
validateDomainOrPatterninsrc/domain-patterns.tsdoes not reject domain strings containing newline characters (\n,\r\n) or other whitespace. Since domains are interpolated directly into Squid's configuration file, an attacker who controls the--allow-domainsargument can inject arbitrary Squid directives.Impact: An
http_access allow allline injected before thehttp_access deny allterminal rule causes Squid to allow traffic to any domain, completely bypassing the whitelist. The same injection works via--block-domains.Attack vector: This is exploitable when:
awfis invoked from a CI/CD pipeline or script where the domain list is sourced from an untrusted input (e.g., an issue body, PR description, environment variable set by a workflow trigger, or agentic task parameter).awfinvocation.Proof of exploitability (
src/docker-manager.ts:1744): The Squid config is base64-encoded and injected as an environment variable — the malicious newline survives encoding/decoding intact.Files affected:
src/domain-patterns.ts(missing newline check invalidateDomainOrPattern),src/squid-config.ts(no sanitization on domain interpolation).Input Validation Assessment
✅ Strengths:
isValidPortSpec()preventing numeric injection.escapeShellArg()properly single-quote-escapes arguments (src/cli.ts:936).[a-zA-Z0-9.-]*instead of.*to prevent ReDoS (src/domain-patterns.ts:87).src/domain-patterns.ts:246).containers/agent/entrypoint.sh:25-31).The DLP system (
src/dlp.ts) uses Squidurl_regexACLs, which match only the URL of HTTP requests. Credential values embedded in POST body JSON (e.g.,{"token": "ghp_xxxxx..."}) are not inspected. An agent that exfiltrates credentials through a POST body to an allowed domain (e.g., a GitHub API endpoint) would not be detected by DLP.✅ Recommendations
🔴 Critical — Fix Immediately
C1: Reject newlines and whitespace in domain names (
src/domain-patterns.ts)Add a check in
validateDomainOrPatternbefore the existing checks:Additionally, add a sanitization guard in
generateSquidConfigas defense-in-depth — escape or reject any domain that would produce a multi-line Squid ACL entry when interpolated.🟠 High — Fix Soon
H1: Enable AppArmor or provide a custom profile for the agent container
Rather than
apparmor:unconfined, consider using Docker's default AppArmor profile (docker-default) or a custom profile that restrictsmountand namespace syscalls. The current justification forunconfinedshould be documented if it is intentional. Ifunconfinedis required for chroot to work on all host distributions (some AppArmor profiles block chroot), document this and add a compensating control.H2: Extend DLP to inspect request body content via ICAP or Squid response_body_max_size
The current DLP implementation only inspects URLs. Adding an ICAP adapter (e.g., C-ICAP) to Squid would allow credential pattern detection in POST bodies. Alternatively, document this limitation prominently so operators understand that
--enable-dlpdoes not protect against body-level exfiltration.🟡 Medium — Plan to Address
M1: Add caller authentication to the API proxy sidecar
The API proxy (
containers/api-proxy/server.js) accepts any unauthenticated HTTP request from the172.30.0.0/24subnet. While the Squid firewall restricts which endpoints the proxy can reach, a compromised agent could still make arbitrary API calls through it. Adding a shared secret (e.g., a UUID injected as an environment variable and required as a bearer token) would prevent other processes that might gain access to the network subnet from using the proxy.M2: Restrict
unshareandsetnsin the seccomp profileThese syscalls are not needed by the agent's user workloads. Blocking them in
containers/agent/seccomp-profile.jsonreduces the attack surface during theSYS_ADMINcapability window without breaking any documented functionality.M3: Document the window between
startContainersand iptables-init completionThe init container writes
/tmp/awf-init/readyand the agent'sentrypoint.shwaits for it before executing the user command. This architecture correctly ensures the agent cannot run before iptables is set up. However, this guarantee should be documented inAGENTS.mdas it is a critical security invariant.🟢 Low — Nice to Have
L1: Consider structured logging with log integrity (append-only, signed)
Squid logs currently use Unix timestamps and are written to a shared volume. Adding log signing or shipping to an append-only store would improve forensic non-repudiation (STRIDE: Repudiation).
L2: Add rate-limiting at the Squid layer in addition to the API proxy layer
The API proxy has per-provider rate limits, but the Squid proxy itself has no connection or bandwidth limits. A compromised agent could flood outbound connections to allowed domains. Squid's
delay_poolsormax_filedesccan be used for this.L3: Document
apparmor:unconfinedas a known trade-offAdd a comment in
src/docker-manager.ts:1185explaining why AppArmor is disabled (e.g., cross-distro compatibility with chroot operations) and what compensating controls exist (seccomp, capability drop, no-new-privs).📈 Security Metrics
cli.ts,docker-manager.ts,host-iptables.ts,squid-config.ts,domain-patterns.ts,dlp.ts)entrypoint.sh,setup-iptables.sh,docker-stub.sh,seccomp-profile.json,server.js)Beta Was this translation helpful? Give feedback.
All reactions