# Running behind a reverse proxy The restic-manager server is HTTP-only by design (see `spec.md` §11): TLS termination, public hostname, ACME, HSTS, and edge-level rate limiting all belong to a reverse proxy that you already operate outside this project. The reference compose in `deploy/docker-compose.yml` stands up *only* the server; this page covers what your proxy needs to do to make the rest of it work. ## 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 (single value, or comma chain) | Rate-limit keys, audit log entries, and OIDC redirect-URI checks all use the real client IP. | | `X-Forwarded-Proto` | `https` | The server emits absolute URLs (e.g. OIDC redirect URIs) using this. | | `Host` | The public hostname clients use | Cookies are scoped to this; `RM_BASE_URL` must match. | | `Connection`/`Upgrade` | Pass through unchanged | The agent connects on `/ws/agent` and the live-log viewer connects on `/api/jobs/{id}/stream` — both are WebSockets and need `Upgrade: websocket` to survive the hop. | 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. ## Example: Caddy ```caddyfile restic.example.com { # Caddy's default reverse_proxy preserves Host, sets # X-Forwarded-For/Proto, and passes Connection: upgrade through, # so a single directive covers HTTP + WebSocket. reverse_proxy 127.0.0.1:8080 encode zstd gzip } ``` `RM_TRUSTED_PROXY=127.0.0.1/32` if Caddy and the server share the host; the docker-bridge CIDR (commonly `172.16.0.0/12`) if Caddy runs in another container on the default bridge network. ## Example: nginx ```nginx server { listen 443 ssl http2; server_name restic.example.com; ssl_certificate /etc/ssl/restic.example.com.fullchain.pem; ssl_certificate_key /etc/ssl/restic.example.com.key.pem; location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; # WebSocket support — agent + live-log endpoints need this. proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; # Trusted-proxy headers. proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; # Live job logs are long-running streams. Bump read timeouts # so nginx doesn't drop them mid-backup. proxy_read_timeout 1h; proxy_send_timeout 1h; } } # Standard websocket upgrade map (define once at the http {} level). map $http_upgrade $connection_upgrade { default upgrade; '' close; } ``` `RM_TRUSTED_PROXY` for the same-host case: `127.0.0.1/32`. ## Example: Traefik (label-based) ```yaml labels: - "traefik.enable=true" - "traefik.http.routers.restic-manager.rule=Host(`restic.example.com`)" - "traefik.http.routers.restic-manager.entrypoints=websecure" - "traefik.http.routers.restic-manager.tls.certresolver=letsencrypt" - "traefik.http.services.restic-manager.loadbalancer.server.port=8080" ``` Traefik handles `X-Forwarded-*` and `Connection: upgrade` by default. `RM_TRUSTED_PROXY` should be the docker network the Traefik container shares with the server (commonly `172.16.0.0/12` for the default bridge, or whatever your overlay network's CIDR is). ## Sanity-checking the wiring After bringing the stack up: 1. `curl -fsS https://restic.example.com/healthz` — should return 200. 2. The login page should report HTTPS in the address bar; cookies set after login should carry the `Secure` flag. 3. Check the server log for the `config resolved` line: `trusted_proxies` must include the IP/CIDR your proxy actually connects from. 4. Enrol a test agent — the WebSocket handshake hitting `/ws/agent` confirms `Upgrade` is being forwarded correctly. If any of those fail, the proxy is the first place to look — the server itself is intentionally minimal.