Skip to content

[Security Review] Daily Security Review and Threat Modeling β€” 2026-02-21Β #994

@github-actions

Description

@github-actions

πŸ“Š Executive Summary

Conducted a comprehensive evidence-based security review of the gh-aw-firewall codebase, analyzing 4,101 lines of security-critical code across 8 core files. The overall security posture is good β€” the architecture applies multiple overlapping controls (iptables at two layers, Squid ACLs, capability dropping, seccomp, one-shot token LD_PRELOAD, tmpfs overlays, /dev/null credential hiding). Three genuine security gaps were identified, the most significant being an IPv6 bypass path under the default configuration.

Category Status
Network egress filtering (IPv4) βœ… Strong
Container privilege dropping βœ… Strong
Credential file protection βœ… Strong (minor gap)
IPv6 egress filtering ⚠️ Gap
Squid DNS config ⚠️ Gap
UDP filtering (container-level) ⚠️ Minor gap
Seccomp profile ⚠️ Permissive
Dependency vulns ℹ️ 1 moderate (indirect)

πŸ” Findings from Firewall Escape Test

The security-review workflow logs were unavailable for download in this run, so this section draws on direct code analysis. The escape vectors analyzed below are the most likely candidates for an escape test agent to attempt.


πŸ›‘οΈ Architecture Security Analysis

Network Security Assessment

The firewall uses a two-layer egress filtering approach:

  1. Host-level (DOCKER-USER chain) β€” src/host-iptables.ts: Creates the custom FW_WRAPPER chain and inserts it into Docker's DOCKER-USER chain, filtered to traffic from the fw-bridge interface. Rules order: Squid allowed unrestricted β†’ established/related β†’ localhost β†’ DNS allowlist β†’ Squid port allow β†’ UDP block β†’ default deny.

  2. Container-level (NAT + OUTPUT chain) β€” containers/agent/setup-iptables.sh: Redirects TCP port 80/443 via DNAT to Squid, then drops all non-redirected TCP in the OUTPUT filter chain.

  3. Squid ACL layer β€” src/squid-config.ts: Domain allowlisting with dstdomain (plain domains) and dstdom_regex (wildcard patterns).

Rule ordering is correct β€” deny rules follow allow rules properly in both iptables chains and Squid ACLs. The Squid blocklist precedes the allowlist (lines ~290-310 in squid-config.ts).

DNS exfiltration is well-mitigated: DNS traffic restricted to trusted servers at both host-level (host-iptables.ts:305-329) and container-level (setup-iptables.sh:101-133). Docker's embedded DNS 127.0.0.11 is also allowed for service name resolution.

ICMP/raw sockets: NET_RAW capability is dropped from both Squid and agent containers (docker-manager.ts:285, 869). This prevents raw socket creation that could be used for ICMP tunneling. Host-level UDP blocking ([FW_BLOCKED_UDP] REJECT) catches non-DNS UDP.

Container Security Assessment

Capability model: Agent container receives NET_ADMIN, SYS_CHROOT, SYS_ADMIN at startup (required for iptables setup and procfs mount), then drops all three via capsh --drop=cap_net_admin,cap_sys_chroot,cap_sys_admin before user code runs (entrypoint.sh:~257). NET_RAW, SYS_PTRACE, SYS_MODULE, SYS_RAWIO, MKNOD are also explicitly cap_drop'd at the Docker level (docker-manager.ts:868-874).

Seccomp profile (containers/agent/seccomp-profile.json): Default action is SCMP_ACT_ALLOW. Only 3 deny rules blocking: ptrace/process_vm_readv/process_vm_writev, kernel load/reboot/module syscalls, and umount/umount2. This is permissive β€” many potentially dangerous syscalls like unshare, clone with namespace flags, mount are allowed. However, namespace escape via unshare requires CAP_SYS_ADMIN which is dropped.

no-new-privileges: Applied (security_opt: ['no-new-privileges:true']). Prevents privilege escalation via setuid binaries after container start.

AppArmor unconfined: apparmor:unconfined is set for the agent container (docker-manager.ts:880) to allow procfs mounting. This removes the AppArmor layer of defense. Mitigated by capability drop before user code.

