From e4331d13f313f439aff9cef1ea221c8adeeebb3a Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 2 Nov 2026 22:46:48 +0100 Subject: api: allow listening to Unix sockets --- docs/example.toml | 14 +++++++++----- docs/operations.md | 54 +++++++++++++++++++++++++++++++++++++++++++++--------- docs/trust.md | 6 ++++-- 3 files changed, 58 insertions(+), 16 deletions(-) (limited to 'docs') diff --git a/docs/example.toml b/docs/example.toml index b6a6ad0..43fdaf1 100644 --- a/docs/example.toml +++ b/docs/example.toml @@ -52,8 +52,10 @@ node-key-file = "/var/lib/continuum/node.key" # --- The HTTP API ------------------------------------------------------------------------ -# Address of the HTTP API. Plain HTTP: to expose it, put a TLS proxy in front. -# bind-addr = "127.0.0.1:3000" +# Where the HTTP API listens: TCP addresses, and Unix sockets (unix:PATH, made readable +# and writable by the node's user and group only), as many as given. Plain HTTP: to +# expose it, put a TLS proxy in front. +# bind-addr = ["127.0.0.1:3000", "unix:/run/continuum/api.sock"] # Users of the HTTP API (an htpasswd file with Argon2 hashes). Required (but with # user-header, or relay-only: without either, no HTTP API). @@ -65,9 +67,11 @@ htpasswd = "/etc/continuum/htpasswd" # authentication. The proxy must set or clear it on every request. # user-header = "X-Remote-User" -# Addresses (an IP address, or a range as ADDRESS/BITS) of the reverse proxies whose -# user-header is believed. -# trusted-proxies = ["127.0.0.1", "::1"] +# Where the reverse proxies whose user-header is believed connect from: IP addresses (or +# ranges, as ADDRESS/BITS), and unix for the API's Unix sockets (bind-addr). On a host +# with other users, leave out the addresses and have the proxy use a Unix socket: others +# can connect over loopback too. +# trusted-proxies = ["127.0.0.1", "::1", "unix"] # HTTP API users (from htpasswd or user-header) who may write protocol data under /store/_continuum # (remake records, members, forget records), e.g. to repair a stuck remake. Such writes diff --git a/docs/operations.md b/docs/operations.md index 4f59208..da73fce 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -107,23 +107,59 @@ One node at a time, restarting each with its files. What to know: 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 the -proxy's address alone (`--user-header`, `--trusted-proxies`, by default this host's -loopback addresses); 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. With nginx: +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://127.0.0.1:3000; + 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 + } } ``` -and the node started with `--user-header X-Remote-User`. The names the proxy gives are -the users of `--protocol-writers` and of who wrote what ([`api.md`](api.md)). +(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. The dashboard's events have a keepalive every 15 s; `/stream` @@ -131,7 +167,7 @@ sends nothing while nothing changes, so nginx ends a quiet one after its `proxy_read_timeout` (60 s): raise that for it, or have clients reconnect. The API may also be served under a path of its own (`location /continuum/ { -proxy_pass http://127.0.0.1:3000/; }`): its redirects (`Location`), the dashboard's +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 diff --git a/docs/trust.md b/docs/trust.md index dc80045..165fc9b 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -36,8 +36,10 @@ it creates is). **The HTTP API.** Every endpoint but the API description needs a user from `--htpasswd`, or one a reverse proxy names (`--user-header`): the node believes that header from `--trusted-proxies` alone, but from those blindly, so whoever can send -requests from such an address (another user of the same host, with the default -loopback addresses) can be any user. It listens on `127.0.0.1:3000` by default (`--bind-addr`) and speaks +requests from there can be any user. With the default loopback addresses, that is every +user of the host; with only the API's Unix socket (`--trusted-proxies unix`), those its +file's permissions let in: the node's user and group ([`operations.md`](operations.md), Behind a +reverse proxy). It listens on `127.0.0.1:3000` by default (`--bind-addr`) and speaks plain HTTP: to expose it, put a TLS proxy in front. All users may read and write user data and use the admin endpoints (remakes, forgetting peers, exports, log filters). There are no read-only users: whoever may open the dashboard may also edit and delete -- cgit v1.3.1