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.
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.