Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions containers/agent/setup-iptables.sh
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,22 @@ if [ -n "$AWF_API_PROXY_IP" ]; then
iptables -A OUTPUT -p tcp -d "$AWF_API_PROXY_IP" -j ACCEPT
fi

# Log dangerous port access attempts for audit (rate-limited to avoid log flooding)
# These ports are blocked by NAT RETURN + final DROP, but logging helps identify
# what the agent tried to access
echo "[iptables] Adding audit LOG rules for dangerous ports and default deny..."
# Build comma-separated list from the DANGEROUS_PORTS array to stay in sync
DANGEROUS_PORTS_LIST="$(IFS=,; echo "${DANGEROUS_PORTS[*]}")"
iptables -A OUTPUT -p tcp -m multiport --dports "$DANGEROUS_PORTS_LIST" \
-m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "[FW_BLOCKED_DANGEROUS_PORT] " --log-level 4 --log-uid

# Drop all other TCP and UDP traffic (default deny policy)
# TCP: ensures only explicitly allowed ports can be accessed
# UDP: prevents DNS exfiltration by blocking direct queries to non-configured DNS servers
echo "[iptables] Drop all non-allowed TCP and UDP traffic (default deny)..."
iptables -A OUTPUT -p tcp -m limit --limit 10/min --limit-burst 20 -j LOG --log-prefix "[FW_BLOCKED_TCP] " --log-level 4 --log-uid
iptables -A OUTPUT -p tcp -j DROP
iptables -A OUTPUT -p udp -m limit --limit 10/min --limit-burst 20 -j LOG --log-prefix "[FW_BLOCKED_UDP_AGENT] " --log-level 4 --log-uid
iptables -A OUTPUT -p udp -j DROP

echo "[iptables] NAT rules applied successfully"
Expand All @@ -328,3 +339,23 @@ if [ "$IP6TABLES_AVAILABLE" = true ]; then
else
echo "[iptables] (ip6tables NAT not available)"
fi

# Dump full iptables state for audit trail
# Written to the init signal volume so it can be preserved by the host
AUDIT_FILE="/tmp/awf-init/iptables-audit.txt"
echo "# iptables audit dump - $(date -u '+%Y-%m-%dT%H:%M:%SZ')" > "$AUDIT_FILE"
echo "" >> "$AUDIT_FILE"
echo "## IPv4 NAT rules" >> "$AUDIT_FILE"
iptables-save -t nat >> "$AUDIT_FILE" 2>/dev/null || echo "(iptables-save not available)" >> "$AUDIT_FILE"
echo "" >> "$AUDIT_FILE"
echo "## IPv4 filter rules" >> "$AUDIT_FILE"
iptables-save -t filter >> "$AUDIT_FILE" 2>/dev/null || echo "(iptables-save not available)" >> "$AUDIT_FILE"
if [ "$IP6TABLES_AVAILABLE" = true ]; then
echo "" >> "$AUDIT_FILE"
echo "## IPv6 NAT rules" >> "$AUDIT_FILE"
ip6tables-save -t nat >> "$AUDIT_FILE" 2>/dev/null || true
echo "" >> "$AUDIT_FILE"
echo "## IPv6 filter rules" >> "$AUDIT_FILE"
ip6tables-save -t filter >> "$AUDIT_FILE" 2>/dev/null || true
fi
echo "[iptables] Audit state dumped to $AUDIT_FILE"
28 changes: 28 additions & 0 deletions samples/audit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Audit Artifact Samples

These are **real** audit artifacts generated by running `awf` locally:

```bash
sudo awf --allow-domains github.com,api.github.com \
--audit-dir /tmp/audit-sample \
--build-local \
-- bash -c 'curl -s https://api.github.com/zen; curl -s https://evil.example.com || true; sleep 2'
```

## Files

