diff options
Diffstat (limited to 'docs/operations.md')
| -rw-r--r-- | docs/operations.md | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/docs/operations.md b/docs/operations.md index fe09537..fae3b6c 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -103,6 +103,74 @@ One node at a time, restarting each with its files. What to know: switching ([`remakes.md`](remakes.md), Limits). Don't remake while the swarm runs two versions. * **The dashboard** reloads by itself once its node serves another version of it. +## Behind a reverse proxy + +The HTTP API speaks plain HTTP on `127.0.0.1:3000`; a reverse proxy in front adds TLS, +and can authenticate users itself (single sign-on, client certificates, its own +password file). It then names the user in a header, which the node believes from where +the proxy connects alone (`--user-header`, `--trusted-proxies`); requests from +elsewhere still need `--htpasswd`, which becomes optional. The proxy must set or clear +that header on every request, or a client could set it itself. + +Where the proxy connects decides who can pose as it. Every user of a host connects +from its loopback addresses, so trusting those (as `--trusted-proxies` does by +default) trusts everyone on the host. A Unix socket is better: `--bind-addr` takes +`unix:PATH` too, as many places as given, and makes the socket readable and writable +by the node's user and group only, so only members of that group (the proxy's user, +added to it) can connect at all. Trust it alone with `--trusted-proxies unix`, and keep +a TCP address for the rest (a command-line tool, with `--htpasswd`): + +```sh +continuum --bind-addr 127.0.0.1:3000,unix:/run/continuum/api.sock \ + --user-header X-Remote-User --trusted-proxies unix --htpasswd htpasswd ... +``` + +```nginx +location / { + auth_basic "continuum"; + auth_basic_user_file /etc/nginx/continuum.htpasswd; + proxy_set_header X-Remote-User $remote_user; + proxy_pass http://unix:/run/continuum/api.sock; +} +``` + +The directory of the socket must exist and let the proxy's user through (with systemd, +`RuntimeDirectory=continuum` and `RuntimeDirectoryMode=0750`, the proxy's user in the +node's group). A socket left behind by an earlier run is replaced, and the node removes +its own when it stops. With a proxy on another host, trust its address instead, and +have the node listen where only that host reaches it. + +Where a socket won't do, the host's firewall can keep everyone but the proxy's user +off the API's port, as Linux filters local connections by the user that opens them. +With nftables, and nginx running as `www-data`: + +``` +table inet continuum { + chain output { + type filter hook output priority 0; + oifname "lo" tcp dport 3000 meta skuid != "www-data" reject + } +} +``` + +(with iptables: `-A OUTPUT -o lo -p tcp --dport 3000 -m owner ! --uid-owner www-data -j +REJECT`). The loopback addresses can then stay trusted; root can still get past it, as it +can past a socket's permissions. Every address of the API on loopback is trusted alike, +so a command-line tool of another user goes through the proxy then. + +The names the proxy gives are the users of `--protocol-writers` and of who wrote what +([`api.md`](api.md)). + +What streams (`/stream`, the dashboard's events) says `X-Accel-Buffering: no`, so nginx +passes it on as it comes, and says it's alive while nothing changes: the dashboard's +events every 15 s, `/stream` every 30 s (a blank line, `--stream-heartbeat`), within +nginx's `proxy_read_timeout` (60 s). With a proxy that gives up sooner, lower +`--stream-heartbeat` below its timeout. + +The API may also be served under a path of its own (`location /continuum/ { +proxy_pass http://unix:/run/continuum/api.sock:/; }`): its redirects (`Location`), the dashboard's +requests and the Swagger UI's are relative, so they stay under it. + ## What to alert on From the metrics (`GET /admin/stats` as Prometheus, [`measurements.md`](measurements.md)): |