Resource limits: 4GB memory limit, 1000 PID limit, default CPU shares (docker-manager.ts:887-889).

Tmpfs overlays: workDir is hidden behind tmpfs to prevent reading docker-compose.yml which contains environment variables in plaintext. MCP logs also hidden. (docker-manager.ts:824-853)

One-shot-token LD_PRELOAD: Custom C library caches sensitive tokens on first getenv() call and clears them from /proc/self/environ, preventing /proc inspection attacks.

Git credential hiding: .gitconfig, .git-credentials, etc. mounted from /dev/null to block credential access.

Domain Validation Assessment

src/domain-patterns.ts validates and sanitizes domain inputs:

  • Explicit checks for *, *.*, pure-wildcard patterns throw errors
  • Double dots rejected
  • Wildcard segments limited: >1 wildcard in TLD position rejected
  • ReDoS mitigated: [a-zA-Z0-9.-]* character class used instead of .* for wildcard expansion
  • Domain length cap (512 chars) before regex matching in isDomainMatchedByPattern()

Input Validation Assessment

  • Port validation in squid-config.ts: Integer parsing, range check (1-65535), DANGEROUS_PORTS blocklist checked before adding to Safe_ports ACL
  • Port sanitization: port.replace(/[^0-9-]/g, '') defense-in-depth after integer validation
  • AWF_USER_UID/GID validated as numeric + non-zero in entrypoint.sh:18-28
  • redact-secrets.ts: Minimal function redacting Authorization headers, env var patterns, GitHub tokens from log output

⚠️ Threat Model

# Threat (STRIDE) Attack Vector Evidence Likelihood Impact Severity
T1 Information Disclosure β€” IPv6 Bypass Agent sends TCP/UDP over IPv6 to bypass domain filtering host-iptables.ts:313 β€” IPv6 chain only created when ipv6DnsServers.length > 0; setup-iptables.sh β€” no ip6tables OUTPUT -p tcp -j DROP Medium (requires host IPv6) High Medium
T2 Information Disclosure β€” Squid DNS misconfiguration Squid uses hardcoded 8.8.8.8 8.8.4.4 regardless of --dns-servers; custom DNS blocks Google DNS at iptables squid-config.ts:551 β€” dns_nameservers 8.8.8.8 8.8.4.4 hardcoded; not dynamic from config.dnsServers Low (only when custom DNS) Medium Low
T3 Information Disclosure β€” GCP ADC credential gap ~/.config/gcloud/application_default_credentials.json not blocked docker-manager.ts:770,802 β€” only credentials.db blocked, not application_default_credentials.json Low Medium Low
T4 Information Disclosure β€” UDP passes container OUTPUT chain Non-DNS UDP from container is only blocked at host level, not container-level setup-iptables.sh:290 β€” only iptables -A OUTPUT -p tcp -j DROP, no UDP rule Low (host-level catches it) Low Low
T5 Elevation of Privilege β€” Permissive seccomp SCMP_ACT_ALLOW default; many syscalls allowed seccomp-profile.json β€” defaultAction ALLOW, only 3 deny rules Low (capabilities mitigate) Medium Low
T6 Tampering β€” AppArmor unconfined No AppArmor profile enforcement on agent container docker-manager.ts:880 β€” apparmor:unconfined Low (capability drop before user code) Medium Low
T7 Information Disclosure β€” ajv ReDoS via $data Indirect ajv dependency vulnerable to ReDoS when $data option used npm advisory GHSA-2g4f-4pwh-qvx6, npm audit output Low (not directly used) Low Low

🎯 Attack Surface Map