| File | Description |
|------|-------------|
| `policy-manifest.json` | Structured description of all firewall rules with evaluation order |
| `access.log` | Squid access log in the `firewall_detailed` text format |
| `audit.jsonl` | Squid access log in structured JSONL format (machine-readable) |
| `squid.conf` | Generated Squid proxy configuration snapshot |
| `docker-compose.redacted.yml` | Container orchestration config with secrets replaced by `[REDACTED]` |

## What to look for

- In `access.log`: `TCP_TUNNEL:HIER_DIRECT` = allowed, `TCP_DENIED:HIER_NONE` = blocked
- In `audit.jsonl`: Same data in JSON format, one object per line
- In `policy-manifest.json`: Rules evaluated top-to-bottom; `deny-unsafe-ports` and `deny-raw-ipv4` come before domain rules
- In `squid.conf`: The actual ACL rules and log format directives
- In `docker-compose.redacted.yml`: Note `AWF_SQUID_CONFIG_B64: '[REDACTED]'` — secrets are stripped
3 changes: 3 additions & 0 deletions samples/audit/access.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
1774290908.910 172.30.0.20:55872 api.github.com:443 140.82.116.5:443 1.1 CONNECT 200 TCP_TUNNEL:HIER_DIRECT api.github.com:443 "curl/7.81.0"
1774290909.180 172.30.0.20:55880 api.github.com:443 140.82.116.5:443 1.1 CONNECT 200 TCP_TUNNEL:HIER_DIRECT api.github.com:443 "curl/7.81.0"
1774290909.186 172.30.0.20:55890 evil.example.com:443 -:- 1.1 CONNECT 403 TCP_DENIED:HIER_NONE evil.example.com:443 "curl/7.81.0"
3 changes: 3 additions & 0 deletions samples/audit/audit.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"ts":1774290908.910,"client":"172.30.0.20","host":"api.github.com:443","dest":"140.82.116.5:443","method":"CONNECT","status":200,"decision":"TCP_TUNNEL","url":"api.github.com:443"}
{"ts":1774290909.180,"client":"172.30.0.20","host":"api.github.com:443","dest":"140.82.116.5:443","method":"CONNECT","status":200,"decision":"TCP_TUNNEL","url":"api.github.com:443"}
{"ts":1774290909.186,"client":"172.30.0.20","host":"evil.example.com:443","dest":"-:-","method":"CONNECT","status":403,"decision":"TCP_DENIED","url":"evil.example.com:443"}
103 changes: 103 additions & 0 deletions samples/audit/docker-compose.redacted.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
services:
squid-proxy:
container_name: awf-squid
networks:
awf-net:
ipv4_address: 172.30.0.10
volumes:
- /tmp/awf-1774290893689/squid-logs:/var/log/squid:rw
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "3128"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
ports:
- '3128:3128'
cap_drop:
- NET_RAW
- SYS_ADMIN
- SYS_PTRACE
- SYS_MODULE
- MKNOD
- AUDIT_WRITE
- SETFCAP
stop_grace_period: 2s
environment:
AWF_SQUID_CONFIG_B64: '[REDACTED]'
entrypoint:
- /bin/bash
- '-c'
- echo "$$AWF_SQUID_CONFIG_B64" | base64 -d > /etc/squid/squid.conf && exec /usr/local/bin/entrypoint.sh
build:
context: ./containers/squid
dockerfile: Dockerfile
agent:
container_name: awf-agent
networks:
awf-net:
ipv4_address: 172.30.0.20
dns: [8.8.8.8, 8.8.4.4]
dns_search: []
volumes:
- /tmp:/tmp:rw
- $WORKSPACE:$WORKSPACE:rw
- /tmp/awf-TIMESTAMP/agent-logs:$HOME/.copilot/logs:rw
- /tmp/awf-TIMESTAMP/init-signal:/tmp/awf-init:rw
- /usr:/host/usr:ro
- /bin:/host/bin:ro
- /sbin:/host/sbin:ro
- /lib:/host/lib:ro
- /lib64:/host/lib64:ro
# ... (selective bind mounts for system binaries, home dirs, etc.)
environment:
HTTP_PROXY: http://172.30.0.10:3128
HTTPS_PROXY: http://172.30.0.10:3128
https_proxy: http://172.30.0.10:3128
SQUID_PROXY_HOST: squid-proxy
SQUID_PROXY_PORT: '3128'
HOME: /home/runner
NO_COLOR: '1'
AWF_ONE_SHOT_TOKENS: COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,GH_TOKEN,...
AWF_DNS_SERVERS: 8.8.8.8,8.8.4.4
AWF_CHROOT_ENABLED: 'true'
AWF_WORKDIR: /home/runner
AWF_USER_UID: '1000'
AWF_USER_GID: '1000'
depends_on:
squid-proxy:
condition: service_healthy
cap_add: [SYS_CHROOT, SYS_ADMIN]
cap_drop: [NET_RAW, SYS_PTRACE, SYS_MODULE, SYS_RAWIO, MKNOD]
security_opt:
- no-new-privileges:true
- seccomp=/tmp/awf-TIMESTAMP/seccomp-profile.json
- apparmor:unconfined
mem_limit: 6g
pids_limit: 1000
tty: false
command:
- /bin/bash
- '-c'
- 'curl -s https://api.github.com/zen; curl -s https://evil.example.com || true; sleep 2'
iptables-init:
container_name: awf-iptables-init
network_mode: service:agent
volumes:
- /tmp/awf-TIMESTAMP/init-signal:/tmp/awf-init:rw
environment:
SQUID_PROXY_HOST: 172.30.0.10
SQUID_PROXY_PORT: '3128'
AWF_DNS_SERVERS: 8.8.8.8,8.8.4.4
depends_on:
agent:
condition: service_healthy
cap_add: [NET_ADMIN, NET_RAW]
cap_drop: [ALL]
entrypoint: [/bin/bash]
command: ['-c', '/usr/local/bin/setup-iptables.sh > /tmp/awf-init/output.log 2>&1 && touch /tmp/awf-init/ready']
mem_limit: 128m
restart: 'no'
networks:
awf-net:
external: true
76 changes: 76 additions & 0 deletions samples/audit/policy-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"version": 1,
"generatedAt": "2026-03-23T18:34:53.894Z",
"rules": [
{
"id": "deny-unsafe-ports",
"order": 1,
"action": "deny",
"aclName": "!Safe_ports",
"protocol": "both",
"domains": [],
"description": "Deny requests to ports not in Safe_ports ACL (only 80, 443, and user-specified ports allowed)"
},
{
"id": "deny-connect-unsafe-ports",
"order": 2,
"action": "deny",
"aclName": "CONNECT !Safe_ports",
"protocol": "https",
"domains": [],
"description": "Deny CONNECT (HTTPS) to ports not in Safe_ports ACL"
},
{
"id": "deny-raw-ipv4",
"order": 3,
"action": "deny",
"aclName": "dst_ipv4",
"protocol": "both",
"domains": [
"^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$"
],
"description": "Deny requests to raw IPv4 addresses (bypasses domain filtering)"
},
{
"id": "deny-raw-ipv6",
"order": 4,
"action": "deny",
"aclName": "dst_ipv6",
"protocol": "both",
"domains": [
"^\\[?[0-9a-fA-F:]+\\]?$"
],
"description": "Deny requests to raw IPv6 addresses (bypasses domain filtering)"
},
{
"id": "allow-both-plain",
"order": 5,
"action": "allow",
"aclName": "allowed_domains",
"protocol": "both",
"domains": [
".github.qkg1.top"
],
"description": "Allow HTTP and HTTPS traffic to these domains"
},
{
"id": "deny-default",
"order": 6,
"action": "deny",
"aclName": "all",
"protocol": "both",
"domains": [],
"description": "Deny all traffic not matching any allow rule (default deny)"
}
],
"dangerousPorts": [
22, 23, 25, 110, 143, 445, 1433, 1521, 3306, 3389,
5432, 5984, 6379, 6984, 8086, 8088, 9200, 9300,
27017, 27018, 28017
],
"dnsServers": ["8.8.8.8", "8.8.4.4"],
"sslBumpEnabled": false,
"dlpEnabled": false,
"hostAccessEnabled": false,
"allowHostPorts": null
}
97 changes: 97 additions & 0 deletions samples/audit/squid.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Squid configuration for egress traffic control
# Generated by awf


