nix(gateway): self-signed TLS as the implicit default, deprecate the toggle
Make self-signed TLS the gateway's default whenever no external TLS source is configured, and deprecate the explicit selfSignedTls toggle. Self-signed is now derived as `tls.certDir == null && !tls.acme.enable`, so an operator selects a TLS mode by setting tls.certDir or tls.acme — or neither, for the self-signed default. There is no http-only mode: matrix discovery hardcodes https, so the gateway always terminates TLS. The selfSignedTls option is kept as a deprecated no-op (warns when set to false) so existing configs still eval. The two selfSignedTls mutual- exclusion assertions and the HSTS-requires-TLS assertion are dropped — they are impossible or vacuous now that self-signed is the floor. The hive-tls module and the forge ROOT_URL scheme consume the derived value: the gateway always terminates TLS, so behind the gateway the forge is always advertised over https. Updates docs/gateway.md (TLS-modes table, self-signed section, the removed http-only section, firewall + discovery notes). Eval-proven: default → self-signed (hive CA active, https ROOT_URL); tls.certDir → CA inactive; selfSignedTls=false → deprecation warning fires.
This commit is contained in:
parent
0b4c2419f5
commit
1a3f82a459
4 changed files with 112 additions and 109 deletions
|
|
@ -21,7 +21,7 @@ Per-agent UIs stay sub-path because they're hyperhive-internal and base-path-awa
|
|||
|
||||
Operator points client at `<hive>`. Sequence:
|
||||
|
||||
1. Client fetches `https://<hive>/.well-known/matrix/client` → `{"m.homeserver":{"base_url":"https://matrix.<hive>"}}` (no port suffix when gateway listens on 443). With `selfSignedTls = false` the scheme drops to http and the port suffix reflects the bare `port` instead.
|
||||
1. Client fetches `https://<hive>/.well-known/matrix/client` → `{"m.homeserver":{"base_url":"https://matrix.<hive>"}}` (no port suffix when gateway listens on 443). The gateway always terminates TLS, so the scheme is always `https`; a non-default `httpsPort` is reflected as the port suffix.
|
||||
2. Client connects to `matrix.<hive>/_matrix/client/...`.
|
||||
3. Gateway routes `/_matrix/*` → tuwunel at `127.0.0.1:8008`.
|
||||
|
||||
|
|
@ -164,14 +164,20 @@ frontend-side derivation.
|
|||
|
||||
## TLS modes
|
||||
|
||||
Four modes:
|
||||
The gateway always terminates TLS — self-signed is the implicit floor when
|
||||
nothing else is configured, so there is no http-only mode. Three modes,
|
||||
selected by which (if any) external TLS source is set:
|
||||
|
||||
| mode | config | cert source | `.well-known` scheme |
|
||||
|---|---|---|---|
|
||||
| self-signed (default) | `selfSignedTls = true` | host hive-CA signs a gateway leaf (RSA-4096) | `https` |
|
||||
| ACME (Let's Encrypt) | `selfSignedTls = false` + `tls.acme.enable = true` | nginx inside container via HTTP-01 | `https` |
|
||||
| operator cert | `selfSignedTls = false` + `tls.certDir` set | bind-mounted from host | `https` |
|
||||
| http-only | `selfSignedTls = false`, no `tls.certDir`, no `tls.acme` | none | `http` |
|
||||
| self-signed (default) | neither `tls.certDir` nor `tls.acme` set | host hive-CA signs a gateway leaf (RSA-4096) | `https` |
|
||||
| ACME (Let's Encrypt) | `tls.acme.enable = true` | nginx inside container via HTTP-01 | `https` |
|
||||
| operator cert | `tls.certDir` set | bind-mounted from host | `https` |
|
||||
|
||||
The `gateway.selfSignedTls` option is **deprecated and ignored** — self-signed
|
||||
is now derived from the absence of `tls.certDir` / `tls.acme`. Setting it to
|
||||
`false` (which used to select http-only or force an external cert) warns and
|
||||
has no effect; use `tls.certDir` / `tls.acme` to override the default.
|
||||
|
||||
### ACME / Let's Encrypt (`tls.acme`)
|
||||
|
||||
|
|
@ -179,7 +185,6 @@ Simplest production path for operators with a public domain:
|
|||
|
||||
```nix
|
||||
services.hyperhive.gateway = {
|
||||
selfSignedTls = false;
|
||||
openFirewall = true;
|
||||
tls.acme = {
|
||||
enable = true;
|
||||
|
|
@ -192,11 +197,11 @@ nginx inside the gateway container obtains and auto-renews certs via the ACME HT
|
|||
|
||||
**Requirements**: `services.hyperhive.domain` must be publicly DNS-resolvable to this host, and `openFirewall = true` so Let's Encrypt can reach `/.well-known/acme-challenge/`. Each active vhost (main domain, `forge.<domain>`, `matrix.<domain>`) gets its own cert via separate ACME challenges.
|
||||
|
||||
Mutual exclusion: `selfSignedTls = true` or `tls.certDir` set together with `tls.acme.enable = true` fails an assertion.
|
||||
Mutual exclusion: `tls.certDir` set together with `tls.acme.enable = true` fails an assertion — pick one external TLS source (or neither, for the self-signed default).
|
||||
|
||||
**Swarm peers**: CA-signed certs are trusted by default — remote hives need no `certFingerprint` in `swarm.peers`.
|
||||
|
||||
### Self-signed TLS (`selfSignedTls`)
|
||||
### Self-signed TLS (default)
|
||||
|
||||
On by default, and listens on `httpsPort` (default 443) on every vhost beside the plain-http `port` (default 80).
|
||||
|
||||
|
|
@ -218,7 +223,6 @@ For operators with a real CA cert (Let's Encrypt, corporate CA, etc.):
|
|||
|
||||
```nix
|
||||
services.hyperhive.gateway = {
|
||||
selfSignedTls = false;
|
||||
tls.certDir = "/var/lib/acme/example.com"; # nixpkgs security.acme output dir
|
||||
# tls.certName = "cert.pem"; # default — matches security.acme layout
|
||||
# tls.keyName = "key.pem"; # default — matches security.acme layout
|
||||
|
|
@ -227,7 +231,7 @@ services.hyperhive.gateway = {
|
|||
|
||||
The directory is bind-mounted read-only into the gateway container at `/run/hive-tls/`. nginx uses `cert.pem` + `key.pem` (override `tls.certName`/`tls.keyName` for different filenames). Both modes listen on `httpsPort` (default 443) and emit `https://` in `.well-known` responses.
|
||||
|
||||
`selfSignedTls = true` and `tls.certDir` set together is an assertion error.
|
||||
`tls.certDir` and `tls.acme.enable` set together is an assertion error.
|
||||
|
||||
**Key file permissions**: nixpkgs's `security.acme` outputs private keys as `0640 root:acme` by default. nginx inside the gateway container runs as the `nginx` user and cannot read a key with that ownership. Fix with:
|
||||
|
||||
|
|
@ -243,11 +247,19 @@ or make the key world-readable (`0644`) if your threat model allows it. nginx er
|
|||
services.hyperhive.swarm.peers."example.com" = { }; # no certFingerprint needed
|
||||
```
|
||||
|
||||
### HTTP-only (`selfSignedTls = false`, no `tls.certDir`)
|
||||
### Fronting with an external TLS terminator
|
||||
|
||||
For operators who front the gateway with an external TLS terminator (caddy, traefik, nginx + ACME on the host). The gateway listens on `port` (default 80) only. `.well-known/matrix` responses use `http://`, which breaks matrix-dart-sdk discovery — acceptable when matrix GUI is off or the external proxy handles the `.well-known` redirect.
|
||||
There is no http-only mode: the gateway always terminates TLS (self-signed
|
||||
floor). Two paths for an operator who wants their own TLS terminator:
|
||||
|
||||
**`.well-known/matrix/{client,server}` scheme**: `https` when TLS is active (either mode), `http` when http-only. The scheme must match what clients see at the external hostname.
|
||||
- give the gateway the real cert via `tls.certDir` (or `tls.acme`) so it
|
||||
serves proper TLS directly — no separate proxy needed; or
|
||||
- front it over a **unix socket** rather than a plain-http TCP port (the
|
||||
intended direction for "bring your own proxy" — the gateway is not meant
|
||||
to expose an unencrypted TCP upstream).
|
||||
|
||||
**`.well-known/matrix/{client,server}` scheme** is always `https` now — the
|
||||
gateway always terminates TLS, so discovery responses always advertise https.
|
||||
|
||||
## Firewall posture (host-level)
|
||||
|
||||
|
|
@ -258,10 +270,9 @@ For operators who front the gateway with an external TLS terminator (caddy, trae
|
|||
`127.0.0.1:<port>` internally — leaving the per-agent ports
|
||||
firewall-open would defeat the single-front-door story.
|
||||
|
||||
`services.hyperhive.gateway.openFirewall = true` opens `port` plus
|
||||
`httpsPort` when `selfSignedTls = true` (default). Operators who
|
||||
flip `selfSignedTls = false` to front the gateway with a real
|
||||
TLS-terminating reverse proxy on the host get only `port` opened.
|
||||
`services.hyperhive.gateway.openFirewall = true` opens both `port` and
|
||||
`httpsPort` — the gateway always terminates TLS (self-signed floor), so
|
||||
both are always served.
|
||||
|
||||
The manager hashes into the same port range as sub-agents (no
|
||||
"manager pinned at 8000" special case), so one range opening covers
|
||||
|
|
@ -358,9 +369,9 @@ covers most cases:
|
|||
| `behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
|
||||
|
||||
The auto-derivation always uses `http://`. Set `rootUrl` explicitly when
|
||||
you need `https://` (e.g. behind a TLS-terminating reverse proxy or when
|
||||
`selfSignedTls = true` and clone URLs must carry `https://`), or when
|
||||
`forge.domain` resolves differently from the public URL. Must end with
|
||||
you need `https://` (e.g. behind a TLS-terminating reverse proxy, or when
|
||||
clone URLs must carry `https://` because the gateway terminates TLS), or
|
||||
when `forge.domain` resolves differently from the public URL. Must end with
|
||||
`/` (Forgejo requirement; an assertion enforces this).
|
||||
|
||||
## Per-agent static frontend split
|
||||
|
|
@ -571,8 +582,7 @@ header is added alongside the other security headers.
|
|||
enabling it on a deployment that later loses TLS locks browsers out
|
||||
until `max-age` expires. Only enable when TLS is permanent.
|
||||
|
||||
**Assertion**: `hsts.enable = true` without a TLS mode configured
|
||||
(`selfSignedTls`, `tls.certDir`, or `tls.acme.enable`) is a NixOS
|
||||
build-time assertion failure — HSTS over plain HTTP is harmless but
|
||||
almost always a misconfiguration.
|
||||
The gateway always terminates TLS now (self-signed floor), so HSTS is
|
||||
always served over https when enabled — the old "HSTS requires a TLS mode"
|
||||
assertion is gone (it can no longer be violated).
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue