# Running behind a reverse proxy

The restic-manager server is HTTP-only by design. TLS termination,
public hostname, ACME, HSTS, and edge-level rate limiting all
belong to a reverse proxy you already operate outside this project.

## What the proxy must forward

The server reads four headers when (and only when) the immediate
peer matches `RM_TRUSTED_PROXY`:

| Header                 | Value                                              | Why |
|------------------------|----------------------------------------------------|-----|
| `X-Forwarded-For`      | The original client IP                             | Rate-limit keys, audit log entries, OIDC redirect-URI checks. |
| `X-Forwarded-Proto`    | `https`                                            | Used for absolute URLs (e.g. OIDC redirect URIs). |
| `Host`                 | The public hostname clients use                    | Cookies are scoped to this; `RM_BASE_URL` must match. |
| `Connection` / `Upgrade` | Pass through unchanged                           | `/ws/agent` and `/api/jobs/{id}/stream` are WebSockets; without `Upgrade: websocket` they fail. |

Set `RM_TRUSTED_PROXY` to the CIDR (or comma-separated list of
CIDRs) the proxy connects from. Anything outside that range has
its `X-Forwarded-*` headers ignored, so a stray request that
bypasses the proxy can't spoof the client IP.

## Caddy

```caddyfile
restic.example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:8080 {
        header_up X-Real-IP {remote_host}
    }
}
```

Caddy adds `X-Forwarded-For` / `X-Forwarded-Proto` automatically
and passes WebSocket headers through by default, so this is the
whole config.

## nginx

```nginx
server {
    listen 443 ssl http2;
    server_name restic.example.com;

    ssl_certificate     /etc/letsencrypt/live/restic.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/restic.example.com/privkey.pem;

    location / {
        proxy_pass         http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto https;

        # WebSocket upgrade
        proxy_set_header   Upgrade           $http_upgrade;
        proxy_set_header   Connection        "upgrade";

        # Long-lived agent WS — disable read timeout for this surface.
        proxy_read_timeout 86400s;
    }
}
```

## Traefik

```yaml
http:
  routers:
    restic-manager:
      rule: "Host(`restic.example.com`)"
      entryPoints: [websecure]
      tls:
        certResolver: letsencrypt
      service: restic-manager

  services:
    restic-manager:
      loadBalancer:
        servers:
          - url: "http://restic-manager:8080"
        passHostHeader: true
```

Traefik forwards WebSocket upgrades and the standard
`X-Forwarded-*` set out of the box.

## Verification

After bringing the proxy up, the audit log should show your real
client IP for an interactive login (not the proxy's local
address). If you see `127.0.0.1` or the proxy's container IP, your
`RM_TRUSTED_PROXY` is wrong or `X-Forwarded-For` isn't being
forwarded.
