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:
|
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/...`.
|
2. Client connects to `matrix.<hive>/_matrix/client/...`.
|
||||||
3. Gateway routes `/_matrix/*` → tuwunel at `127.0.0.1:8008`.
|
3. Gateway routes `/_matrix/*` → tuwunel at `127.0.0.1:8008`.
|
||||||
|
|
||||||
|
|
@ -164,14 +164,20 @@ frontend-side derivation.
|
||||||
|
|
||||||
## TLS modes
|
## 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 |
|
| mode | config | cert source | `.well-known` scheme |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| self-signed (default) | `selfSignedTls = true` | host hive-CA signs a gateway leaf (RSA-4096) | `https` |
|
| self-signed (default) | neither `tls.certDir` nor `tls.acme` set | 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` |
|
| ACME (Let's Encrypt) | `tls.acme.enable = true` | nginx inside container via HTTP-01 | `https` |
|
||||||
| operator cert | `selfSignedTls = false` + `tls.certDir` set | bind-mounted from host | `https` |
|
| operator cert | `tls.certDir` set | bind-mounted from host | `https` |
|
||||||
| http-only | `selfSignedTls = false`, no `tls.certDir`, no `tls.acme` | none | `http` |
|
|
||||||
|
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`)
|
### ACME / Let's Encrypt (`tls.acme`)
|
||||||
|
|
||||||
|
|
@ -179,7 +185,6 @@ Simplest production path for operators with a public domain:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
services.hyperhive.gateway = {
|
services.hyperhive.gateway = {
|
||||||
selfSignedTls = false;
|
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
tls.acme = {
|
tls.acme = {
|
||||||
enable = true;
|
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.
|
**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`.
|
**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).
|
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
|
```nix
|
||||||
services.hyperhive.gateway = {
|
services.hyperhive.gateway = {
|
||||||
selfSignedTls = false;
|
|
||||||
tls.certDir = "/var/lib/acme/example.com"; # nixpkgs security.acme output dir
|
tls.certDir = "/var/lib/acme/example.com"; # nixpkgs security.acme output dir
|
||||||
# tls.certName = "cert.pem"; # default — matches security.acme layout
|
# tls.certName = "cert.pem"; # default — matches security.acme layout
|
||||||
# tls.keyName = "key.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.
|
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:
|
**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
|
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)
|
## 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
|
`127.0.0.1:<port>` internally — leaving the per-agent ports
|
||||||
firewall-open would defeat the single-front-door story.
|
firewall-open would defeat the single-front-door story.
|
||||||
|
|
||||||
`services.hyperhive.gateway.openFirewall = true` opens `port` plus
|
`services.hyperhive.gateway.openFirewall = true` opens both `port` and
|
||||||
`httpsPort` when `selfSignedTls = true` (default). Operators who
|
`httpsPort` — the gateway always terminates TLS (self-signed floor), so
|
||||||
flip `selfSignedTls = false` to front the gateway with a real
|
both are always served.
|
||||||
TLS-terminating reverse proxy on the host get only `port` opened.
|
|
||||||
|
|
||||||
The manager hashes into the same port range as sub-agents (no
|
The manager hashes into the same port range as sub-agents (no
|
||||||
"manager pinned at 8000" special case), so one range opening covers
|
"manager pinned at 8000" special case), so one range opening covers
|
||||||
|
|
@ -358,9 +369,9 @@ covers most cases:
|
||||||
| `behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
|
| `behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
|
||||||
|
|
||||||
The auto-derivation always uses `http://`. Set `rootUrl` explicitly when
|
The auto-derivation always uses `http://`. Set `rootUrl` explicitly when
|
||||||
you need `https://` (e.g. behind a TLS-terminating reverse proxy or when
|
you need `https://` (e.g. behind a TLS-terminating reverse proxy, or when
|
||||||
`selfSignedTls = true` and clone URLs must carry `https://`), or when
|
clone URLs must carry `https://` because the gateway terminates TLS), or
|
||||||
`forge.domain` resolves differently from the public URL. Must end with
|
when `forge.domain` resolves differently from the public URL. Must end with
|
||||||
`/` (Forgejo requirement; an assertion enforces this).
|
`/` (Forgejo requirement; an assertion enforces this).
|
||||||
|
|
||||||
## Per-agent static frontend split
|
## 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
|
enabling it on a deployment that later loses TLS locks browsers out
|
||||||
until `max-age` expires. Only enable when TLS is permanent.
|
until `max-age` expires. Only enable when TLS is permanent.
|
||||||
|
|
||||||
**Assertion**: `hsts.enable = true` without a TLS mode configured
|
The gateway always terminates TLS now (self-signed floor), so HSTS is
|
||||||
(`selfSignedTls`, `tls.certDir`, or `tls.acme.enable`) is a NixOS
|
always served over https when enabled — the old "HSTS requires a TLS mode"
|
||||||
build-time assertion failure — HSTS over plain HTTP is harmless but
|
assertion is gone (it can no longer be violated).
|
||||||
almost always a misconfiguration.
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,14 @@ let
|
||||||
# forgejo `DOMAIN` setting AND the gateway vhost server-name, so
|
# forgejo `DOMAIN` setting AND the gateway vhost server-name, so
|
||||||
# ROOT_URL just uses it directly (dropping the port suffix on the
|
# ROOT_URL just uses it directly (dropping the port suffix on the
|
||||||
# canonical port for the scheme — 80 for http, 443 for https). The
|
# canonical port for the scheme — 80 for http, 443 for https). The
|
||||||
# scheme + port follow what the gateway actually serves: `https` when
|
# gateway always terminates TLS now — self-signed is the implicit floor
|
||||||
# the gateway terminates TLS (a self-signed cert or an external
|
# when neither `tls.certDir` nor ACME is configured — so behind the
|
||||||
# `tls.certDir`), `http` otherwise — advertising `http://` for a TLS
|
# gateway the forge is always advertised over `https` on `httpsPort`.
|
||||||
# gateway produces broken clone links + mixed-content redirects.
|
|
||||||
# When direct (gateway off or `behindGateway = false`), keep the
|
# When direct (gateway off or `behindGateway = false`), keep the
|
||||||
# host:httpPort shape so direct browser access still produces correct
|
# host:httpPort shape so direct browser access still produces correct
|
||||||
# links. Operators can still override via `cfg.rootUrl` for bespoke
|
# links. Operators can still override via `cfg.rootUrl` for bespoke
|
||||||
# shapes.
|
# shapes.
|
||||||
gatewayTls = gatewayCfg.selfSignedTls || gatewayCfg.tls.certDir != null;
|
gatewayTls = true;
|
||||||
defaultRootUrl =
|
defaultRootUrl =
|
||||||
if cfg.behindGateway then
|
if cfg.behindGateway then
|
||||||
let
|
let
|
||||||
|
|
@ -157,11 +156,11 @@ in
|
||||||
(default), `ROOT_URL` is derived from `cfg.domain` + gateway
|
(default), `ROOT_URL` is derived from `cfg.domain` + gateway
|
||||||
state, including the scheme:
|
state, including the scheme:
|
||||||
|
|
||||||
- `behindGateway = true` → `https://''${cfg.domain}/` when the
|
- `behindGateway = true` → `https://''${cfg.domain}/`. The gateway
|
||||||
gateway terminates TLS (`gateway.selfSignedTls = true` or
|
always terminates TLS (self-signed is the implicit floor when no
|
||||||
`gateway.tls.certDir` set), otherwise `http://''${cfg.domain}/`.
|
`gateway.tls.certDir` / ACME is set), so the forge is always
|
||||||
A non-canonical gateway port (`gateway.port` for http,
|
advertised over https. A non-canonical `gateway.httpsPort` is
|
||||||
`gateway.httpsPort` for https) is appended as `:<port>`.
|
appended as `:<port>`.
|
||||||
- `behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/`
|
- `behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/`
|
||||||
|
|
||||||
The TLS scheme is derived automatically now, so you only need to
|
The TLS scheme is derived automatically now, so you only need to
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,14 @@ let
|
||||||
forgeCfg = config.services.hyperhive.forge;
|
forgeCfg = config.services.hyperhive.forge;
|
||||||
networkCfg = config.services.hyperhive.network;
|
networkCfg = config.services.hyperhive.network;
|
||||||
|
|
||||||
|
# Self-signed TLS is the implicit floor: when neither an operator cert
|
||||||
|
# (`tls.certDir`) nor ACME (`tls.acme.enable`) is configured, the gateway
|
||||||
|
# generates + serves a hive-CA-signed leaf (see hive-tls.nix). There is no
|
||||||
|
# explicit toggle and no http-only mode — matrix discovery requires https,
|
||||||
|
# so the gateway always terminates TLS. The deprecated `selfSignedTls`
|
||||||
|
# option is a no-op kept only so existing configs eval (see warnings).
|
||||||
|
useSelfSigned = cfg.tls.certDir == null && !cfg.tls.acme.enable;
|
||||||
|
|
||||||
# Static error pages for `/agent/<name>/` mishaps.
|
# Static error pages for `/agent/<name>/` mishaps.
|
||||||
# Useful pages of nginx's default 404/502 for routes
|
# Useful pages of nginx's default 404/502 for routes
|
||||||
# we've already special-cased. See `docs/gateway.md::Per-agent
|
# we've already special-cased. See `docs/gateway.md::Per-agent
|
||||||
|
|
@ -152,24 +160,16 @@ in
|
||||||
default = true;
|
default = true;
|
||||||
example = false;
|
example = false;
|
||||||
description = ''
|
description = ''
|
||||||
Generate a self-signed TLS cert at first gateway boot and
|
**DEPRECATED — ignored.** Self-signed TLS is now the implicit
|
||||||
listen on `httpsPort` (default 443) with it on every vhost.
|
default: when neither `tls.certDir` nor `tls.acme.enable` is
|
||||||
On by default because matrix-dart-sdk (the SDK behind
|
configured, the gateway generates and serves a hive-CA-signed
|
||||||
FluffyChat + several other Matrix clients) hardcodes
|
leaf (see the `hive-tls` module). There is no explicit toggle and
|
||||||
`https://<host>/.well-known/matrix/client` for homeserver
|
no http-only mode — matrix discovery hardcodes
|
||||||
discovery and refuses to fall back to plain http — without
|
`https://<host>/.well-known/matrix/client`, so the gateway always
|
||||||
TLS the browser client just won't connect.
|
terminates TLS. This option is retained as a no-op so existing
|
||||||
|
configs eval; setting it (to either value) warns and has no
|
||||||
Self-signed means browsers will show a "not secure" warning
|
effect, and it will be removed in a future release. Use
|
||||||
on first visit; the operator clicks through once per
|
`tls.certDir` or `tls.acme` to override the self-signed default.
|
||||||
browser. For production deployments, set this to `false`
|
|
||||||
and front the gateway with a reverse proxy (caddy, traefik,
|
|
||||||
or nginx with ACME) that does proper TLS termination.
|
|
||||||
|
|
||||||
The cert is regenerated on demand if the file is missing
|
|
||||||
but never rotated automatically; delete
|
|
||||||
`/var/lib/hive-gateway/tls/cert.pem` inside the gateway
|
|
||||||
container to force a fresh one.
|
|
||||||
|
|
||||||
See `docs/gateway.md` ("Self-signed TLS").
|
See `docs/gateway.md` ("Self-signed TLS").
|
||||||
'';
|
'';
|
||||||
|
|
@ -180,10 +180,10 @@ in
|
||||||
default = 443;
|
default = 443;
|
||||||
example = 8443;
|
example = 8443;
|
||||||
description = ''
|
description = ''
|
||||||
TCP port for the TLS-terminated vhosts. Active when
|
TCP port for the TLS-terminated vhosts. Default 443. The gateway
|
||||||
`selfSignedTls = true` OR `tls.certDir` is set. Default 443.
|
always terminates TLS (self-signed is the implicit floor when no
|
||||||
Setting `selfSignedTls = false` and leaving `tls.certDir = null`
|
`tls.certDir` / ACME is configured), so this port is always active
|
||||||
renders this inert (the gateway listens on `port` only).
|
alongside the plain-http `port`.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -195,8 +195,8 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Path to a host directory containing a TLS certificate and
|
Path to a host directory containing a TLS certificate and
|
||||||
private key for nginx. When set, nginx listens on `httpsPort`
|
private key for nginx. When set, nginx listens on `httpsPort`
|
||||||
and uses this cert, making `selfSignedTls` unnecessary —
|
and uses this cert, overriding the self-signed default — the
|
||||||
the auto-generated self-signed cert is skipped entirely.
|
auto-generated hive-CA-signed leaf is skipped entirely.
|
||||||
|
|
||||||
The directory is bind-mounted read-only into the gateway
|
The directory is bind-mounted read-only into the gateway
|
||||||
container at `/run/hive-tls/`. nginx reads
|
container at `/run/hive-tls/`. nginx reads
|
||||||
|
|
@ -209,15 +209,14 @@ in
|
||||||
security.acme.certs."example.com" = { ... };
|
security.acme.certs."example.com" = { ... };
|
||||||
services.hyperhive.gateway.tls.certDir =
|
services.hyperhive.gateway.tls.certDir =
|
||||||
config.security.acme.certs."example.com".directory;
|
config.security.acme.certs."example.com".directory;
|
||||||
services.hyperhive.gateway.selfSignedTls = false;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When using an external CA cert, peer hives can declare this
|
When using an external CA cert, peer hives can declare this
|
||||||
hive in `services.hyperhive.swarm.peers` without
|
hive in `services.hyperhive.swarm.peers` without
|
||||||
`certFingerprint` — the standard CA bundle validates.
|
`certFingerprint` — the standard CA bundle validates.
|
||||||
|
|
||||||
Mutual exclusion: `selfSignedTls = true` and `tls.certDir`
|
Mutual exclusion with `tls.acme.enable` — set one or the other,
|
||||||
set together fails an assertion at eval time.
|
not both.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -258,13 +257,12 @@ in
|
||||||
Let's Encrypt can reach `/.well-known/acme-challenge/`.
|
Let's Encrypt can reach `/.well-known/acme-challenge/`.
|
||||||
- `tls.acme.email` must be set (ACME account contact).
|
- `tls.acme.email` must be set (ACME account contact).
|
||||||
|
|
||||||
Mutual exclusion: `selfSignedTls = true` or `tls.certDir`
|
Mutual exclusion: `tls.certDir` set together with
|
||||||
set together with `tls.acme.enable = true` fails at eval.
|
`tls.acme.enable = true` fails at eval — pick one TLS source.
|
||||||
|
|
||||||
Typical setup:
|
Typical setup:
|
||||||
```nix
|
```nix
|
||||||
services.hyperhive.gateway = {
|
services.hyperhive.gateway = {
|
||||||
selfSignedTls = false;
|
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
tls.acme = {
|
tls.acme = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
@ -332,10 +330,10 @@ in
|
||||||
will lock browsers out until the max-age expires. Only enable
|
will lock browsers out until the max-age expires. Only enable
|
||||||
this when you are certain TLS is permanent.
|
this when you are certain TLS is permanent.
|
||||||
|
|
||||||
Requires TLS to be active (`selfSignedTls = true`, a `tls.certDir`,
|
The gateway always terminates TLS now (self-signed floor), so
|
||||||
or `tls.acme.enable = true`). Enabling HSTS without TLS is
|
HSTS is always served over https when enabled — but mind the
|
||||||
technically harmless (browsers ignore the header over plain HTTP)
|
warning above: HSTS pins https in the browser, so only enable it
|
||||||
but is almost certainly a misconfiguration.
|
when TLS is permanent for this deployment.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -374,23 +372,6 @@ in
|
||||||
or leave `localHostsEntry` at its default of false.
|
or leave `localHostsEntry` at its default of false.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
{
|
|
||||||
assertion = !(cfg.selfSignedTls && cfg.tls.certDir != null);
|
|
||||||
message = ''
|
|
||||||
services.hyperhive.gateway.selfSignedTls = true and
|
|
||||||
services.hyperhive.gateway.tls.certDir are mutually exclusive.
|
|
||||||
Set `selfSignedTls = false` when providing an external cert
|
|
||||||
via `tls.certDir`.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = !(cfg.tls.acme.enable && cfg.selfSignedTls);
|
|
||||||
message = ''
|
|
||||||
services.hyperhive.gateway.tls.acme.enable = true and
|
|
||||||
selfSignedTls = true are mutually exclusive.
|
|
||||||
Set `selfSignedTls = false` when using ACME.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
assertion = !(cfg.tls.acme.enable && cfg.tls.certDir != null);
|
assertion = !(cfg.tls.acme.enable && cfg.tls.certDir != null);
|
||||||
message = ''
|
message = ''
|
||||||
|
|
@ -406,16 +387,22 @@ in
|
||||||
Let's Encrypt needs a contact address for the ACME account.
|
Let's Encrypt needs a contact address for the ACME account.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
{
|
|
||||||
assertion = !cfg.hsts.enable || cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable;
|
|
||||||
message = ''
|
|
||||||
services.hyperhive.gateway.hsts.enable = true requires TLS to be
|
|
||||||
configured (selfSignedTls, tls.certDir, or tls.acme.enable). HSTS
|
|
||||||
over plain HTTP is ignored by browsers and indicates a config error.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Deprecation surface for the removed `selfSignedTls` toggle. Self-signed
|
||||||
|
# is now the implicit floor (used whenever neither `tls.certDir` nor
|
||||||
|
# `tls.acme` is set), so the toggle no longer does anything. Warn only
|
||||||
|
# when it's set to `false` — that's the case that previously meant
|
||||||
|
# "http-only / external-only", which no longer exists; `= true` matches
|
||||||
|
# the effective behaviour and stays silent to avoid noise.
|
||||||
|
warnings = lib.optional (!cfg.selfSignedTls) ''
|
||||||
|
services.hyperhive.gateway.selfSignedTls = false is deprecated and
|
||||||
|
ignored — self-signed TLS is now the default whenever no other TLS
|
||||||
|
source is configured, and there is no http-only mode. Remove the
|
||||||
|
setting; configure `tls.certDir` or `tls.acme` to override the
|
||||||
|
self-signed default.
|
||||||
|
'';
|
||||||
|
|
||||||
# Ensure bind-mount sources exist at host boot before the gateway
|
# Ensure bind-mount sources exist at host boot before the gateway
|
||||||
# container's first start. nspawn would auto-create missing dirs
|
# container's first start. nspawn would auto-create missing dirs
|
||||||
# tmpfiles rules make the intent explicit
|
# tmpfiles rules make the intent explicit
|
||||||
|
|
@ -472,9 +459,9 @@ in
|
||||||
isReadOnly = true;
|
isReadOnly = true;
|
||||||
};
|
};
|
||||||
# Operator-provided TLS cert dir (e.g. Let's Encrypt / ACME).
|
# Operator-provided TLS cert dir (e.g. Let's Encrypt / ACME).
|
||||||
# Only mounted when `tls.certDir` is set; mutually exclusive with
|
# Only mounted when `tls.certDir` is set; when it is, the self-signed
|
||||||
# `selfSignedTls = true` (assertion above). nginx reads cert +
|
# floor is off (so the `/run/hive-ca` mount above is absent). nginx
|
||||||
# key from `/run/hive-tls/<certName>` and `/run/hive-tls/<keyName>`.
|
# reads cert + key from `/run/hive-tls/<certName>` and `<keyName>`.
|
||||||
bindMounts."/run/hive-tls" = lib.mkIf (cfg.tls.certDir != null) {
|
bindMounts."/run/hive-tls" = lib.mkIf (cfg.tls.certDir != null) {
|
||||||
hostPath = cfg.tls.certDir;
|
hostPath = cfg.tls.certDir;
|
||||||
isReadOnly = true;
|
isReadOnly = true;
|
||||||
|
|
@ -484,7 +471,7 @@ in
|
||||||
# Bind-mount that dir read-only so the in-container import service
|
# Bind-mount that dir read-only so the in-container import service
|
||||||
# (below) can copy the leaf into nginx's state dir with the right
|
# (below) can copy the leaf into nginx's state dir with the right
|
||||||
# owner/mode. Source files: `gateway.pem` + `gateway-key.pem`.
|
# owner/mode. Source files: `gateway.pem` + `gateway-key.pem`.
|
||||||
bindMounts."/run/hive-ca" = lib.mkIf cfg.selfSignedTls {
|
bindMounts."/run/hive-ca" = lib.mkIf useSelfSigned {
|
||||||
hostPath = config.services.hyperhive.tls.stateDir;
|
hostPath = config.services.hyperhive.tls.stateDir;
|
||||||
isReadOnly = true;
|
isReadOnly = true;
|
||||||
};
|
};
|
||||||
|
|
@ -493,14 +480,18 @@ in
|
||||||
let
|
let
|
||||||
tlsDir = "/var/lib/hive-gateway/tls";
|
tlsDir = "/var/lib/hive-gateway/tls";
|
||||||
# TLS cert + key paths inside the container.
|
# TLS cert + key paths inside the container.
|
||||||
# - selfSignedTls=true: generated cert stored in persistent state dir.
|
# - self-signed (default): imported hive-CA-signed leaf in the
|
||||||
|
# persistent state dir.
|
||||||
# - tls.certDir set: operator-provided cert bind-mounted at /run/hive-tls.
|
# - tls.certDir set: operator-provided cert bind-mounted at /run/hive-tls.
|
||||||
tlsCert =
|
tlsCert =
|
||||||
if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.certName}" else "${tlsDir}/cert.pem";
|
if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.certName}" else "${tlsDir}/cert.pem";
|
||||||
tlsKey =
|
tlsKey =
|
||||||
if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.keyName}" else "${tlsDir}/key.pem";
|
if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.keyName}" else "${tlsDir}/key.pem";
|
||||||
# True when nginx should listen with TLS (any mode).
|
# The gateway always terminates TLS now: self-signed is the
|
||||||
hasTls = cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable;
|
# implicit floor (`useSelfSigned`) when neither `tls.certDir` nor
|
||||||
|
# ACME is set, so there is no http-only mode. Kept as a named
|
||||||
|
# binding for the vhost listen/ssl wiring below.
|
||||||
|
hasTls = true;
|
||||||
# Listen addresses every vhost shares. Plain http on `cfg.port`
|
# Listen addresses every vhost shares. Plain http on `cfg.port`
|
||||||
# always; `cfg.httpsPort` with TLS sits beside it when TLS is
|
# always; `cfg.httpsPort` with TLS sits beside it when TLS is
|
||||||
# active (any mode). See `docs/gateway.md` ("TLS modes").
|
# active (any mode). See `docs/gateway.md` ("TLS modes").
|
||||||
|
|
@ -797,7 +788,7 @@ in
|
||||||
# hive domain plus `forge.`, `matrix.` and `*.${hyperhiveDomain}`
|
# hive domain plus `forge.`, `matrix.` and `*.${hyperhiveDomain}`
|
||||||
# so all sub-domains validate under the same cert + the hive CA.
|
# so all sub-domains validate under the same cert + the hive CA.
|
||||||
# See `docs/gateway.md` ("Self-signed TLS").
|
# See `docs/gateway.md` ("Self-signed TLS").
|
||||||
systemd.services.hive-gateway-self-signed-cert = lib.mkIf cfg.selfSignedTls {
|
systemd.services.hive-gateway-self-signed-cert = lib.mkIf useSelfSigned {
|
||||||
description = "Import host-generated TLS leaf for hive-gateway";
|
description = "Import host-generated TLS leaf for hive-gateway";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
before = [ "nginx.service" ];
|
before = [ "nginx.service" ];
|
||||||
|
|
@ -986,8 +977,10 @@ in
|
||||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [
|
||||||
cfg.port
|
cfg.port
|
||||||
]
|
# The gateway always terminates TLS now (self-signed floor), so
|
||||||
++ lib.optional (cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable) cfg.httpsPort;
|
# `httpsPort` is always opened alongside the plain-http `port`.
|
||||||
|
cfg.httpsPort
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# `/etc/hosts` entries for local dev — bare hive domain + any
|
# `/etc/hosts` entries for local dev — bare hive domain + any
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@ let
|
||||||
|
|
||||||
# The host-managed hive CA is the trust anchor for self-signed mode.
|
# The host-managed hive CA is the trust anchor for self-signed mode.
|
||||||
# It is only stood up when the gateway actually serves a self-signed
|
# It is only stood up when the gateway actually serves a self-signed
|
||||||
# cert: a domain must be set (the leaf SANs derive from it) and
|
# cert: a domain must be set (the leaf SANs derive from it) and the
|
||||||
# `gateway.selfSignedTls` must be on. With an operator-supplied
|
# gateway must be in self-signed mode — i.e. neither an operator cert
|
||||||
# `tls.certDir` or ACME the public/operator CA already validates, so
|
# (`tls.certDir`) nor ACME is configured. With either of those the
|
||||||
# the hive CA is unnecessary.
|
# public/operator CA already validates, so the hive CA is unnecessary.
|
||||||
active = hyperhiveCfg.enable && gatewayCfg.selfSignedTls && domain != null;
|
useSelfSigned = gatewayCfg.tls.certDir == null && !gatewayCfg.tls.acme.enable;
|
||||||
|
active = hyperhiveCfg.enable && useSelfSigned && domain != null;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Host-side TLS trust root for the self-signed gateway mode.
|
# Host-side TLS trust root for the self-signed gateway mode.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue