Files
restic-manager/docs/book/src/getting-started/reverse-proxy.md
T
steve bb4ed3502d P5: OSS readiness — docs site, contributor onboarding, e2e harness
P5-01 — Documentation site under docs/book/ rendered with mdBook
(downloaded via Makefile, same static-binary pattern as Tailwind).
Structured chapters: getting started, concepts, operations,
security, reference. `make docs` / `make docs-watch`. Generated
output gitignored.

P5-02 — CONTRIBUTING.md rewritten from placeholder to a full
guide. CODE_OF_CONDUCT.md adapted from Contributor Covenant for a
single-maintainer project. .gitea/issue_template/{bug,feature}.md
and PULL_REQUEST_TEMPLATE.md.

P5-04 — Six README screenshots captured live from a fresh server
bootstrap (login, empty dashboard, add-host, alerts, settings,
audit log). README rewritten to centre the screenshot grid and
link out to the docs site.

P5-05 — SECURITY.md with disclosure policy (3-day ack, 30-day
default window), scope in/out, threat-model summary, operator
hardening checklist. Mirrored as a docs-site chapter.

P5-06 — End-to-end test harness. e2e/compose.e2e.yml brings up
server + sibling Linux agent (alpine + restic) + restic/rest-server.
Agent uses announce-and-approve so Playwright can drive the full
operator flow: bootstrap → login → accept pending → backup →
verify terminal status. Second spec scrapes /metrics to assert
the P6-04 endpoint surface. .gitea/workflows/e2e.yml runs on every
PR; local how-to in docs/e2e.md.
2026-05-08 20:08:23 +01:00

3.1 KiB

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

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

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

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.