Entry Point File Protections Risk
--allow-domains input domain-patterns.ts:validate... Empty check, wildcard validation, ReDoS mitigation via char class Low
--allow-host-ports input squid-config.ts:318-360 Integer parse, 1-65535 check, DANGEROUS_PORTS blocklist Low
Agent command execution docker-manager.ts:895 Shell escape (replace(/\$/g, '$$$$')), runs as non-root awfuser Low
Container environment vars docker-manager.ts:341-448 EXCLUDED_ENV_VARS set, credential isolation for API proxy Low
IPv4 TCP egress setup-iptables.sh + squid-config.ts DNAT β†’ Squid β†’ domain ACL, OUTPUT chain TCP DROP Low
IPv6 TCP egress host-iptables.ts:313 No default-deny when IPv4-only DNS Medium
UDP egress host-iptables.ts:483-489 Host-level REJECT via FW_WRAPPER chain Low
DNS queries setup-iptables.sh:101-122 Allowlisted servers only via NAT RETURN Low
/proc inspection entrypoint.sh, one-shot-token lib Tokens unset from env via LD_PRELOAD after 5s sleep Low
Docker socket containers/agent/docker-stub.sh Replaced with stub that errors on exec Low
Credential files docker-manager.ts:757-803 /dev/null mounts + selective home mounting Low
Squid proxy containers/squid/ cap_drop, domain ACL, healthcheck Low

πŸ“‹ Evidence Collection

T1: IPv6 Bypass β€” Code Evidence
# host-iptables.ts:313 β€” setupIpv6Chain only called inside this block:
if (ipv6DnsServers.length > 0) {
  // ...
  await setupIpv6Chain(bridgeName);
  // ...IPv6 deny rules...
}
// With default --dns-servers 8.8.8.8,8.8.4.4: ipv6DnsServers = [] β†’ NO IPv6 chain setup

# setup-iptables.sh final OUTPUT rule:
iptables -A OUTPUT -p tcp -j DROP   # IPv4-only β€” no ip6tables equivalent
# No: ip6tables -A OUTPUT -p tcp -j DROP
````

With `ip6tablesAvailable=true` on a host with IPv6, an agent can send IPv6 TCP traffic (e.g., `curl -6 https://[2001:4860:4860::8888]/...`) that bypasses Squid domain filtering entirely.
</details>

<details>
<summary>T2: Squid DNS Hardcoded β€” Code Evidence</summary>

````
# squid-config.ts:551 (hardcoded, never uses config.dnsServers):
dns_nameservers 8.8.8.8 8.8.4.4

# types.ts:326 shows dnsServers field exists in SquidConfig but is never consumed:
dnsServers?: string[];

# Impact: user runs: awf --dns-servers 1.1.1.1,1.0.0.1 ...
# β†’ iptables blocks 8.8.8.8 (not in trusted list)
# β†’ Squid tries to query 8.8.8.8 β†’ blocked β†’ DNS resolution fails
# β†’ All proxied requests fail
````

</details>

<details>
<summary>T3: GCP ADC Credential Gap β€” Code Evidence</summary>

````
# docker-manager.ts:770 β€” BLOCKED:
`\$\{effectiveHome}/.config/gcloud/credentials.db`
# NOT BLOCKED:
# ~/.config/gcloud/application_default_credentials.json  ← OAuth2 tokens for gcloud CLI
# ~/.config/gcloud/legacy_credentials/*/adc.json         ← Legacy ADC tokens
# ~/.config/gcloud/access_tokens.db                      ← Access token cache
````

</details>

<details>
<summary>T4: UDP Container-Level Gap</summary>

````
# setup-iptables.sh:290 β€” final rule only drops TCP:
iptables -A OUTPUT -p tcp -j DROP
# No matching: ip6tables or iptables -A OUTPUT -p udp -j DROP

# MITIGATED at host level (host-iptables.ts:483-489):
'-p', 'udp', '-j', 'LOG', '--log-prefix', '[FW_BLOCKED_UDP] '
'-p', 'udp', '-j', 'REJECT', '--reject-with', 'icmp-port-unreachable'
# Defense-in-depth gap: relies only on host-level filtering for UDP
Seccomp Profile Summary
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {"action": "SCMP_ACT_ERRNO", "names": ["ptrace", "process_vm_readv", "process_vm_writev"]},
    {"action": "SCMP_ACT_ERRNO", "names": ["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", "ustat", "sysfs", "vhangup",
      "get_kernel_syms", "query_module", "create_module", "nfsservctl"]},
    {"action": "SCMP_ACT_ERRNO", "names": ["umount", "umount2"]}
  ]
}
````