# Disable pinger (ICMP) - requires NET_RAW capability which we don't have for security
pinger_enable off

# PID file location - use proxy-owned directory since container runs as non-root
pid_filename /var/run/squid/squid.pid

# Custom log format with detailed connection information
# Format: timestamp client_ip:port dest_domain dest_ip:port protocol method status decision url user_agent
# Note: For CONNECT requests (HTTPS), the domain is in the URL field
logformat firewall_detailed %ts.%03tu %>a:%>p %{Host}>h %<a:%<p %rv %rm %>Hs %Ss:%Sh %ru "%{User-Agent}>h"

# Structured JSONL audit log for machine-readable analysis
# Note: Squid logformat does not JSON-escape strings, so fields like User-Agent
# could break JSON parsing. We omit User-Agent to reduce breakage risk.
logformat audit_jsonl {"ts":%ts.%03tu,"client":"%>a","host":"%{Host}>h","dest":"%<a:%<p","method":"%rm","status":%>Hs,"decision":"%Ss","url":"%ru"}

# Access log and cache configuration
# Don't log healthcheck probes from localhost (using ACL filter on access_log)
acl healthcheck_localhost src 127.0.0.1 ::1
access_log /var/log/squid/access.log firewall_detailed !healthcheck_localhost
access_log /var/log/squid/audit.jsonl audit_jsonl !healthcheck_localhost
cache_log /var/log/squid/cache.log
cache deny all

