# Caddy Build

Custom Caddy reverse proxy image with WAF, rate limiting, and shared Redis storage for TLS certificates and rate limit state across a container fleet.

## Caddy Modules

Built via `xcaddy` with:

- **[coraza-caddy](https://github.com/corazawaf/coraza-caddy)** — OWASP Core Rule Set WAF. Blocks SQL injection, XSS, command injection, path traversal, and other OWASP Top 10 attacks. Runs at paranoia level 2.
- **[caddy-ratelimit](https://github.com/mholt/caddy-ratelimit)** — Per-IP rate limiting with distributed state sync via Redis.
- **[caddy-storage-redis](https://github.com/pberkel/caddy-storage-redis)** — Shared storage backend for TLS certificates, ACME challenge tokens, and rate limit state. Eliminates EFS eventual-consistency issues with ACME challenges in a fleet.

## Custom WAF Rules

`custom-rules.conf` is baked into the image at `/etc/caddy/custom-rules.conf` and pulled in
by both Caddyfiles via `Include /etc/caddy/custom-rules.conf` inside the `coraza_waf`
directives block. Rule IDs `100000-199999` are reserved for these rules.

| ID | Blocks | Status |
|----|--------|--------|
| `100001` | Host header that is an IP address | 403 |
| `100002` | Missing Host header | 403 |
| `100003` | Scanner user agents (nikto, sqlmap, nmap, masscan, zgrab) | 403 |
| `100004` | Sensitive paths (`.env`, `.git`, `.svn`, `.hg`, wp-admin, wp-login, phpinfo, phpmyadmin, `admin.php`, `xmlrpc.php`) | 403 |
| `100005` | Request bodies over 10MB | 413 |
| `100006` | Missing User-Agent header (absent, not empty) | 403 |

When editing these rules, escape regex metacharacters with a **single** backslash (`\.`).
Coraza's seclang parser unescapes double-quoted strings, so `\\.` reaches the regex engine
as "literal backslash followed by any char" and silently never matches.

> **Note:** `load_owasp_crs` only makes the OWASP CRS files *available* for inclusion — it
> does not load them. The Caddyfiles do not currently `Include @owasp_crs/*.conf`, so CRS
> rules (and the `tx.paranoia_level` setting) are not evaluated. Only the custom rules above
> are active. See "Enabling OWASP CRS" below.

### Enabling OWASP CRS

To actually evaluate CRS rules, add these includes ahead of the other directives:

```
Include @coraza.conf-recommended
Include @crs-setup.conf.example
Include @owasp_crs/*.conf
```

This turns on CRS 4.x in blocking mode and will reject SQLi/XSS/traversal payloads. Expect
false positives on legitimate traffic; tune before enabling in production.

## Container Tags

Two image variants are published to `netfoundry/zrok-caddy`:

| Tag | Caddyfile | ACME Provider | EAB Required |
|-----|-----------|---------------|--------------|
| `waf-noip-le` | `Caddyfile.letsencrypt` | Let's Encrypt | No |
| `waf-noip-dc` | `Caddyfile.digicert` | Digicert (configurable) | Yes |

## Building & Pushing

```bash
# Push Let's Encrypt variant
./push_new_tag.sh waf-noip-le Caddyfile.letsencrypt

# Push Digicert variant
./push_new_tag.sh waf-noip-dc Caddyfile.digicert
```

Script usage: `./push_new_tag.sh <tag> <Caddyfile.letsencrypt|Caddyfile.digicert>`

Builds multi-arch images (`linux/amd64`, `linux/arm64`) using the `xbuilder` buildx instance.

### Local Test Build

```bash
# Build locally
docker build --build-arg CADDYFILE=Caddyfile.letsencrypt -t zrok-caddy:test .

# Validate Caddyfile syntax
docker run --rm \
  -e ASK_URL=http://localhost:9999 \
  -e REDIS_HOST=localhost \
  zrok-caddy:test caddy adapt --config /etc/caddy/Caddyfile
```

## Environment Variables

### Both variants

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `ASK_URL` | Yes | — | URL for on-demand TLS domain validation (ask-http service) |
| `REDIS_HOST` | Yes | `localhost` | Redis/ElastiCache endpoint |
| `REDIS_PORT` | No | `6379` | Redis port |
| `REDIS_DB` | No | `0` | Redis database number |
| `REDIS_TLS` | No | `false` | Enable TLS to Redis |
| `CADDY_LOG_LEVEL` | No | `INFO` | Log level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `RL_EVENTS_PER_WINDOW` | No | `100` | Max requests per IP per window |
| `RL_WINDOW` | No | `1m` | Rate limit window duration |
| `RL_WRITE_INTERVAL` | No | `5s` | How often to sync rate limit state to Redis |
| `RL_READ_INTERVAL` | No | `5s` | How often to read peer rate limit state from Redis |

### Digicert variant only (`waf-noip-dc`)

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `ACME_DIRECTORY_URL` | Yes | — | Digicert ACME directory URL |
| `EAB_KID` | Yes | — | External Account Binding key ID |
| `EAB_HMAC_KEY` | Yes | — | External Account Binding HMAC key |
