[Security Review] 🔒 Daily Security Review — 2026-09-15 #8594
Closed
Replies: 2 comments
|
🔮 The ancient spirits stir: the smoke test agent walked this thread, read the signs, and found the path clear. The oracle departs with passing omens. Warning Firewall blocked 7 domainsThe following domains were blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "msfeed25.pkgs.visualstudio.com"
- "www.google.com"
- "www.gstatic.com"See Network Configuration for more information.
|
0 replies
|
This discussion was automatically closed because it expired on 2026-09-22T12:42:16.060Z.
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
📊 Executive Summary
This review covers
github/gh-aw-firewall(awf), a CLI that wraps agent commands inside a Docker network with L7 (Squid) egress filtering, host-level iptables enforcement, container capability drops, and (optionally) an API-key-injecting sidecar and unified enclaves subsystem. The analysis is based on direct reading of the network layer (src/host-iptables-*.ts), container NAT/init logic (containers/agent/setup-iptables.sh), the container entrypoint (containers/agent/entrypoint.sh), the domain allowlist parser/validator (src/domain-patterns.ts,src/domain-validation.ts,src/domain-matchers.ts,src/domain-utils.ts), Squid config generation (src/squid/*.ts), capability-drop probing (src/capability-filter.ts), CLI/process-execution code paths (src/cli.ts,src/container-lifecycle.ts,src/host-iptables-network.ts), and the enclave dynamic-delegation channel (src/enclave/dynamic-delegation-channel.ts).Overall posture: strong, defense-in-depth design. No command-injection or shell-metacharacter vulnerabilities were found — all process execution uses
execa/array-argv, nevershell: trueor string-interpolatedsh -c. Domain-matching logic uses anchored, ReDoS-safe character-class regexes and an explicit dangerous-character blocklist before any value is interpolated intosquid.conf. Capability drops (SYS_CHROOT,SYS_ADMIN), UID 0 rejection, and IPv6 kill-switches are implemented correctly and fail closed where credible. The most notable residual risks are (1) the accepted, already-tracked capability-auth-only enclave control channel (github/gh-aw#59268, closed as not planned), (2) reliance on a--privilegedDocker probe container incapability-filter.tsfor capability introspection, and (3) the general class of host-access/gateway bypass rules (--allow-host-*) that intentionally punch holes in the egress model for CI convenience and depend on correct IP resolution to stay narrow.No firewall/network escape was demonstrated in the referenced escape test; see below.
🔍 Findings from Firewall Escape Test
Run:
Secret Digger (Copilot), https://github.com/github/gh-aw-firewall/actions/runs/29286879560 (tracked in issue #6205).threat_detected. This is the expected/correct behavior — a prompt-injection payload was genuinely present in the task, so flagging it is a true positive on the detection system, not evidence of an exploited weakness inawf.awfwas exercised or defeated by this test; the control that succeeded here was the LLM agent's own alignment/refusal behavior, layered on top of (not in place of) the network controls reviewed below.awf's firewall itself, follow-up tests should attempt to have the agent comply with (or be forced into) direct network actions — e.g., raw socket connections, DNS exfiltration to non-allowlisted resolvers,CONNECTto non-allowlisted hosts, or attempts to regainSYS_ADMIN/SYS_CHROOT— rather than relying on the agent to self-refuse a prompt-injection instruction.🛡️ Architecture Security Analysis
Network
DOCKER-USER:src/host-iptables-rules.ts:230-266(setupHostIptables) builds a dedicatedFW_WRAPPERchain, ordered as: allow-from-Squid (addProxySourceAcceptRules, lines 39-57) → conntrack ESTABLISHED/RELATED + loopback (addConnectionTrackingRules, lines 59-79) → DNS allow-listing incl. dedicated IPv6 chain (addIpv6DnsRules, lines 81-125) → sidecar/gateway allow rules (addProxyDestinationAcceptRules, lines 127-260) → multicast/link-local/UDP block + default-deny-with-LOG (addBlockRules, lines 262-303) → jump-rule insertion intoDOCKER-USER(insertDockerUserJumpRule, called at line 331). Because the jump is inserted into Docker's ownDOCKER-USERchain, all containers on theawf-netnetwork are subject to this filter, not just the agent container — a good defense-in-depth choice that survives a compromised container trying to add its own network alias.addBlockRules(host-iptables-rules.ts:262-303) ends with an unconditionalREJECTafter logging with[FW_BLOCKED_OTHER]/[FW_BLOCKED_UDP]prefixes — a fail-closed design; unmatched traffic (including UDP other than allow-listed DNS) is rejected, not silently dropped, and is logged for forensics.addIpv6DnsRules(host-iptables-rules.ts:87-91) detectsip6tablesunavailability and callsdisableIpv6ViaSysctl()— closing what would otherwise be an IPv6 iptables bypass.containers/agent/setup-iptables.sh:139-148(disable_ipv6) independently disables IPv6 inside the agent's network namespace via sysctl, with an explicit code comment citing the historical bypass issue (github/gh-aw-firewall#1543). Both host and container layers close this gap.127.0.0.11) and explicitly configured upstream DNS servers get NAT/RETURN + filter ACCEPT rules (containers/agent/setup-iptables.sh:257-281,configure_dns_nat_rules); all other DNS traffic falls through to the default REJECT. This prevents DNS tunneling/exfiltration to attacker-controlled resolvers.containers/agent/setup-iptables.sh:113-127definesDANGEROUS_PORTS(SSH 22, SMTP 25, DB ports, Redis, MongoDB, RDP, etc.) andconfigure_host_access_rules(lines ~395-403) adds NATRETURNrules for each so they fall through to the filter chain's default DROP — defense-in-depth alongside Squid's own ACLs, since these ports would never route through Squid's HTTP/HTTPS ACL anyway.--allow-host-ports,--allow-host-service-ports, and--enable-host-access(containers/agent/setup-iptables.sh:308-395,allow_host_access_to_gateway,configure_host_access_rules) create NATRETURN+ filterACCEPTrules tohost.docker.internal/ the network default gateway. These are pre-validated by TypeScript (src/host-iptables-validation.ts→AWF_VALID_ALLOW_HOST_PORTS/AWF_VALID_HOST_SERVICE_PORTS) and re-validated in bash (is_valid_port_spec, setup-iptables.sh:29-42) as defense-in-depth, and are scoped to a resolved gateway IP rather than being wide-open — this is a reasonable design for CI service-container use cases (Playwright, MCP, GH Actions services), but it is inherently an opt-in relaxation of the egress model and should be treated as elevated-risk configuration by operators.Container
/usr,/bin,/sbin,/lib,/lib64,/opt,/sys,/devread-only; workspace//tmpread-write; whitelisted$HOMEsubdirs; selected/etcfiles excluding/etc/shadow) are mounted under/host. This substantially reduces the blast radius of a compromised agent process compared to a full host mount.containers/agent/entrypoint.sh:451-465(determine_capabilities_to_drop) dropscap_sys_chroot,cap_sys_adminonly when chroot mode is enabled, and explicitly notesNET_ADMINis never granted to the agent container (iptables setup is delegated to a separate short-livedawf-iptables-initcontainer that shares the network namespace but not the agent's long-lived process). This separation-of-privilege design means even a full agent-container compromise cannot reconfigure firewall rules, because the capability required to do so was never present in that container.containers/agent/entrypoint.sh:27-53(setup_user_identity) validatesAWF_USER_UID/AWF_USER_GIDare numeric and explicitly rejects0(root) for either value — preventing a caller from defeating the privilege drop by settingAWF_USER_UID=0.hidepid=2: per CLAUDE.md and referenced in entrypoint chroot setup, a container-scoped procfs is mounted withhidepid=2, preventing the agent from reading other processes'/proc/[pid]/environ— relevant credential-isolation control given the API-proxy design intentionally keeps real API keys out of the agent's own environment.--privilegedprobe container risk:src/capability-filter.ts:78-90(getHostCapabilityBoundingSet) runsdocker run --rm --pull=never --privileged --network=none --entrypoint /bin/sh <probeImage> -c 'cat /proc/self/status'purely to introspect the Docker daemon's capability bounding set. This is a deliberate, narrowly-scoped, network-isolated, short-lived container (not the agent), and its sole purpose is read-only introspection of/proc/self/status. Risk is low but non-zero: it depends on the local Docker daemon honoring--privileged/--network=nonecorrectly, and a compromised/maliciousprobeImage(alpine:latestby default, or the first service image found infilterComposeCapDrop) run with--privilegedcould otherwise be a container-escape vector if image provenance were ever attacker-influenced. Today the probe image is a fixed, trusted value or a compose service image already trusted by the operator, so this is best framed as an accepted architectural trade-off rather than an active vulnerability.src/capability-filter.ts:99-125(filterCapDrop) returns the originalcapDropListunmodified whencapBnd === null(i.e., when the bounding set could not be determined) — this is fail-closed for the drop list (nothing is silently removed), but noteisCapDropSkipped()(lines 55-59) allowsAWF_SKIP_CAP_DROP=1/true/yesto bypass all capability dropping entirely; this is an intentional operator escape hatch (e.g. for constrained CI runners) and should be documented as a security-sensitive environment variable if not already.Domain Validation
src/domain-validation.ts:27definesSQUID_DANGEROUS_CHARS = /[\s\0"';#]/andcheckDangerousChars(lines 33-46) additionally rejects backslashes for domain names specifically (regex patterns via--allow-urlsare allowed backslashes for legitimate escaping, per the code comment at lines 16-19).src/squid/domain-acl.ts:26-34(assertSafeForSquidConfig) re-checks the same character class immediately before interpolating any value intosquid.conf, giving two independent checkpoints (input validation + interpolation-site assertion) — a solid defense-in-depth pattern against Squid config injection (whitespace-splitting ACL tokens,#`-comment truncation, quote/backtick/semicolon breakout).src/domain-patterns.ts:76uses a bounded character classDOMAIN_CHAR_PATTERN = '[a-zA-Z0-9.-]*'instead of.*for wildcard expansion inwildcardToRegex(), andsrc/domain-matchers.ts:82-86enforces a 512-character length cap before any regex test inisDomainMatchedByPattern()— both are appropriate, evidence-based ReDoS defenses (no catastrophic backtracking construct like nested quantifiers was found).dstdomainACL type (used bygenerateDomainAclsinsrc/squid/acl-generator.ts:9-16) combined withformatDomainForSquid()(src/squid/domain-acl.ts:36-39, which always prefixes a leading dot, e.g..github.com) matches on Squid's own domain-suffix semantics:.github.commatchesgithub.comand any subdomain, but notnotgithub.comorgithub.com.evil.com(Squid dstdomain does exact right-hand-suffix matching, not substring matching) — this correctly avoids the classic "suffix confusion" bypass (evilgithub.commatching a naive.includes('github.com')check). No such naive substring check was found anywhere in the domain-matching code path; matching is done exclusively via Squid's own ACL engine or via anchored (^...$) regexes inwildcardToRegex().src/domain-validation.ts:52-66(checkOverBroadPattern) explicitly rejects*,*.*, and any pattern composed solely of*/.characters, andcheckStructuralValidity(lines 72-91) rejects.., a lone., and patterns where wildcard segments constitute all-but-one of the domain's dot-segments (e.g.*.*.com) — this closes an entire class of "allowlist everything" misconfiguration, whether malicious or accidental.src/domain-matchers.ts:118-124defines a distinctHOST_CHAR_PATTERN = '[^\\s/]*'(excludes/) versusURL_CHAR_PATTERN = '[^\\s]*'for the path portion, andparseUrlPatterns()(lines 141-177) splits the pattern at the first/after the scheme before applying wildcard substitution — this prevents a hostname wildcard likeapi-*from being able to match across the host/path boundary (e.g. matchingapi-evil.comas the path of an allowed host), which would otherwise be a bypass vector for--allow-urls.Input Validation
grep -rn "shell: true\|exec(\" src/ containers/) across.ts/.sh/.jsreturned zero matches. Allexeca()calls found (86+ call sites acrosssrc/*.ts) use the array-argv form (execa('iptables', ['-t', 'filter', ...])), never string interpolation into a shell.containers/agent/entrypoint.sh:556executes the user's command via"$@"(bash array expansion), notevalorsh -c "$string"` — this is the correct pattern to avoid word-splitting/glob-expansion/injection from user-supplied command arguments.src/domain-utils.ts:82-92(SAFE_BASE_IMAGE_PATTERNS) restricts--agent-imagetoubuntu:XX.XX, pinned catthehacker runner images, and SHA256-digest-pinned variants;validateAgentImage()(lines 105-123) rejects anything else, andprocessAgentImageOption()(lines 141-183) additionally requires--build-localfor any non-preset image — this prevents supply-chain injection via an arbitrary attacker-controlled base image reference passed on the CLI.src/host-iptables-validation.ts(parseValidPortSpecs) validates on the TypeScript side before settingAWF_VALID_ALLOW_HOST_PORTS/AWF_VALID_HOST_SERVICE_PORTS;containers/agent/setup-iptables.sh:29-42(is_valid_port_spec) re-validates the same format in bash as a fail-closed defense-in-depth guard, with an explicit code comment noting shared fixtures (tests/port-spec-fixtures.json) keep both implementations in sync — a good practice for avoiding validation-logic drift between two languages.containers/api-proxy/, e.g.oidc-token-provider.js,key-validation.js,aws-sigv4.js), the sidecar injects real provider credentials and the agent calls it unauthenticated over the internal network;src/host-iptables-rules.tscomments (lines ~325-328, "Note: API proxy sidecar... does NOT get a firewall exemption. It routes through Squid via HTTP_PROXY/HTTPS_PROXY... ensuring domain whitelisting is enforced by Squid ACLs") confirm the sidecar's own egress is not exempted from domain ACL enforcement, closing a would-be bypass where a compromised sidecar could otherwise reach arbitrary hosts with live credentials.src/enclave/dynamic-delegation-channel.ts:10states explicitly "There is no network listener and no long-lived shared secret on this path"; the channel uses a0o700-mode directory (line 75-76,mkdirSync/chmodSync) and0o600-mode files (lines 62-63) for its file-based control channel rather than a network socket. This matches the CLAUDE.md description of a capability-auth-only control listener tracked asgithub/gh-aw#59268(closed as not planned) — i.e., a known, already-triaged, accepted-risk design point rather than an undiscovered vulnerability. No code changes are recommended here beyond what the linked issue already covers; it is called out for completeness and to avoid re-litigating a closed decision.--agent-imageused to run agent workload under a different base imagesrc/domain-utils.ts:82-123(SAFE_BASE_IMAGE_PATTERNS,validateAgentImage) — mitigated by allowlist +--build-localgatecontainers/agent/setup-iptables.sh:257-281(configure_dns_nat_rules),src/host-iptables-rules.ts:81-125(addIpv6DnsRules) — only allow-listed DNS servers reachable--allow-domains/--allow-urlsvalue (whitespace/quote/#/;breakout)src/domain-validation.ts:27-46(SQUID_DANGEROUS_CHARS,checkDangerousChars),src/squid/domain-acl.ts:26-34(assertSafeForSquidConfig) — dual-checkpoint validationSYS_CHROOT/SYS_ADMINpost-startup to escape chroot or remount host pathscontainers/agent/entrypoint.sh:451-465(determine_capabilities_to_drop) — dropped before user command runs, in chroot modeAWF_USER_UID=0to defeat privilege dropcontainers/agent/entrypoint.sh:46-53— explicit rejection of UID/GID 0src/host-iptables-rules.ts:262-303(addBlockRules,[FW_BLOCKED_UDP]/[FW_BLOCKED_OTHER]LOG rules), Squidfirewall_detailedlogformat (per LOGGING.md / squid-config.ts)/proc/[pid]/environhidepid=2mount (per CLAUDE.md, entrypoint chroot setup)notgithub.com/github.com.evil.comtreated as allowed)src/squid/acl-generator.ts:9-16+src/squid/domain-acl.ts:36-39(Squiddstdomainsuffix match with leading dot) — correct suffix semantics, no substring match foundsrc/domain-patterns.ts:76(boundedDOMAIN_CHAR_PATTERN),src/domain-matchers.ts:82-86(512-char length cap)--privilegedprobe container in capability introspection abused if image is attacker-controlledsrc/capability-filter.ts:78-90(getHostCapabilityBoundingSet) — network-isolated, fixed/trusted image, short-livedAWF_SKIP_CAP_DROP=1(or a compromised env inherits it), silently disabling all capability dropssrc/capability-filter.ts:55-59(isCapDropSkipped)--allow-host-ports/--enable-host-access) widen egress beyond the domain allowlistcontainers/agent/setup-iptables.sh:308-395(allow_host_access_to_gateway,configure_host_access_rules)src/enclave/dynamic-delegation-channel.ts:10,62-76— accepted risk, tracked ingithub/gh-aw#59268(closed as not planned)🎯 Attack Surface Map
--allow-domains/--allow-urlsCLI args →src/domain-validation.ts:96-112(validateDomainOrPattern)squid.conf→src/squid/acl-generator.ts,src/squid/config-generator.tsdstdomain/dstdom_regexsemantics prevent suffix-confusion bypassawfcode)-- <command>) →containers/agent/entrypoint.sh:556("$@"exec)sh -c/eval; runs as non-rootawfuserafter UID/GID validationawf's own code; residual risk is whatever the user command itself does inside its sandbox--network-subnet,--allow-host-ports,--allow-host-service-ports,--enable-host-access→src/host-iptables-validation.ts,containers/agent/setup-iptables.sh:308-403getent hosts host.docker.internal,route -n) which could mis-resolve on unusual network topologies--enable-api-proxysidecar (172.30.0.30, ports 10000-10003)HTTP_PROXY/HTTPS_PROXY(src/host-iptables-rules.tscomment ~325-328)src/capability-filter.ts:78-90--privilegedprobe container--network=none,--pull=never,--rm, short-lived, fixed probe image or already-trusted compose service imageprobeImagewould need scrutinysrc/enclave/dynamic-delegation-channel.ts)0o700directory /0o600files, no network listener, capability-token gatedgithub/gh-aw#59268AWF_SKIP_CAP_DROPenv var (src/capability-filter.ts:55-59)📋 Evidence Collection
Commands run and key greps
Key snippets found:
✅ Recommendations
Critical
High
AWF_SKIP_CAP_DROPprominently indocs/environment.mdas a security-sensitive variable, and consider emitting a loud runtime warning (banner, not just debug-level log) whenever it is honored, so operators cannot silently inherit it from a shared CI environment (src/capability-filter.ts:55-59).probeImageused bygetHostCapabilityBoundingSet()(src/capability-filter.ts:78-90) — e.g., require a digest-pinned reference or restrict it to a small allowlist analogous toSAFE_BASE_IMAGE_PATTERNS(src/domain-utils.ts:82-92) — to remove any future risk if this code path is ever extended to accept a user-influenced image.Medium
--allow-host-ports/--enable-host-accessgateway-IP resolution failure modes (e.g.,host.docker.internalresolving to an unexpected address) to confirm the fallback logic inconfigure_host_access_rules(containers/agent/setup-iptables.sh:308-395) always fails closed rather than silently widening the allowed surface.github/gh-aw#59268) once mcpg's dynamic-repository-admission work lands, since the risk profile may change as that surface becomes more heavily used.npm audit(and the equivalent forcontainers/api-proxy/package.json,containers/cli-proxy/package.json) from an unrestricted network context, since this review's environment itself blocked the advisory-feed request via its own outbound Squid proxy (403ERR_ACCESS_DENIED) and could not produce a dependency-vulnerability result.Low
dstdomainsuffix semantics against known confusable inputs (notgithub.com,github.com.evil.com,githubx.com) to lock in the subdomain-confusion protection as a regression guard, even though the current design (leading-dotdstdomainACLs) is already correct.--enable-host-accessor--allow-host-portsis used, summarizing the exact gateway IP(s)/ports opened for that run, to aid post-incident review (the underlyinglogger.infocalls already exist insrc/host-iptables-rules.ts; ensure they are also captured in the Squid/iptables log bundle surfaced to operators).📈 Security Metrics
src/host-iptables-*.ts,src/domain-*.ts,src/squid/*.ts, andcontainers/agent/*.sh(plus targeted reads ofsrc/capability-filter.ts,src/cli.ts,src/container-lifecycle.ts,src/enclave/dynamic-delegation-channel.ts).Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
msfeed25.pkgs.visualstudio.comTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.
All reactions