diff --git a/README.md b/README.md index f829e31..a89b750 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ sudo awf --help - [Quick start](docs/quickstart.md) — install, verify, and run your first command - [Usage guide](docs/usage.md) — CLI flags, domain allowlists, Docker-in-Docker examples +- [SSL Bump](docs/ssl-bump.md) — HTTPS content inspection for URL path filtering - [Logging quick reference](docs/logging_quickref.md) and [Squid log filtering](docs/squid_log_filtering.md) — view and filter traffic - [Security model](docs/security.md) — what the firewall protects and how - [Architecture](docs/architecture.md) — how Squid, Docker, and iptables fit together diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index e397e92..c19c990 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -98,6 +98,19 @@ if [ -f /etc/resolv.conf ]; then echo "[entrypoint] DNS configured with Docker embedded DNS (127.0.0.11) and trusted servers: $DNS_SERVERS" fi +# Update CA certificates if SSL Bump is enabled +# The CA certificate is mounted at /usr/local/share/ca-certificates/awf-ca.crt +if [ "${AWF_SSL_BUMP_ENABLED}" = "true" ]; then + echo "[entrypoint] SSL Bump mode detected - updating CA certificates..." + if [ -f /usr/local/share/ca-certificates/awf-ca.crt ]; then + update-ca-certificates 2>/dev/null + echo "[entrypoint] CA certificates updated for SSL Bump" + echo "[entrypoint] ⚠️ WARNING: HTTPS traffic will be intercepted for URL inspection" + else + echo "[entrypoint][WARN] SSL Bump enabled but CA certificate not found" + fi +fi + # Setup Docker socket permissions if Docker socket is mounted # This allows MCP servers that run as Docker containers to work # Store DOCKER_GID once to avoid redundant stat calls diff --git a/containers/squid/Dockerfile b/containers/squid/Dockerfile index e4bb732..629fd60 100644 --- a/containers/squid/Dockerfile +++ b/containers/squid/Dockerfile @@ -1,15 +1,17 @@ FROM ubuntu/squid:latest -# Install additional tools for debugging and healthcheck +# Install additional tools for debugging, healthcheck, and SSL Bump RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ dnsutils \ net-tools \ - netcat-openbsd && \ + netcat-openbsd \ + openssl \ + squid-openssl && \ rm -rf /var/lib/apt/lists/* -# Create log directory +# Create log directory and SSL database directory RUN mkdir -p /var/log/squid && \ chown -R proxy:proxy /var/log/squid @@ -17,8 +19,9 @@ RUN mkdir -p /var/log/squid && \ COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh -# Expose Squid port +# Expose Squid port (3128 for HTTP, 3129 for HTTPS with SSL Bump) EXPOSE 3128 +EXPOSE 3129 # Use entrypoint to fix permissions before starting Squid ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/containers/squid/entrypoint.sh b/containers/squid/entrypoint.sh index e7316f1..d6d2fa5 100644 --- a/containers/squid/entrypoint.sh +++ b/containers/squid/entrypoint.sh @@ -6,5 +6,18 @@ set -e chown -R proxy:proxy /var/log/squid chmod -R 755 /var/log/squid +# Fix permissions on SSL certificate database if SSL Bump is enabled +# The database is initialized on the host side by awf, but the permissions +# need to be fixed for the proxy user inside the container. +if [ -d "/var/spool/squid_ssl_db" ]; then + echo "[squid-entrypoint] SSL Bump mode detected - fixing SSL database permissions..." + + # Fix ownership for Squid (runs as proxy user) + chown -R proxy:proxy /var/spool/squid_ssl_db + chmod -R 700 /var/spool/squid_ssl_db + + echo "[squid-entrypoint] SSL certificate database ready" +fi + # Start Squid exec squid -N -d 1 diff --git a/docs-site/src/content/docs/index.md b/docs-site/src/content/docs/index.md index 0ed7684..af489da 100644 --- a/docs-site/src/content/docs/index.md +++ b/docs-site/src/content/docs/index.md @@ -15,6 +15,7 @@ When AI agents like GitHub Copilot CLI run with access to tools and MCP servers, **Key Capabilities:** - **Domain Whitelisting**: Allow only specific domains (automatically includes subdomains) +- **URL Path Filtering**: Restrict access to specific URL paths with [SSL Bump](/gh-aw-firewall/reference/ssl-bump/) - **Docker-in-Docker Enforcement**: Spawned containers inherit firewall restrictions - **Host-Level Protection**: Uses iptables DOCKER-USER chain for defense-in-depth - **Zero Trust**: Block all traffic by default, allow only what you explicitly permit diff --git a/docs-site/src/content/docs/reference/cli-reference.md b/docs-site/src/content/docs/reference/cli-reference.md index c09bd84..6663038 100644 --- a/docs-site/src/content/docs/reference/cli-reference.md +++ b/docs-site/src/content/docs/reference/cli-reference.md @@ -23,6 +23,8 @@ awf [options] -- | `--allow-domains-file ` | string | — | Path to file containing allowed domains | | `--block-domains ` | string | — | Comma-separated list of blocked domains (takes precedence over allowed) | | `--block-domains-file ` | string | — | Path to file containing blocked domains | +| `--ssl-bump` | flag | `false` | Enable SSL Bump for HTTPS content inspection | +| `--allow-urls ` | string | — | Comma-separated list of allowed URL patterns (requires `--ssl-bump`) | | `--log-level ` | string | `info` | Logging verbosity: `debug`, `info`, `warn`, `error` | | `--keep-containers` | flag | `false` | Keep containers running after command exits | | `--tty` | flag | `false` | Allocate pseudo-TTY for interactive tools | @@ -77,6 +79,47 @@ Path to file with blocked domains. Supports the same format as `--allow-domains- --block-domains-file ./blocked-domains.txt ``` +### `--ssl-bump` + +Enable SSL Bump for HTTPS content inspection. When enabled, the firewall generates a per-session CA certificate and intercepts HTTPS connections, allowing URL path filtering. + +```bash +--ssl-bump --allow-urls "https://github.com/githubnext/*" +``` + +:::caution[HTTPS Interception] +SSL Bump decrypts HTTPS traffic at the proxy. The proxy can see full URLs, headers, and request bodies. Applications with certificate pinning will fail to connect. +::: + +**How it works:** +1. A unique CA certificate is generated (valid for 1 day) +2. The CA is injected into the agent container's trust store +3. Squid intercepts HTTPS using SSL Bump (peek, stare, bump) +4. Full URLs become visible for filtering via `--allow-urls` + +**See also:** [SSL Bump Reference](/gh-aw-firewall/reference/ssl-bump/) for complete documentation. + +### `--allow-urls ` + +Comma-separated list of allowed URL patterns for HTTPS traffic. Requires `--ssl-bump`. + +```bash +# Single pattern +--allow-urls "https://github.com/githubnext/*" + +# Multiple patterns +--allow-urls "https://github.com/org1/*,https://api.github.com/repos/*" +``` + +**Pattern syntax:** +- Must include scheme (`https://`) +- `*` matches any characters in a path segment +- Patterns are matched against the full request URL + +:::note +Without `--ssl-bump`, the firewall can only see domain names (via SNI). Enable `--ssl-bump` to filter by URL path. +::: + ### `--log-level ` Set logging verbosity. @@ -234,6 +277,7 @@ Log sources are auto-discovered in this order: running containers, `AWF_LOGS_DIR ## See Also +- [SSL Bump Reference](/gh-aw-firewall/reference/ssl-bump/) - HTTPS content inspection and URL filtering - [Quick Start Guide](/gh-aw-firewall/quickstart) - Getting started with examples - [Usage Guide](/gh-aw-firewall/usage) - Detailed usage patterns and examples - [Troubleshooting](/gh-aw-firewall/troubleshooting) - Common issues and solutions diff --git a/docs-site/src/content/docs/reference/security-architecture.md b/docs-site/src/content/docs/reference/security-architecture.md index 339493f..2f42851 100644 --- a/docs-site/src/content/docs/reference/security-architecture.md +++ b/docs-site/src/content/docs/reference/security-architecture.md @@ -176,6 +176,63 @@ We considered isolating the agent in a network namespace with zero external conn mitmproxy would let us inspect HTTPS payloads, potentially catching exfiltration in POST bodies. But it requires injecting a CA certificate and breaks certificate pinning (common in security-sensitive clients). Squid's CONNECT method reads SNI without decryption—less powerful but zero client-side changes, and we maintain end-to-end encryption. +:::tip[SSL Bump for URL Filtering] +When you need URL path filtering (not just domain filtering), enable `--ssl-bump`. This uses Squid's SSL Bump feature with a per-session CA certificate, providing full URL visibility while maintaining security through short-lived, session-specific certificates. +::: + +--- + +## SSL Bump Security Model + +When `--ssl-bump` is enabled, the firewall intercepts HTTPS traffic for URL path filtering. This changes the security model significantly. + +### How SSL Bump Works + +1. **CA Generation**: A unique CA key pair is generated at session start +2. **Trust Store Injection**: The CA certificate is added to the agent container's trust store +3. **TLS Interception**: Squid terminates TLS and re-establishes encrypted connections to destinations +4. **URL Filtering**: Full request URLs (including paths) become visible for ACL matching + +### Security Safeguards + +| Safeguard | Description | +|-----------|-------------| +| **Per-session CA** | Each awf execution generates a unique CA certificate | +| **Short validity** | CA certificate valid for 1 day maximum | +| **Ephemeral key storage** | CA private key exists only in temp directory, deleted on cleanup | +| **Container-only trust** | CA injected only into agent container, not host system | + +### Trade-offs vs. SNI-Only Mode + +| Aspect | SNI-Only (Default) | SSL Bump | +|--------|-------------------|----------| +| Filtering granularity | Domain only | Full URL path | +| End-to-end encryption | ✓ Preserved | Modified (proxy-terminated) | +| Certificate pinning | Works | Broken | +| Proxy visibility | Domain:port | Full request (URL, headers) | +| Performance | Faster | Slight overhead | + +:::caution[When to Use SSL Bump] +Only enable SSL Bump when you specifically need URL path filtering. For most use cases, domain-based filtering provides sufficient control with stronger encryption guarantees. +::: + +### SSL Bump Threat Considerations + +**What SSL Bump enables:** +- Fine-grained access control (e.g., allow only `/githubnext/*` paths) +- Better audit logging with full URLs +- Detection of path-based exfiltration attempts + +**What SSL Bump exposes:** +- Full HTTP request/response content visible to proxy +- Applications with certificate pinning will fail +- Slightly increased attack surface (CA key compromise) + +**Mitigations:** +- CA key never leaves the temporary work directory +- Session isolation: each execution uses a fresh CA +- Automatic cleanup removes all key material + --- ## Failure Modes diff --git a/docs-site/src/content/docs/reference/ssl-bump.md b/docs-site/src/content/docs/reference/ssl-bump.md new file mode 100644 index 0000000..d735e5c --- /dev/null +++ b/docs-site/src/content/docs/reference/ssl-bump.md @@ -0,0 +1,243 @@ +--- +title: SSL Bump +description: Enable HTTPS content inspection for URL path filtering with per-session CA certificates. +--- + +:::note[Power-User Feature] +SSL Bump is an advanced feature that intercepts HTTPS traffic. It requires local Docker image builds and adds performance overhead. Only enable this when you need URL path filtering for HTTPS traffic. For most use cases, domain-based filtering (default mode) is sufficient. +::: + +SSL Bump enables deep inspection of HTTPS traffic, allowing URL path filtering instead of just domain-based filtering. + +## Overview + +By default, awf filters HTTPS traffic based on domain names using SNI (Server Name Indication). You can allow `github.com`, but cannot restrict access to specific paths like `https://github.com/githubnext/*`. + +With SSL Bump enabled, the firewall generates a per-session CA certificate and intercepts HTTPS connections, enabling: + +- **URL path filtering**: Restrict access to specific paths, not just domains +- **Full HTTP request inspection**: See complete URLs in logs +- **Wildcard URL patterns**: Use `*` wildcards in `--allow-urls` patterns + +:::caution[HTTPS Interception] +SSL Bump intercepts and decrypts HTTPS traffic. The proxy can see full request URLs and headers. Only use this when you understand the security implications. +::: + +## Quick Start + +```bash +# Enable SSL Bump for URL path filtering +sudo awf \ + --allow-domains github.com \ + --ssl-bump \ + --allow-urls "https://github.com/githubnext/*,https://api.github.com/repos/*" \ + -- curl https://github.com/githubnext/some-repo +``` + +## CLI Flags + +### `--ssl-bump` + +Enable SSL Bump for HTTPS content inspection. + +| Property | Value | +|----------|-------| +| Type | Flag (boolean) | +| Default | `false` | +| Requires | N/A | + +When enabled: +1. A per-session CA certificate is generated (valid for 1 day) +2. The CA is injected into the agent container's trust store +3. Squid intercepts HTTPS connections using SSL Bump +4. URL-based filtering becomes available via `--allow-urls` + +### `--allow-urls ` + +Comma-separated list of allowed URL patterns for HTTPS traffic. + +| Property | Value | +|----------|-------| +| Type | String (comma-separated) | +| Default | — | +| Requires | `--ssl-bump` flag | + +**Wildcard syntax:** +- `*` matches any characters within a path segment +- Patterns must include the full URL scheme (`https://`) + +```bash +# Allow specific repository paths +--allow-urls "https://github.com/githubnext/*" + +# Allow API endpoints +--allow-urls "https://api.github.com/repos/*,https://api.github.com/users/*" + +# Combine with domain allowlist +--allow-domains github.com --ssl-bump --allow-urls "https://github.com/githubnext/*" +``` + +## How It Works + +### Without SSL Bump (Default) + +``` +Agent → CONNECT github.com:443 → Squid checks domain ACL → Pass/Block + (SNI only, no path visibility) +``` + +Squid sees only the domain from the TLS ClientHello SNI extension. The URL path is encrypted and invisible. + +### With SSL Bump + +``` +Agent → CONNECT github.com:443 → Squid intercepts TLS + → Squid presents session CA certificate + → Agent trusts session CA (injected into trust store) + → Full HTTPS request visible: GET /githubnext/repo + → Squid checks URL pattern ACL → Pass/Block +``` + +Squid terminates the TLS connection and establishes a new encrypted connection to the destination. + +## Security Model + +### Per-Session CA Certificate + +Each awf execution generates a unique CA certificate: + +| Property | Value | +|----------|-------| +| Generation | Fresh key pair at session start | +| Validity | 1 day maximum | +| Storage | Temporary work directory only | +| Cleanup | Deleted when session ends | + +:::tip[Session Isolation] +Each awf execution uses a unique CA certificate. Old session certificates become useless after cleanup. +::: + +### Trust Store Modification + +- The session CA is injected only into the agent container's trust store +- Host system trust stores are not modified +- Spawned containers inherit the modified trust store + +### Traffic Visibility + +When SSL Bump is enabled: + +| What's Visible | To Whom | +|----------------|---------| +| Full URLs (including paths) | Squid proxy | +| HTTP headers | Squid proxy | +| Request/response bodies | Configurable (off by default) | + +:::danger[Security Consideration] +Full HTTP request/response content is visible to the proxy when SSL Bump is enabled. Ensure you understand this before enabling for sensitive workloads. +::: + +## Example Use Cases + +### Restrict GitHub to Specific Organizations + +```bash +sudo awf \ + --allow-domains github.com \ + --ssl-bump \ + --allow-urls "https://github.com/githubnext/*,https://github.com/github/*" \ + -- copilot --prompt "Clone the githubnext/copilot-workspace repo" +``` + +Allows access to `githubnext` and `github` organizations while blocking other repositories. + +### API Endpoint Restrictions + +```bash +sudo awf \ + --allow-domains api.github.com \ + --ssl-bump \ + --allow-urls "https://api.github.com/repos/githubnext/*,https://api.github.com/users/*" \ + -- curl https://api.github.com/repos/githubnext/gh-aw-firewall +``` + +### Debug with Verbose Logging + +```bash +sudo awf \ + --allow-domains github.com \ + --ssl-bump \ + --allow-urls "https://github.com/*" \ + --log-level debug \ + -- curl https://github.com/githubnext/gh-aw-firewall + +# View full URL paths in Squid logs +sudo cat /tmp/squid-logs-*/access.log +``` + +## Comparison: SNI-Only vs SSL Bump + +| Feature | SNI-Only (Default) | SSL Bump | +|---------|-------------------|----------| +| Domain filtering | ✓ | ✓ | +| Path filtering | ✗ | ✓ | +| End-to-end encryption | ✓ | Modified (proxy-terminated) | +| Certificate pinning | Works | Broken | +| Performance | Faster | Slight overhead | +| Log detail | Domain:port only | Full URLs | + +## Troubleshooting + +### Certificate Errors + +**Problem**: Agent reports certificate validation failures + +**Solutions**: +```bash +# Check if CA was injected +docker exec awf-agent ls -la /usr/local/share/ca-certificates/ + +# Verify trust store was updated +docker exec awf-agent cat /etc/ssl/certs/ca-certificates.crt | grep -A1 "AWF Session CA" +``` + +:::note +Applications with certificate pinning will fail to connect when SSL Bump is enabled. Use domain-only filtering for these applications. +::: + +### URL Patterns Not Matching + +**Problem**: Allowed URL patterns are being blocked + +```bash +# Enable debug logging +sudo awf --log-level debug --ssl-bump --allow-urls "..." -- your-command + +# Check exact URL format in logs +sudo cat /tmp/squid-logs-*/access.log | grep your-domain + +# Ensure patterns include scheme (https://) +# ✗ Wrong: github.com/githubnext/* +# ✓ Correct: https://github.com/githubnext/* +``` + +## Known Limitations + +### Certificate Pinning + +Applications that implement certificate pinning will fail when SSL Bump is enabled. The pinned certificate won't match the session CA's generated certificate. + +**Workaround**: Use domain-only filtering without SSL Bump for these applications. + +### HTTP/3 (QUIC) + +SSL Bump works with HTTP/1.1 and HTTP/2. HTTP/3 (QUIC) is not currently supported. + +### WebSocket Connections + +WebSocket over HTTPS (`wss://`) is intercepted and filtered. The initial handshake URL is checked against `--allow-urls` patterns. + +## See Also + +- [CLI Reference](/gh-aw-firewall/reference/cli-reference/) - Complete command-line options +- [Security Architecture](/gh-aw-firewall/reference/security-architecture/) - How the firewall protects traffic diff --git a/docs/ssl-bump.md b/docs/ssl-bump.md new file mode 100644 index 0000000..0be1ec5 --- /dev/null +++ b/docs/ssl-bump.md @@ -0,0 +1,258 @@ +# SSL Bump: HTTPS Content Inspection + +> ⚠️ **Power-User Feature**: SSL Bump is an advanced feature that intercepts HTTPS traffic. It requires local Docker image builds and adds performance overhead. Only enable this when you need URL path filtering for HTTPS traffic. For most use cases, domain-based filtering (default mode) is sufficient. + +SSL Bump enables deep inspection of HTTPS traffic, allowing URL path filtering instead of just domain-based filtering. + +## Overview + +By default, awf filters HTTPS traffic based on domain names using SNI (Server Name Indication). This means you can allow or block `github.com`, but you cannot restrict access to specific paths like `https://github.com/githubnext/*`. + +With SSL Bump enabled (`--ssl-bump`), the firewall generates a per-session CA certificate and intercepts HTTPS connections. This allows: + +- **URL path filtering**: Restrict access to specific paths, not just domains +- **Full HTTP request inspection**: See complete URLs in logs +- **Wildcard URL patterns**: Use `*` wildcards in `--allow-urls` patterns + +## Quick Start + +```bash +# Enable SSL Bump for URL path filtering +sudo awf \ + --allow-domains github.com \ + --ssl-bump \ + --allow-urls "https://github.com/githubnext/*,https://api.github.com/repos/*" \ + -- curl https://github.com/githubnext/some-repo +``` + +## CLI Flags + +### `--ssl-bump` + +Enable SSL Bump for HTTPS content inspection. + +- **Type**: Flag (boolean) +- **Default**: `false` + +When enabled: +1. A per-session CA certificate is generated (valid for 1 day) +2. The CA is injected into the agent container's trust store +3. Squid intercepts HTTPS connections using SSL Bump (peek, stare, bump) +4. URL-based filtering becomes available via `--allow-urls` + +### `--allow-urls ` + +Comma-separated list of allowed URL patterns for HTTPS traffic. Requires `--ssl-bump`. + +- **Type**: String (comma-separated) +- **Requires**: `--ssl-bump` flag + +**Wildcard syntax:** +- `*` matches any characters within a path segment +- Patterns must include the full URL scheme (`https://`) + +**Examples:** +```bash +# Allow specific repository paths +--allow-urls "https://github.com/githubnext/*" + +# Allow API endpoints +--allow-urls "https://api.github.com/repos/*,https://api.github.com/users/*" + +# Combine with domain allowlist +--allow-domains github.com --ssl-bump --allow-urls "https://github.com/githubnext/*" +``` + +## How It Works + +### Without SSL Bump (Default) + +``` +Agent → CONNECT github.com:443 → Squid checks domain ACL → Pass/Block + (SNI only, no path visibility) +``` + +Squid sees only the domain from the TLS ClientHello SNI extension. The URL path is encrypted and invisible. + +### With SSL Bump + +``` +Agent → CONNECT github.com:443 → Squid intercepts TLS + → Squid presents session CA certificate + → Agent trusts session CA (injected into trust store) + → Full HTTPS request visible: GET /githubnext/repo + → Squid checks URL pattern ACL → Pass/Block +``` + +Squid terminates the TLS connection and establishes a new encrypted connection to the destination. This is commonly called a "man-in-the-middle" proxy, but in this case, you control both endpoints. + +### Session CA Certificate Lifecycle + +1. **Generation**: A unique CA key pair is generated at session start +2. **Validity**: Certificate is valid for 1 day maximum +3. **Injection**: CA certificate is added to the agent container's trust store +4. **Cleanup**: CA private key exists only in the temporary work directory +5. **Isolation**: Each awf execution uses a unique CA certificate + +## Example Use Cases + +### Restrict GitHub Access to Specific Organizations + +```bash +sudo awf \ + --allow-domains github.com \ + --ssl-bump \ + --allow-urls "https://github.com/githubnext/*,https://github.com/github/*" \ + -- copilot --prompt "Clone the githubnext/copilot-workspace repo" +``` + +This allows access to repositories under `githubnext` and `github` organizations, but blocks access to other GitHub repositories. + +### API Endpoint Restrictions + +```bash +sudo awf \ + --allow-domains api.github.com \ + --ssl-bump \ + --allow-urls "https://api.github.com/repos/githubnext/*,https://api.github.com/users/*" \ + -- curl https://api.github.com/repos/githubnext/gh-aw-firewall +``` + +Allow only specific API endpoint patterns while blocking others. + +### Debugging with Verbose Logging + +```bash +sudo awf \ + --allow-domains github.com \ + --ssl-bump \ + --allow-urls "https://github.com/*" \ + --log-level debug \ + -- curl https://github.com/githubnext/gh-aw-firewall + +# View full URL paths in Squid logs +sudo cat /tmp/squid-logs-*/access.log +``` + +With SSL Bump enabled, Squid logs show complete URLs, not just domain:port. + +## Security Considerations + +### CA Private Key Protection + +- The CA private key is generated fresh for each session +- It's stored only in the temporary work directory (`/tmp/awf-/`) +- The key is never persisted beyond the session +- Cleanup removes the key when the session ends + +### Certificate Validity + +- Session CA certificates are valid for 1 day maximum +- Short validity limits the window of exposure if a key is compromised +- Each execution generates a new CA, so old certificates become useless + +### Trust Store Modification + +- The session CA is injected only into the agent container's trust store +- Host system trust stores are not modified +- Spawned containers inherit the modified trust store via volume mounts + +### Traffic Visibility + +When SSL Bump is enabled: +- Full HTTP request/response headers are visible to the proxy +- Request bodies can be logged (if configured) +- This is necessary for URL path filtering + +**Warning**: SSL Bump means the proxy can see decrypted HTTPS traffic. Only use this feature when you control the environment and understand the implications. + +### Comparison: SNI-Only vs SSL Bump + +| Feature | SNI-Only (Default) | SSL Bump | +|---------|-------------------|----------| +| Domain filtering | ✓ | ✓ | +| Path filtering | ✗ | ✓ | +| End-to-end encryption | ✓ | Modified (proxy-terminated) | +| Certificate pinning | Works | Broken | +| Performance | Faster | Slight overhead | +| Log detail | Domain:port only | Full URLs | + +## Troubleshooting + +### Certificate Errors in Agent + +**Problem**: Agent reports certificate validation failures + +**Causes**: +1. CA not properly injected into trust store +2. Application uses certificate pinning +3. Custom CA bundle in application ignoring system trust store + +**Solutions**: +```bash +# Check if CA was injected +docker exec awf-agent ls -la /usr/local/share/ca-certificates/ + +# Verify trust store was updated +docker exec awf-agent cat /etc/ssl/certs/ca-certificates.crt | grep -A1 "AWF Session CA" + +# For Node.js apps, ensure NODE_EXTRA_CA_CERTS is not overriding +docker exec awf-agent printenv | grep -i cert +``` + +### URL Patterns Not Matching + +**Problem**: Allowed URL patterns are being blocked + +**Solutions**: +```bash +# Enable debug logging to see pattern matching +sudo awf --log-level debug --ssl-bump --allow-urls "..." -- your-command + +# Check exact URL format in Squid logs +sudo cat /tmp/squid-logs-*/access.log | grep your-domain + +# Ensure patterns include scheme (https://) +# ✗ Wrong: github.com/githubnext/* +# ✓ Correct: https://github.com/githubnext/* +``` + +### Performance Impact + +SSL Bump adds overhead due to TLS termination and re-encryption. For performance-sensitive workloads: + +```bash +# Use domain filtering without SSL Bump when path filtering isn't needed +sudo awf --allow-domains github.com -- your-command + +# Only enable SSL Bump when you specifically need URL path filtering +sudo awf --allow-domains github.com --ssl-bump --allow-urls "..." -- your-command +``` + +## Limitations + +### Certificate Pinning + +Applications that implement certificate pinning will fail to connect when SSL Bump is enabled. The pinned certificate won't match the session CA's generated certificate. + +**Affected applications may include**: +- Mobile apps (if running in container) +- Some security-focused CLI tools +- Applications with hardcoded certificate expectations + +**Workaround**: Use domain-only filtering (`--allow-domains`) without SSL Bump for these applications. + +### HTTP/2 and HTTP/3 + +SSL Bump works with HTTP/1.1 and HTTP/2 over TLS. HTTP/3 (QUIC) is not currently supported by Squid's SSL Bump implementation. + +### WebSocket Connections + +WebSocket connections over HTTPS (`wss://`) are intercepted and filtered the same as regular HTTPS traffic. The initial handshake URL is checked against `--allow-urls` patterns. + +## Related Documentation + +- [Usage Guide](usage.md) - Complete CLI reference +- [Architecture](architecture.md) - How the proxy works +- [Troubleshooting](troubleshooting.md) - Common issues and fixes +- [Logging Quick Reference](logging_quickref.md) - Viewing traffic logs diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 701ad52..a39bfa9 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -349,6 +349,54 @@ docker exec awf-agent dmesg | grep FW_BLOCKED ``` 3. This is why pre-test cleanup is critical in CI/CD +## SSL Bump Issues + +### Certificate Validation Failures + +**Problem:** Agent reports SSL/TLS certificate errors when `--ssl-bump` is enabled + +**Solution:** +1. Verify the CA was injected into the trust store: + ```bash + docker exec awf-agent ls -la /usr/local/share/ca-certificates/ + docker exec awf-agent cat /etc/ssl/certs/ca-certificates.crt | grep -A1 "AWF Session CA" + ``` +2. Check if the application uses certificate pinning (incompatible with SSL Bump) +3. For Node.js applications, verify NODE_EXTRA_CA_CERTS is not overriding: + ```bash + docker exec awf-agent printenv | grep -i cert + ``` + +### URL Patterns Not Matching + +**Problem:** Allowed URL patterns are being blocked with `--ssl-bump` + +**Solution:** +1. Enable debug logging to see pattern matching: + ```bash + sudo awf --log-level debug --ssl-bump --allow-urls "..." 'your-command' + ``` +2. Check the exact URL format in Squid logs: + ```bash + sudo cat /tmp/squid-logs-*/access.log | grep your-domain + ``` +3. Ensure patterns include the scheme: + ```bash + # ✗ Wrong: github.com/githubnext/* + # ✓ Correct: https://github.com/githubnext/* + ``` + +### Application Fails with Certificate Pinning + +**Problem:** Application refuses to connect due to certificate pinning + +**Solution:** +- Applications with certificate pinning are incompatible with SSL Bump +- Use domain-only filtering without `--ssl-bump` for these applications: + ```bash + sudo awf --allow-domains github.com 'your-pinned-app' + ``` + ## Getting More Help If you're still experiencing issues: @@ -372,4 +420,5 @@ If you're still experiencing issues: 4. **Check documentation:** - [Architecture](architecture.md) - Understand how the system works - [Usage Guide](usage.md) - Detailed usage examples + - [SSL Bump](ssl-bump.md) - HTTPS content inspection and URL filtering - [Logging Documentation](../LOGGING.md) - Comprehensive logging guide diff --git a/docs/usage.md b/docs/usage.md index eb7329a..801711c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -8,6 +8,12 @@ sudo awf [options] Options: --allow-domains Comma-separated list of allowed domains (required) Example: github.com,api.github.com,arxiv.org + --block-domains Comma-separated list of blocked domains + Takes precedence over allowed domains + --block-domains-file Path to file containing blocked domains + --ssl-bump Enable SSL Bump for HTTPS content inspection + --allow-urls Comma-separated list of allowed URL patterns (requires --ssl-bump) + Example: https://github.com/githubnext/*,https://api.github.com/repos/* --log-level Log level: debug, info, warn, error (default: info) --keep-containers Keep containers running after command exits --work-dir Working directory for temporary files @@ -254,6 +260,55 @@ sudo awf \ - Block known bad domains while allowing a curated list - Prevent access to internal services from AI agents +## SSL Bump (HTTPS Content Inspection) + +By default, awf filters HTTPS traffic based on domain names only (using SNI). Enable SSL Bump to filter by URL path. + +### Enabling SSL Bump + +```bash +sudo awf \ + --allow-domains github.com \ + --ssl-bump \ + --allow-urls "https://github.com/githubnext/*" \ + 'curl https://github.com/githubnext/some-repo' +``` + +### URL Pattern Syntax + +URL patterns support wildcards: + +```bash +# Match any path under an organization +--allow-urls "https://github.com/githubnext/*" + +# Match specific API endpoints +--allow-urls "https://api.github.com/repos/*,https://api.github.com/users/*" + +# Multiple patterns (comma-separated) +--allow-urls "https://github.com/org1/*,https://github.com/org2/*" +``` + +### How It Works + +When `--ssl-bump` is enabled: + +1. A per-session CA certificate is generated (valid for 1 day) +2. The CA is injected into the agent container's trust store +3. Squid intercepts HTTPS connections to inspect full URLs +4. Requests are matched against `--allow-urls` patterns + +### Security Note + +SSL Bump requires intercepting HTTPS traffic: + +- The session CA is unique to each execution +- CA private key exists only in the temporary work directory +- Short certificate validity (1 day) limits exposure +- Traffic is re-encrypted between proxy and destination + +For more details, see [SSL Bump documentation](ssl-bump.md). + ## Limitations ### No Internationalized Domains diff --git a/src/cli.ts b/src/cli.ts index b6e2f7f..1f4d98e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -384,6 +384,16 @@ program '--proxy-logs-dir ', 'Directory to save Squid proxy logs to (writes access.log directly to this directory)' ) + .option( + '--ssl-bump', + 'Enable SSL Bump for HTTPS content inspection (allows URL path filtering for HTTPS)', + false + ) + .option( + '--allow-urls ', + 'Comma-separated list of allowed URL patterns for HTTPS (requires --ssl-bump).\n' + + ' Supports wildcards: https://github.com/githubnext/*' + ) .argument('[args...]', 'Command and arguments to execute (use -- to separate from options)') .action(async (args: string[], options) => { // Require -- separator for passing command arguments @@ -532,6 +542,21 @@ program process.exit(1); } + // Parse --allow-urls for SSL Bump mode + let allowedUrls: string[] | undefined; + if (options.allowUrls) { + allowedUrls = parseDomains(options.allowUrls); + if (allowedUrls.length > 0 && !options.sslBump) { + logger.error('--allow-urls requires --ssl-bump to be enabled'); + process.exit(1); + } + } + + // Validate SSL Bump option + if (options.sslBump) { + logger.info('SSL Bump mode enabled - HTTPS content inspection will be performed'); + } + const config: WrapperConfig = { allowedDomains, blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined, @@ -549,6 +574,8 @@ program containerWorkDir: options.containerWorkdir, dnsServers, proxyLogsDir: options.proxyLogsDir, + sslBump: options.sslBump, + allowedUrls, }; // Warn if --env-all is used diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 60980ce..13a8119 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -6,6 +6,7 @@ import execa from 'execa'; import { DockerComposeConfig, WrapperConfig, BlockedTarget } from './types'; import { logger } from './logger'; import { generateSquidConfig } from './squid-config'; +import { generateSessionCa, initSslDb, CaFiles, parseUrlPatterns } from './ssl-bump'; const SQUID_PORT = 3128; @@ -146,13 +147,22 @@ async function _generateRandomSubnet(): Promise<{ subnet: string; squidIp: strin ); } +/** + * SSL configuration for Docker Compose (when SSL Bump is enabled) + */ +export interface SslConfig { + caFiles: CaFiles; + sslDbPath: string; +} + /** * Generates Docker Compose configuration * Note: Uses external network 'awf-net' created by host-iptables setup */ export function generateDockerCompose( config: WrapperConfig, - networkConfig: { subnet: string; squidIp: string; agentIp: string } + networkConfig: { subnet: string; squidIp: string; agentIp: string }, + sslConfig?: SslConfig ): DockerComposeConfig { const projectRoot = path.join(__dirname, '..'); @@ -164,6 +174,20 @@ export function generateDockerCompose( // Squid logs path: use proxyLogsDir if specified (direct write), otherwise workDir/squid-logs const squidLogsPath = config.proxyLogsDir || `${config.workDir}/squid-logs`; + // Build Squid volumes list + const squidVolumes = [ + `${config.workDir}/squid.conf:/etc/squid/squid.conf:ro`, + `${squidLogsPath}:/var/log/squid:rw`, + ]; + + // Add SSL-related volumes if SSL Bump is enabled + if (sslConfig) { + squidVolumes.push(`${sslConfig.caFiles.certPath}:${sslConfig.caFiles.certPath}:ro`); + squidVolumes.push(`${sslConfig.caFiles.keyPath}:${sslConfig.caFiles.keyPath}:ro`); + // Mount SSL database at /var/spool/squid_ssl_db (Squid's expected location) + squidVolumes.push(`${sslConfig.sslDbPath}:/var/spool/squid_ssl_db:rw`); + } + // Squid service configuration const squidService: any = { container_name: 'awf-squid', @@ -172,10 +196,7 @@ export function generateDockerCompose( ipv4_address: networkConfig.squidIp, }, }, - volumes: [ - `${config.workDir}/squid.conf:/etc/squid/squid.conf:ro`, - `${squidLogsPath}:/var/log/squid:rw`, - ], + volumes: squidVolumes, healthcheck: { test: ['CMD', 'nc', '-z', 'localhost', '3128'], interval: '5s', @@ -187,7 +208,8 @@ export function generateDockerCompose( }; // Use GHCR image or build locally - if (useGHCR) { + // For SSL Bump, we always build locally to include OpenSSL tools + if (useGHCR && !config.sslBump) { squidService.image = `${registry}/squid:${tag}`; } else { squidService.build = { @@ -275,6 +297,14 @@ export function generateDockerCompose( `${config.workDir}/agent-logs:${process.env.HOME}/.copilot/logs:rw`, ]; + // Add SSL CA certificate mount if SSL Bump is enabled + // This allows the agent container to trust the dynamically-generated CA + if (sslConfig) { + agentVolumes.push(`${sslConfig.caFiles.certPath}:/usr/local/share/ca-certificates/awf-ca.crt:ro`); + // Set environment variable to indicate SSL Bump is enabled + environment.AWF_SSL_BUMP_ENABLED = 'true'; + } + // Add custom volume mounts if specified if (config.volumeMounts && config.volumeMounts.length > 0) { logger.debug(`Adding ${config.volumeMounts.length} custom volume mount(s)`); @@ -435,18 +465,48 @@ export async function writeConfigs(config: WrapperConfig): Promise { } } + // Generate SSL Bump certificates if enabled + let sslConfig: SslConfig | undefined; + if (config.sslBump) { + logger.info('SSL Bump enabled - generating per-session CA certificate...'); + try { + const caFiles = await generateSessionCa({ workDir: config.workDir }); + const sslDbPath = await initSslDb(config.workDir); + sslConfig = { caFiles, sslDbPath }; + logger.info('SSL Bump CA certificate generated successfully'); + logger.warn('⚠️ SSL Bump mode: HTTPS traffic will be intercepted for URL inspection'); + logger.warn(' A per-session CA certificate has been generated (valid for 1 day)'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.error(`Failed to generate SSL Bump CA: ${message}`); + throw new Error(`SSL Bump initialization failed: ${message}`); + } + } + + // Transform user URL patterns to regex patterns for Squid ACLs + let urlPatterns: string[] | undefined; + if (config.allowedUrls && config.allowedUrls.length > 0) { + urlPatterns = parseUrlPatterns(config.allowedUrls); + logger.debug(`Parsed ${urlPatterns.length} URL pattern(s) for SSL Bump filtering`); + } + // Write Squid config + // Note: Use container path for SSL database since it's mounted at /var/spool/squid_ssl_db const squidConfig = generateSquidConfig({ domains: config.allowedDomains, blockedDomains: config.blockedDomains, port: SQUID_PORT, + sslBump: config.sslBump, + caFiles: sslConfig?.caFiles, + sslDbPath: sslConfig ? '/var/spool/squid_ssl_db' : undefined, + urlPatterns, }); const squidConfigPath = path.join(config.workDir, 'squid.conf'); fs.writeFileSync(squidConfigPath, squidConfig); logger.debug(`Squid config written to: ${squidConfigPath}`); // Write Docker Compose config - const dockerCompose = generateDockerCompose(config, networkConfig); + const dockerCompose = generateDockerCompose(config, networkConfig, sslConfig); const dockerComposePath = path.join(config.workDir, 'docker-compose.yml'); fs.writeFileSync(dockerComposePath, yaml.dump(dockerCompose)); logger.debug(`Docker Compose config written to: ${dockerComposePath}`); diff --git a/src/squid-config.test.ts b/src/squid-config.test.ts index 59c5d0b..582a8a0 100644 --- a/src/squid-config.test.ts +++ b/src/squid-config.test.ts @@ -933,4 +933,143 @@ describe('generateSquidConfig', () => { expect(() => generateSquidConfig(config)).toThrow(); }); }); + + describe('SSL Bump Mode', () => { + it('should add SSL Bump section when sslBump is enabled', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + sslBump: true, + caFiles: { + certPath: '/tmp/test/ssl/ca-cert.pem', + keyPath: '/tmp/test/ssl/ca-key.pem', + }, + sslDbPath: '/tmp/test/ssl_db', + }; + const result = generateSquidConfig(config); + expect(result).toContain('SSL Bump configuration for HTTPS content inspection'); + expect(result).toContain('ssl-bump'); + expect(result).toContain('security_file_certgen'); + }); + + it('should include SSL Bump warning comment', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + sslBump: true, + caFiles: { + certPath: '/tmp/test/ssl/ca-cert.pem', + keyPath: '/tmp/test/ssl/ca-key.pem', + }, + sslDbPath: '/tmp/test/ssl_db', + }; + const result = generateSquidConfig(config); + expect(result).toContain('SSL Bump mode enabled'); + expect(result).toContain('HTTPS traffic will be intercepted'); + }); + + it('should configure HTTP port with SSL Bump', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + sslBump: true, + caFiles: { + certPath: '/tmp/test/ssl/ca-cert.pem', + keyPath: '/tmp/test/ssl/ca-key.pem', + }, + sslDbPath: '/tmp/test/ssl_db', + }; + const result = generateSquidConfig(config); + expect(result).toContain('http_port 3128 ssl-bump'); + }); + + it('should include CA certificate path', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + sslBump: true, + caFiles: { + certPath: '/tmp/test/ssl/ca-cert.pem', + keyPath: '/tmp/test/ssl/ca-key.pem', + }, + sslDbPath: '/tmp/test/ssl_db', + }; + const result = generateSquidConfig(config); + expect(result).toContain('cert=/tmp/test/ssl/ca-cert.pem'); + expect(result).toContain('key=/tmp/test/ssl/ca-key.pem'); + }); + + it('should include SSL Bump ACL steps', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + sslBump: true, + caFiles: { + certPath: '/tmp/test/ssl/ca-cert.pem', + keyPath: '/tmp/test/ssl/ca-key.pem', + }, + sslDbPath: '/tmp/test/ssl_db', + }; + const result = generateSquidConfig(config); + expect(result).toContain('acl step1 at_step SslBump1'); + expect(result).toContain('acl step2 at_step SslBump2'); + expect(result).toContain('ssl_bump peek step1'); + expect(result).toContain('ssl_bump stare step2'); + }); + + it('should include ssl_bump rules for allowed domains', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + sslBump: true, + caFiles: { + certPath: '/tmp/test/ssl/ca-cert.pem', + keyPath: '/tmp/test/ssl/ca-key.pem', + }, + sslDbPath: '/tmp/test/ssl_db', + }; + const result = generateSquidConfig(config); + expect(result).toContain('ssl_bump bump allowed_domains'); + expect(result).toContain('ssl_bump terminate all'); + }); + + it('should include URL pattern ACLs when provided', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + sslBump: true, + caFiles: { + certPath: '/tmp/test/ssl/ca-cert.pem', + keyPath: '/tmp/test/ssl/ca-key.pem', + }, + sslDbPath: '/tmp/test/ssl_db', + urlPatterns: ['^https://github\\.com/githubnext/.*'], + }; + const result = generateSquidConfig(config); + expect(result).toContain('acl allowed_url_0 url_regex'); + expect(result).toContain('^https://github\\.com/githubnext/.*'); + }); + + it('should not include SSL Bump section when disabled', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + sslBump: false, + }; + const result = generateSquidConfig(config); + expect(result).not.toContain('SSL Bump configuration'); + expect(result).not.toContain('https_port'); + expect(result).not.toContain('ssl-bump'); + }); + + it('should use http_port only when SSL Bump is disabled', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: defaultPort, + }; + const result = generateSquidConfig(config); + expect(result).toContain('http_port 3128'); + expect(result).not.toContain('https_port'); + }); + }); }); diff --git a/src/squid-config.ts b/src/squid-config.ts index 468010c..a360366 100644 --- a/src/squid-config.ts +++ b/src/squid-config.ts @@ -53,6 +53,107 @@ function groupPatternsByProtocol(patterns: DomainPattern[]): PatternsByProtocol return result; } +/** + * Generates SSL Bump configuration section for HTTPS content inspection + * + * @param caFiles - Paths to CA certificate and key + * @param sslDbPath - Path to SSL certificate database + * @param hasPlainDomains - Whether there are plain domain ACLs + * @param hasPatterns - Whether there are pattern ACLs + * @param urlPatterns - Optional URL patterns for HTTPS filtering + * @returns Squid SSL Bump configuration string + */ +function generateSslBumpSection( + caFiles: { certPath: string; keyPath: string }, + sslDbPath: string, + hasPlainDomains: boolean, + hasPatterns: boolean, + urlPatterns?: string[] +): string { + // Build the SSL Bump domain list for the bump directive + let bumpAcls = ''; + if (hasPlainDomains && hasPatterns) { + bumpAcls = 'ssl_bump bump allowed_domains\nssl_bump bump allowed_domains_regex'; + } else if (hasPlainDomains) { + bumpAcls = 'ssl_bump bump allowed_domains'; + } else if (hasPatterns) { + bumpAcls = 'ssl_bump bump allowed_domains_regex'; + } else { + // No domains configured - terminate all + bumpAcls = '# No domains configured - terminate all SSL connections'; + } + + // Generate URL pattern ACLs if provided + let urlAclSection = ''; + let urlAccessRules = ''; + if (urlPatterns && urlPatterns.length > 0) { + const urlAcls = urlPatterns + .map((pattern, i) => `acl allowed_url_${i} url_regex ${pattern}`) + .join('\n'); + urlAclSection = `\n# URL pattern ACLs for HTTPS content inspection\n${urlAcls}\n`; + + // Build access rules for URL patterns + // When URL patterns are specified, we: + // 1. Allow requests matching the URL patterns + // 2. Deny all other requests to allowed_domains (they didn't match URL patterns) + const urlAccessLines = urlPatterns + .map((_, i) => `http_access allow allowed_url_${i}`) + .join('\n'); + + // Deny requests to allowed domains that don't match URL patterns + // This ensures URL-level filtering is enforced + // IMPORTANT: Use !CONNECT to only deny actual HTTP requests after bump, + // not the CONNECT request itself (which must be allowed for SSL bump to work) + const denyNonMatching = hasPlainDomains + ? 'http_access deny !CONNECT allowed_domains' + : hasPatterns + ? 'http_access deny !CONNECT allowed_domains_regex' + : ''; + + urlAccessRules = `\n# Allow HTTPS requests matching URL patterns\n${urlAccessLines}\n\n# Deny requests that don't match URL patterns\n${denyNonMatching}\n`; + } + + return ` +# SSL Bump configuration for HTTPS content inspection +# WARNING: This enables TLS interception - traffic is decrypted for inspection +# A per-session CA certificate is used for dynamic certificate generation + +# HTTP port with SSL Bump enabled for HTTPS interception +# This handles both HTTP requests and HTTPS CONNECT requests +http_port 3128 ssl-bump \\ + cert=${caFiles.certPath} \\ + key=${caFiles.keyPath} \\ + generate-host-certificates=on \\ + dynamic_cert_mem_cache_size=16MB \\ + options=NO_SSLv3,NO_TLSv1,NO_TLSv1_1 + +# SSL certificate database for dynamic certificate generation +# Using 16MB for certificate cache (sufficient for typical AI agent sessions) +sslcrtd_program /usr/lib/squid/security_file_certgen -s ${sslDbPath} -M 16MB +sslcrtd_children 5 + +# SSL Bump ACL steps: +# Step 1 (SslBump1): Peek at ClientHello to get SNI +# Step 2 (SslBump2): Stare at server certificate to validate +# Step 3 (SslBump3): Bump or splice based on policy +acl step1 at_step SslBump1 +acl step2 at_step SslBump2 +acl step3 at_step SslBump3 + +# Peek at ClientHello to see SNI (Server Name Indication) +ssl_bump peek step1 + +# Stare at server certificate to validate it +ssl_bump stare step2 + +# Bump (intercept) connections to allowed domains +${bumpAcls} + +# Terminate (deny) connections to non-allowed domains +ssl_bump terminate all +${urlAclSection}${urlAccessRules}`; +} + /** * Generates Squid proxy configuration with domain whitelisting and optional blocklisting * @@ -67,6 +168,8 @@ function groupPatternsByProtocol(patterns: DomainPattern[]): PatternsByProtocol * - https://domain.com -> allow only HTTPS traffic * - domain.com -> allow both HTTP and HTTPS (default) * + * When sslBump is enabled, adds SSL Bump configuration for HTTPS inspection. + * * @example * // Plain domain: github.com -> acl allowed_domains dstdomain .github.com * // Wildcard: *.github.com -> acl allowed_domains_regex dstdom_regex -i ^.*\.github\.com$ @@ -74,7 +177,7 @@ function groupPatternsByProtocol(patterns: DomainPattern[]): PatternsByProtocol * // Blocked: internal.example.com -> acl blocked_domains dstdomain .internal.example.com */ export function generateSquidConfig(config: SquidConfig): string { - const { domains, blockedDomains, port } = config; + const { domains, blockedDomains, port, sslBump, caFiles, sslDbPath, urlPatterns } = config; // Parse domains into plain domains and wildcard patterns // Note: parseDomainList extracts and preserves protocol info from prefixes (http://, https://) @@ -275,8 +378,30 @@ export function generateSquidConfig(config: SquidConfig): string { ? allAccessRules.join('\n') + '\n' : ''; + // Generate SSL Bump section if enabled + let sslBumpSection = ''; + let portConfig = `http_port ${port}`; + + // For SSL Bump, we need to check hasPlainDomains and hasPatterns for the 'both' protocol domains + // since those are the ones that go into allowed_domains / allowed_domains_regex ACLs + const hasPlainDomainsForSslBump = domainsByProto.both.length > 0; + const hasPatternsForSslBump = patternsByProto.both.length > 0; + + if (sslBump && caFiles && sslDbPath) { + sslBumpSection = generateSslBumpSection( + caFiles, + sslDbPath, + hasPlainDomainsForSslBump, + hasPatternsForSslBump, + urlPatterns + ); + // SSL Bump section includes its own port config, so use that instead + portConfig = ''; + } + return `# Squid configuration for egress traffic control # Generated by awf +${sslBump ? '\n# SSL Bump mode enabled - HTTPS traffic will be intercepted for URL inspection' : ''} # Custom log format with detailed connection information # Format: timestamp client_ip:port dest_domain dest_ip:port protocol method status decision url user_agent @@ -288,11 +413,12 @@ access_log /var/log/squid/access.log firewall_detailed cache_log /var/log/squid/cache.log cache deny all -# Port configuration -http_port ${port} - ${aclSection} +# Port configuration +${portConfig} +${sslBumpSection} + # Network ACLs acl localnet src 10.0.0.0/8 acl localnet src 172.16.0.0/12 diff --git a/src/ssl-bump.test.ts b/src/ssl-bump.test.ts new file mode 100644 index 0000000..441dd36 --- /dev/null +++ b/src/ssl-bump.test.ts @@ -0,0 +1,68 @@ +import { parseUrlPatterns } from './ssl-bump'; + +describe('SSL Bump', () => { + describe('parseUrlPatterns', () => { + it('should escape regex special characters except wildcards', () => { + const patterns = parseUrlPatterns(['https://github.com/user']); + expect(patterns).toEqual(['^https://github\\.com/user$']); + }); + + it('should convert * wildcard to .* regex', () => { + const patterns = parseUrlPatterns(['https://github.com/githubnext/*']); + expect(patterns).toEqual(['^https://github\\.com/githubnext/.*']); + }); + + it('should handle multiple wildcards', () => { + const patterns = parseUrlPatterns(['https://api-*.example.com/*']); + expect(patterns).toEqual(['^https://api-.*\\.example\\.com/.*']); + }); + + it('should remove trailing slash for consistency', () => { + const patterns = parseUrlPatterns(['https://github.com/']); + expect(patterns).toEqual(['^https://github\\.com$']); + }); + + it('should handle exact match patterns', () => { + const patterns = parseUrlPatterns(['https://api.example.com/v1/users']); + expect(patterns).toEqual(['^https://api\\.example\\.com/v1/users$']); + }); + + it('should handle query parameters', () => { + const patterns = parseUrlPatterns(['https://api.example.com/v1?key=value']); + expect(patterns).toEqual(['^https://api\\.example\\.com/v1\\?key=value$']); + }); + + it('should escape dots in domain names', () => { + const patterns = parseUrlPatterns(['https://sub.domain.example.com/path']); + expect(patterns).toEqual(['^https://sub\\.domain\\.example\\.com/path$']); + }); + + it('should handle multiple patterns', () => { + const patterns = parseUrlPatterns([ + 'https://github.com/githubnext/*', + 'https://api.example.com/v1/*', + ]); + expect(patterns).toHaveLength(2); + expect(patterns[0]).toBe('^https://github\\.com/githubnext/.*'); + expect(patterns[1]).toBe('^https://api\\.example\\.com/v1/.*'); + }); + + it('should handle empty array', () => { + const patterns = parseUrlPatterns([]); + expect(patterns).toEqual([]); + }); + + it('should anchor patterns correctly for exact matches', () => { + const patterns = parseUrlPatterns(['https://github.com/exact']); + // Should have both start and end anchors for exact matches + expect(patterns[0]).toBe('^https://github\\.com/exact$'); + }); + + it('should not add end anchor for wildcard patterns', () => { + const patterns = parseUrlPatterns(['https://github.com/*']); + // Should only have start anchor for patterns ending with .* + expect(patterns[0]).toBe('^https://github\\.com/.*'); + expect(patterns[0]).not.toContain('$'); + }); + }); +}); diff --git a/src/ssl-bump.ts b/src/ssl-bump.ts new file mode 100644 index 0000000..d0c0a8d --- /dev/null +++ b/src/ssl-bump.ts @@ -0,0 +1,208 @@ +/** + * SSL Bump utilities for HTTPS content inspection + * + * This module provides functionality to generate per-session CA certificates + * for Squid SSL Bump mode, which enables URL path filtering for HTTPS traffic. + * + * Security considerations: + * - CA key is stored only in workDir (tmpfs-backed in container) + * - Certificate is valid for 1 day only + * - Private key is never logged + * - CA is unique per session + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import execa from 'execa'; +import { logger } from './logger'; + +/** + * Configuration for SSL Bump CA generation + */ +export interface SslBumpConfig { + /** Working directory to store CA files */ + workDir: string; + /** Common name for the CA certificate (default: 'AWF Session CA') */ + commonName?: string; + /** Validity period in days (default: 1) */ + validityDays?: number; +} + +/** + * Result of CA generation containing paths to certificate files + */ +export interface CaFiles { + /** Path to CA certificate (PEM format) */ + certPath: string; + /** Path to CA private key (PEM format) */ + keyPath: string; + /** DER format certificate for easy import */ + derPath: string; +} + +/** + * Generates a self-signed CA certificate for SSL Bump + * + * The CA certificate is used by Squid to generate per-host certificates + * on-the-fly, allowing it to inspect HTTPS traffic for URL filtering. + * + * @param config - SSL Bump configuration + * @returns Paths to generated CA files + * @throws Error if OpenSSL commands fail + */ +export async function generateSessionCa(config: SslBumpConfig): Promise { + const { workDir, commonName = 'AWF Session CA', validityDays = 1 } = config; + + // Create ssl directory in workDir + const sslDir = path.join(workDir, 'ssl'); + if (!fs.existsSync(sslDir)) { + fs.mkdirSync(sslDir, { recursive: true, mode: 0o700 }); + } + + const certPath = path.join(sslDir, 'ca-cert.pem'); + const keyPath = path.join(sslDir, 'ca-key.pem'); + const derPath = path.join(sslDir, 'ca-cert.der'); + + logger.debug(`Generating SSL Bump CA certificate in ${sslDir}`); + + try { + // Generate RSA private key and self-signed certificate in one command + // Using -batch to avoid interactive prompts + await execa('openssl', [ + 'req', + '-new', + '-newkey', 'rsa:2048', + '-days', validityDays.toString(), + '-nodes', // No password on private key + '-x509', + '-subj', `/CN=${commonName}`, + '-keyout', keyPath, + '-out', certPath, + '-batch', + ]); + + // Set restrictive permissions on private key + fs.chmodSync(keyPath, 0o600); + fs.chmodSync(certPath, 0o644); + + logger.debug(`CA certificate generated: ${certPath}`); + logger.debug(`CA private key generated: ${keyPath}`); + + // Generate DER format for easier import into trust stores + await execa('openssl', [ + 'x509', + '-in', certPath, + '-outform', 'DER', + '-out', derPath, + ]); + + fs.chmodSync(derPath, 0o644); + logger.debug(`CA certificate (DER) generated: ${derPath}`); + + return { certPath, keyPath, derPath }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to generate SSL Bump CA: ${message}`); + } +} + +/** + * Initializes Squid's SSL certificate database + * + * Squid requires a certificate database to store dynamically generated + * certificates for SSL Bump mode. The database structure expected by Squid is: + * - ssl_db/certs/ - Directory for storing generated certificates + * - ssl_db/index.txt - Index file for certificate lookups + * - ssl_db/size - File tracking current database size + * + * NOTE: We create this structure on the host because security_file_certgen + * (Squid's DB initialization tool) requires the directory to NOT exist when + * it runs. Since Docker volume mounts create the directory, we need to + * pre-populate the structure ourselves. + * + * @param workDir - Working directory + * @returns Path to the SSL database directory + */ +export async function initSslDb(workDir: string): Promise { + const sslDbPath = path.join(workDir, 'ssl_db'); + const certsPath = path.join(sslDbPath, 'certs'); + const indexPath = path.join(sslDbPath, 'index.txt'); + const sizePath = path.join(sslDbPath, 'size'); + + // Create the database structure + if (!fs.existsSync(sslDbPath)) { + fs.mkdirSync(sslDbPath, { recursive: true, mode: 0o700 }); + } + + // Create certs subdirectory + if (!fs.existsSync(certsPath)) { + fs.mkdirSync(certsPath, { mode: 0o700 }); + } + + // Create index.txt (empty file for certificate index) + if (!fs.existsSync(indexPath)) { + fs.writeFileSync(indexPath, '', { mode: 0o600 }); + } + + // Create size file (tracks current DB size, starts at 0) + if (!fs.existsSync(sizePath)) { + fs.writeFileSync(sizePath, '0\n', { mode: 0o600 }); + } + + logger.debug(`SSL certificate database initialized at: ${sslDbPath}`); + return sslDbPath; +} + +/** + * Validates that OpenSSL is available + * + * @returns true if OpenSSL is available, false otherwise + */ +export async function isOpenSslAvailable(): Promise { + try { + await execa('openssl', ['version']); + return true; + } catch { + return false; + } +} + +/** + * Parses URL patterns for SSL Bump ACL rules + * + * Converts user-friendly URL patterns into Squid url_regex ACL patterns. + * + * Examples: + * - `https://github.com/githubnext/*` → `^https://github\.com/githubnext/.*` + * - `https://api.example.com/v1/users` → `^https://api\.example\.com/v1/users$` + * + * @param patterns - Array of URL patterns (can include wildcards) + * @returns Array of regex patterns for Squid url_regex ACL + */ +export function parseUrlPatterns(patterns: string[]): string[] { + return patterns.map(pattern => { + // Remove trailing slash for consistency + let p = pattern.replace(/\/$/, ''); + + // Preserve .* patterns by using a placeholder before escaping + const WILDCARD_PLACEHOLDER = '\x00WILDCARD\x00'; + p = p.replace(/\.\*/g, WILDCARD_PLACEHOLDER); + + // Escape regex special characters except * + p = p.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); + + // Convert * wildcards to .* regex + p = p.replace(/\*/g, '.*'); + + // Restore .* patterns from placeholder + p = p.replace(new RegExp(WILDCARD_PLACEHOLDER, 'g'), '.*'); + + // Anchor the pattern + // If pattern ends with .* (from wildcard), don't add end anchor + if (p.endsWith('.*')) { + return `^${p}`; + } + // For exact matches, add end anchor + return `^${p}$`; + }); +} diff --git a/src/types.ts b/src/types.ts index 35e9b89..192eb86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -233,6 +233,35 @@ export interface WrapperConfig { * @example '/tmp/my-proxy-logs' */ proxyLogsDir?: string; + + /** + * Whether to enable SSL Bump for HTTPS content inspection + * + * When true, Squid will intercept HTTPS connections and generate + * per-host certificates on-the-fly, allowing inspection of URL paths, + * query parameters, and request methods for HTTPS traffic. + * + * Security implications: + * - A per-session CA certificate is generated (valid for 1 day) + * - The CA certificate is injected into the agent container's trust store + * - HTTPS traffic is decrypted at the proxy for inspection + * - The CA private key is stored only in the temporary work directory + * + * @default false + */ + sslBump?: boolean; + + /** + * URL patterns to allow for HTTPS traffic (requires sslBump: true) + * + * When SSL Bump is enabled, these patterns are used to filter HTTPS + * traffic by URL path, not just domain. Supports wildcards (*). + * + * If not specified, falls back to domain-only filtering. + * + * @example ['https://github.com/githubnext/*', 'https://api.example.com/v1/*'] + */ + allowedUrls?: string[]; } /** @@ -284,6 +313,41 @@ export interface SquidConfig { * @default 3128 */ port: number; + + /** + * Whether to enable SSL Bump for HTTPS content inspection + * + * When true, Squid will intercept HTTPS connections and generate + * per-host certificates on-the-fly, allowing inspection of URL paths. + * + * @default false + */ + sslBump?: boolean; + + /** + * Paths to CA certificate files for SSL Bump + * + * Required when sslBump is true. + */ + caFiles?: { + certPath: string; + keyPath: string; + }; + + /** + * Path to SSL certificate database for dynamic certificate generation + * + * Required when sslBump is true. + */ + sslDbPath?: string; + + /** + * URL patterns for HTTPS traffic filtering (requires sslBump) + * + * When SSL Bump is enabled, these regex patterns are used to filter + * HTTPS traffic by URL path, not just domain. + */ + urlPatterns?: string[]; } /**