# ACL definitions for allowed domains (HTTP and HTTPS)
acl allowed_domains dstdomain .github.qkg1.top

# Port configuration
http_port 3128


# Network ACLs
acl localnet src 10.0.0.0/8
acl localnet src 172.16.0.0/12
acl localnet src 192.168.0.0/16
acl localnet src fc00::/7
acl localnet src fe80::/10

# Port ACLs
acl SSL_ports port 443
acl Safe_ports port 80 # HTTP
acl Safe_ports port 443 # HTTPS
acl CONNECT method CONNECT

# Access rules
# Deny unsafe ports (only allow Safe_ports defined above)
http_access deny !Safe_ports
# Allow CONNECT to Safe_ports instead of just SSL_ports (443)
http_access deny CONNECT !Safe_ports

# Deny CONNECT to raw IP addresses (IPv4 and IPv6)
# Prevents bypassing domain-based filtering via direct IP connections
acl dst_ipv4 dstdom_regex ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$
acl dst_ipv6 dstdom_regex ^\[?[0-9a-fA-F:]+\]?$
http_access deny dst_ipv4
http_access deny dst_ipv6

# Deny requests to unknown domains (not in allow-list)
http_access deny !allowed_domains

# Allow from trusted sources (after domain filtering)
http_access allow localnet
http_access allow localhost

# Deny everything else
http_access deny all

# Disable caching
cache deny all

# DNS settings - Squid resolves all domains for HTTP/HTTPS traffic
dns_nameservers 8.8.8.8 8.8.4.4

# Forwarded headers
forwarded_for delete
via off

# Error page customization
error_directory /usr/share/squid/errors/en

# Memory and file descriptor limits
cache_mem 64 MB
maximum_object_size 0 KB

# Timeout settings for streaming/long-lived connections (AI inference APIs)
read_timeout 30 minutes
connect_timeout 30 seconds
request_timeout 2 minutes
persistent_request_timeout 2 minutes
pconn_timeout 2 minutes
client_lifetime 8 hours
half_closed_clients on
shutdown_lifetime 0 seconds
Loading
Loading