`SCMP_ACT_ALLOW` default means all other syscalls are permitted. `unshare`, `clone`, `mount` (with namespace flags) are allowed β€” namespace escape requires `CAP_SYS_ADMIN` which is capability-dropped before user code.
</details>

<details>
<summary>npm audit output</summary>

````
Package: ajv (indirect dependency)
Severity: moderate
Advisory: GHSA-2g4f-4pwh-qvx6 β€” ReDoS when using $data option
Direct: false

βœ… Recommendations

πŸ”΄ Medium Priority

M1 β€” Establish IPv6 default-deny at both host and container levels

Even without IPv6 DNS servers configured, the host-level DOCKER-USER should have a default-deny IPv6 rule, and the container-level OUTPUT chain should drop non-DNS IPv6 TCP:

// host-iptables.ts: Always set up IPv6 chain (not only when IPv6 DNS configured)
// Add default-deny rules even when no IPv6 DNS servers specified
await setupIpv6Chain(bridgeName); // Always called
// Then in the chain: deny all IPv6 TCP/UDP unless it's to a trusted IPv6 DNS server
# setup-iptables.sh: Add default IPv6 TCP deny alongside IPv4
if [ "$IP6TABLES_AVAILABLE" = true ]; then
  ip6tables -A OUTPUT -p tcp -j DROP
  ip6tables -A OUTPUT -p udp -j DROP
fi

🟑 Low Priority

L1 β€” Fix Squid dns_nameservers to use configured DNS servers

squid-config.ts:551 hardcodes dns_nameservers 8.8.8.8 8.8.4.4. Squid should use the configured DNS servers to match what iptables allows. The SquidConfig type has dnsServers?: string[] (unused):

// squid-config.ts β€” replace hardcoded line:
// dns_nameservers 8.8.8.8 8.8.4.4
// with:
const dnsServersLine = config.dnsServers && config.dnsServers.length > 0
  ? `dns_nameservers \$\{config.dnsServers.join(' ')}`
  : 'dns_nameservers 8.8.8.8 8.8.4.4';

And pass dnsServers when calling generateSquidConfig() in docker-manager.ts.

L2 β€” Extend GCP credential blocking to ADC and access token files

Add to credentialFiles array in docker-manager.ts:

`\$\{effectiveHome}/.config/gcloud/application_default_credentials.json`,
`\$\{effectiveHome}/.config/gcloud/access_tokens.db`,
`\$\{effectiveHome}/.config/gcloud/legacy_credentials`, // directory, not file β€” needs special handling

Note: Blocking an entire directory requires a different approach (tmpfs overlay or not mounting ~/.config).

L3 β€” Add container-level UDP default-deny

setup-iptables.sh drops TCP but not UDP. While the host-level chain blocks UDP, adding defense-in-depth:

# setup-iptables.sh: After TCP DROP rule, add UDP DROP
iptables -A OUTPUT -p tcp -j DROP
# Add: block non-DNS UDP at container level too (defense-in-depth)
iptables -A OUTPUT -p udp -j DROP

L4 β€” Harden seccomp profile to allowlist rather than denylist

Consider switching from SCMP_ACT_ALLOW default with a denylist to a SCMP_ACT_ERRNO default with an explicit allowlist of needed syscalls. This is the CIS Docker Benchmark recommendation. Docker's default seccomp profile is a good starting point to derive an allowlist from.

L5 β€” Update ajv indirect dependency (minor)

npm audit fix  # or pin to a patched version

πŸ“ˆ Security Metrics

Metric Value
Security-critical code lines analyzed 4,101
Attack surfaces identified 14
STRIDE threats modeled 7
Critical findings 0
Medium findings 1 (T1: IPv6 bypass)
Low findings 6
False positives 0
Defense layers (network) 3 (host iptables + container iptables + Squid ACL)
Credential files explicitly protected 14 per docker-manager.ts:757-803

Note: This was intended to be a discussion, but discussions could not be created due to permissions issues. This issue was created as a fallback.

Tip: Discussion creation may fail if the specified category is not announcement-capable. Consider using the "Announcements" category or another announcement-capable category in your workflow configuration.

Generated by Daily Security Review and Threat Modeling

  • expires on Feb 28, 2026, 1:41 PM UTC

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions