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 analysis of the @github/agentic-workflow-firewall codebase, examining ~4,104 lines of security-critical code across the network filtering stack (host-iptables, setup-iptables, Squid config, container entrypoint, and Docker orchestration). The system's overall security posture is good, with multiple defense-in-depth layers. Three actionable findings are documented below.
Category
Count
Critical
0
High
0
Medium
2
Low
2
Informational
4
🔍 Findings from Firewall Escape Test
The security-review workflow was the most recent in-scope agentic workflow; no prior escape-test run logs were accessible via agenticworkflows-logs (download error). Complementary findings from previous security guards are reflected where patterns overlap.
🛡️ Architecture Security Analysis
Network Security Assessment — Strong
The traffic interception chain is well-designed:
Host-level DOCKER-USER chain (src/host-iptables.ts) creates a custom FW_WRAPPER chain that filters all egress from the bridge interface. Rules are ordered correctly: Squid exempt → ESTABLISHED/RELATED → localhost → trusted DNS → Squid port → API proxy port → multicast/link-local block → UDP block → default deny.
Container-level NAT (containers/agent/setup-iptables.sh) redirects HTTP/HTTPS to Squid and terminates all other TCP with a final DROP rule.
Squid ACL (src/squid-config.ts) enforces domain whitelisting and blocks dangerous ports via !Safe_ports.
DNS exfiltration prevention is implemented at both layers: only whitelisted DNS server IPs are permitted (default Google 8.8.8.8/8.8.4.4).
IPv6 handling: ip6tables is set up when available; when unavailable, sysctl net.ipv6.conf.all.disable_ipv6=1 prevents unfiltered IPv6 bypass.
NET_ADMIN is added to the agent container for iptables setup and then dropped via capsh before user code executes (entrypoint.sh:649, cap_drop: ['NET_RAW','SYS_PTRACE','SYS_MODULE','SYS_RAWIO','MKNOD'] in docker-manager.ts:872-878).
no-new-privileges:true prevents re-acquisition of dropped privileges.
Resource limits are applied: mem_limit: 4g, pids_limit: 1000.
One-shot token library (LD_PRELOAD=/usr/local/lib/one-shot-token.so) protects secrets from /proc/self/environ re-reads after agent startup.
Token unsetting (unset_sensitive_tokens) runs after a 5-second grace period for agent initialization.
Domain Validation Assessment — Strong
ReDoS protection: wildcards become [a-zA-Z0-9.-]* (not .*) in domain-patterns.ts:77.
Overly-broad patterns (*, *.*, *.*.com) are rejected.
Domain length capped at 512 chars to prevent regex abuse (isDomainMatchedByPattern:279).
A custom ESLint rule (eslint-rules/no-unsafe-execa.js) statically enforces that execa() is only called with literal string commands — no template literals with expressions or string concatenation. Only 2 justified eslint-disable exceptions exist (log-discovery.ts:157 for container name filter, ssl-bump.ts:201 for OpenSSL cert subject — both use carefully-controlled variables, not user input).
All commands are passed as arrays to execa, not through a shell.
⚠️ Threat Model
#
Threat (STRIDE)
Description
Evidence
Severity
Likelihood
T1
Elevation of Privilege
Permissive custom seccomp profile
seccomp-profile.json + docker-manager.ts:885
Medium
Low
T2
Elevation of Privilege
AppArmor disabled on agent container
docker-manager.ts:886
Medium
Low
T3
Tampering
Domain names not checked for newline characters — Squid config injection
domain-patterns.ts:139-185
Low
Very Low
T4
Spoofing
Dangerous ports list inconsistency between Squid and iptables
squid-config.ts:10-31 vs setup-iptables.sh:203-221
Docker's built-in default seccomp profile uses SCMP_ACT_ERRNO as its default (deny-by-default allowlist). The AWF custom profile uses SCMP_ACT_ALLOW (allow-by-default blocklist). Syscalls not explicitly blocked include mount, unshare, and clone with CLONE_NEWUSER. These are mitigated by capability drops (cap_drop: CAP_SYS_ADMIN via capsh before user code) but the profile itself provides weaker guarantees.
Applied in docker-manager.ts:885:
security_opt: ['no-new-privileges:true',`seccomp=\$\{config.workDir}/seccomp-profile.json`,'apparmor:unconfined',// <-- AppArmor also disabled (T2)],
Finding T2: AppArmor Disabled
// docker-manager.ts:880-886// AppArmor is set to unconfined to allow mounting procfs at /host/proc// (Docker's default AppArmor profile blocks mount). This is safe because SYS_ADMIN is// dropped via capsh before user code runs, so user code cannot mount anything.security_opt: ['no-new-privileges:true',`seccomp=\$\{config.workDir}/seccomp-profile.json`,'apparmor:unconfined',],
The comment explains the rationale (procfs mounting). However, apparmor:unconfined removes MAC-level process confinement for the entire container lifecycle, not just during setup. Once capsh drops SYS_ADMIN, mount calls from user code would fail at the capability check — but AppArmor would no longer provide an independent audit trail or secondary enforcement for other operations (file access, network access from processes, etc.).
Finding T3: Domain Names Not Checked for Newlines (Squid Config Injection)
The validateDomainOrPattern() function in domain-patterns.ts:139-185 validates:
Empty input
*, *.*, [*.]+ (too broad)
.. (double dots)
. alone, *., .*
Too many wildcard segments
But it does not check for newline characters (\n, \r). Domains are then embedded directly into the Squid configuration:
A domain like github.com\nhttp_access allow all would be parsed by commander as a single argument containing a literal newline (if passed via a config file or programmatic API), and when written to squid.conf, the second line would become http_access allow all, allowing unrestricted access.
Mitigating factors: Shell argument parsing strips/escapes newlines in practice when domains are passed via CLI. The parseDomains() function splits on commas but not newlines. Risk is very low in normal CLI usage but theoretically present if the AWF library is used programmatically or if the allow-domains-file feature is used with malicious input.
Finding T4: Dangerous Ports List Inconsistency
src/squid-config.ts DANGEROUS_PORTS (21 entries) includes ports not in setup-iptables.sh (15 entries):
5984 (CouchDB), 6984 (CouchDB SSL)
8086, 8088 (InfluxDB)
9200, 9300 (Elasticsearch)
However, in the container's OUTPUT filter chain, the final rule is:
iptables -A OUTPUT -p tcp -j DROP
This catches all TCP not explicitly redirected to Squid or allowed. Ports 5984, 9200, etc. would still be dropped. The inconsistency is a defense-in-depth gap (Squid and iptables don't give identical explicit denials) but does NOT represent an actual bypass.
Good Security Controls (evidence)
ReDoS protection:
// domain-patterns.ts:77constDOMAIN_CHAR_PATTERN='[a-zA-Z0-9.-]*';// Not .*
Execa injection prevention:
# Only 2 eslint-disable instances in 4000+ lines of TS
$ grep -rn "no-unsafe-execa" src/ --include="*.ts"
src/logs/log-discovery.ts:157 # container name filter
src/ssl-bump.ts:201 # OpenSSL cert subject line
One-shot token protection:
# entrypoint.sh:634-656export LD_PRELOAD=/usr/local/lib/one-shot-token.so
capsh --drop=$CAPS_TO_DROP -- -c "exec gosu awfuser ..."&
sleep 5 # grace period for token caching
unset_sensitive_tokens # clear from /proc/1/environ````**npm audit:**````
Total vulnerabilities: {'info': 0, 'low': 0, 'moderate': 0, 'high': 0, 'critical': 0, 'total': 0}
✅ Recommendations
Medium Priority
M1 — Replace permissive seccomp profile with Docker's built-in default
The custom seccomp-profile.json has defaultAction: SCMP_ACT_ALLOW (allow-by-default). Consider switching to Docker's built-in default seccomp profile (seccomp:docker-default) which uses deny-by-default and only adds specific blocks needed. If procfs mounting requires exceptions, add those specific syscalls to an allowlist-based profile rather than switching to a blocklist approach. File: containers/agent/seccomp-profile.json, src/docker-manager.ts:885.
M2 — Document or scope AppArmor unconfined
If apparmor:unconfined is only needed for the setup phase (mounting procfs), investigate whether a custom AppArmor profile can be created that allows the specific mount operations needed, then re-enables confinement. Alternatively, explicitly document in the security architecture that AppArmor is intentionally bypassed and what mitigations replace it (capability drops, seccomp, no-new-privileges). File: src/docker-manager.ts:886.
Low Priority
L1 — Add newline sanitization to domain and URL validation
Add an explicit check for control characters (including \n, \r, \t) in validateDomainOrPattern() (domain-patterns.ts:139) and in the URL pattern validation in cli.ts:1004. A one-line fix:
if(/[\n\r\t\x00-\x1f]/.test(trimmed)){thrownewError(`Domain '\$\{trimmed}' contains invalid control characters`);}
This closes the theoretical Squid config injection vector, particularly relevant for the --allow-domains-file code path.
L2 — Synchronize dangerous ports lists
Add the 6 ports present in squid-config.ts but missing from setup-iptables.sh to the iptables DANGEROUS_PORTS array: 5984, 6984, 8086, 8088, 9200, 9300. This ensures defense-in-depth is consistent at both enforcement layers.
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-03-07 | Reviewer: Security Review Agent | Version: 0.23.1
This review covers a deep analysis of the
@github/agentic-workflow-firewallcodebase, examining ~4,104 lines of security-critical code across the network filtering stack (host-iptables, setup-iptables, Squid config, container entrypoint, and Docker orchestration). The system's overall security posture is good, with multiple defense-in-depth layers. Three actionable findings are documented below.🔍 Findings from Firewall Escape Test
The
security-reviewworkflow was the most recent in-scope agentic workflow; no prior escape-test run logs were accessible viaagenticworkflows-logs(download error). Complementary findings from previous security guards are reflected where patterns overlap.🛡️ Architecture Security Analysis
Network Security Assessment — Strong
The traffic interception chain is well-designed:
src/host-iptables.ts) creates a customFW_WRAPPERchain that filters all egress from the bridge interface. Rules are ordered correctly: Squid exempt → ESTABLISHED/RELATED → localhost → trusted DNS → Squid port → API proxy port → multicast/link-local block → UDP block → default deny.containers/agent/setup-iptables.sh) redirects HTTP/HTTPS to Squid and terminates all other TCP with a final DROP rule.src/squid-config.ts) enforces domain whitelisting and blocks dangerous ports via!Safe_ports.ip6tablesis set up when available; when unavailable,sysctl net.ipv6.conf.all.disable_ipv6=1prevents unfiltered IPv6 bypass.Evidence:
Container Security Assessment — Good with Notes
NET_ADMINis added to the agent container for iptables setup and then dropped viacapshbefore user code executes (entrypoint.sh:649,cap_drop: ['NET_RAW','SYS_PTRACE','SYS_MODULE','SYS_RAWIO','MKNOD']indocker-manager.ts:872-878).no-new-privileges:trueprevents re-acquisition of dropped privileges.mem_limit: 4g,pids_limit: 1000.LD_PRELOAD=/usr/local/lib/one-shot-token.so) protects secrets from/proc/self/environre-reads after agent startup.unset_sensitive_tokens) runs after a 5-second grace period for agent initialization.Domain Validation Assessment — Strong
[a-zA-Z0-9.-]*(not.*) indomain-patterns.ts:77.*,*.*,*.*.com) are rejected.isDomainMatchedByPattern:279).Input Validation / Command Injection Assessment — Strong
eslint-rules/no-unsafe-execa.js) statically enforces thatexeca()is only called with literal string commands — no template literals with expressions or string concatenation. Only 2 justifiedeslint-disableexceptions exist (log-discovery.ts:157for container name filter,ssl-bump.ts:201for OpenSSL cert subject — both use carefully-controlled variables, not user input).execa, not through a shell.seccomp-profile.json+docker-manager.ts:885docker-manager.ts:886domain-patterns.ts:139-185squid-config.ts:10-31vssetup-iptables.sh:203-221setup-iptables.sh:156-200containers/agent/docker-stub.shdocker-manager.ts:888-892[FW_DNS_QUERY]) but iptables logs go to kernel dmesg onlyhost-iptables.ts:310-316🎯 Attack Surface Map
--allow-domains)src/cli.ts:870-878validateDomainOrPattern()rejects broad patterns; no newline check--allow-urls)src/cli.ts:1004-1038https://prefix, rejects broad patterns; no newline checksrc/squid-config.ts:119,247,256[^0-9-]/g)src/docker-manager.ts:896$$) for YAML; passes throughbash -cas intendedsrc/host-iptables.tscontainers/agent/setup-iptables.shis_valid_ipv4()validates resolved IPs before usesetup-iptables.sh:43-48entrypoint.sh:649capsh --drop=cap_net_adminbefore user code;no-new-privileges:trueentrypoint.sh:634, 647-656unset_sensitive_tokensafter 5s grace periodcontainers/agent/get-claude-key.shcontainers/agent/seccomp-profile.json📋 Evidence Collection
Finding T1: Permissive Seccomp Profile
Command run:
Docker's built-in default seccomp profile uses
SCMP_ACT_ERRNOas its default (deny-by-default allowlist). The AWF custom profile usesSCMP_ACT_ALLOW(allow-by-default blocklist). Syscalls not explicitly blocked includemount,unshare, andclonewithCLONE_NEWUSER. These are mitigated by capability drops (cap_drop: CAP_SYS_ADMINvia capsh before user code) but the profile itself provides weaker guarantees.Applied in
docker-manager.ts:885:Finding T2: AppArmor Disabled
The comment explains the rationale (procfs mounting). However,
apparmor:unconfinedremoves MAC-level process confinement for the entire container lifecycle, not just during setup. Once capsh dropsSYS_ADMIN, mount calls from user code would fail at the capability check — but AppArmor would no longer provide an independent audit trail or secondary enforcement for other operations (file access, network access from processes, etc.).Finding T3: Domain Names Not Checked for Newlines (Squid Config Injection)
The
validateDomainOrPattern()function indomain-patterns.ts:139-185validates:*,*.*,[*.]+(too broad)..(double dots).alone,*.,.*But it does not check for newline characters (
\n,\r). Domains are then embedded directly into the Squid configuration:A domain like
github.com\nhttp_access allow allwould be parsed bycommanderas a single argument containing a literal newline (if passed via a config file or programmatic API), and when written tosquid.conf, the second line would becomehttp_access allow all, allowing unrestricted access.Mitigating factors: Shell argument parsing strips/escapes newlines in practice when domains are passed via CLI. The
parseDomains()function splits on commas but not newlines. Risk is very low in normal CLI usage but theoretically present if the AWF library is used programmatically or if the allow-domains-file feature is used with malicious input.Finding T4: Dangerous Ports List Inconsistency
src/squid-config.tsDANGEROUS_PORTS (21 entries) includes ports not insetup-iptables.sh(15 entries):However, in the container's OUTPUT filter chain, the final rule is:
This catches all TCP not explicitly redirected to Squid or allowed. Ports 5984, 9200, etc. would still be dropped. The inconsistency is a defense-in-depth gap (Squid and iptables don't give identical explicit denials) but does NOT represent an actual bypass.
Good Security Controls (evidence)
ReDoS protection:
Execa injection prevention:
One-shot token protection:
✅ Recommendations
Medium Priority
M1 — Replace permissive seccomp profile with Docker's built-in default
The custom
seccomp-profile.jsonhasdefaultAction: SCMP_ACT_ALLOW(allow-by-default). Consider switching to Docker's built-in default seccomp profile (seccomp:docker-default) which uses deny-by-default and only adds specific blocks needed. If procfs mounting requires exceptions, add those specific syscalls to an allowlist-based profile rather than switching to a blocklist approach. File:containers/agent/seccomp-profile.json,src/docker-manager.ts:885.M2 — Document or scope AppArmor unconfined
If
apparmor:unconfinedis only needed for the setup phase (mounting procfs), investigate whether a custom AppArmor profile can be created that allows the specificmountoperations needed, then re-enables confinement. Alternatively, explicitly document in the security architecture that AppArmor is intentionally bypassed and what mitigations replace it (capability drops, seccomp,no-new-privileges). File:src/docker-manager.ts:886.Low Priority
L1 — Add newline sanitization to domain and URL validation
Add an explicit check for control characters (including
\n,\r,\t) invalidateDomainOrPattern()(domain-patterns.ts:139) and in the URL pattern validation incli.ts:1004. A one-line fix:This closes the theoretical Squid config injection vector, particularly relevant for the
--allow-domains-filecode path.L2 — Synchronize dangerous ports lists
Add the 6 ports present in
squid-config.tsbut missing fromsetup-iptables.shto the iptables DANGEROUS_PORTS array: 5984, 6984, 8086, 8088, 9200, 9300. This ensures defense-in-depth is consistent at both enforcement layers.📈 Security Metrics
Beta Was this translation helpful? Give feedback.
All reactions