Follow-up to the plumbing removal: docs/swarm/README.md gets the biggest rewrite (drops the whole "Fingerprint format" section, fixes the runtime-effects list, the WireGuard config example + "what the mesh does" bullet), docs/gateway.md and hive-gateway/options.nix drop 4 "needs no certFingerprint" mentions, swarm-peers-removed.nix's migration-warning text no longer tells an upgrading operator to carry a field over that no longer exists, swarm.nix/swarm-wireguard.nix/ swarm-controller.nix/swarm-controller's main.rs get comment fixes where they described the now-removed HYPERHIVE_PEERS shape. Also caught one more stale "peer hives" mention in docs/web-ui/README.md's SW4RM tab description that the first pass on this issue missed.
399 lines
15 KiB
Nix
399 lines
15 KiB
Nix
# Option declarations for `services.hyperhive.gateway.*`. The gateway
|
||
# is always run alongside hyperhive (it's the single nginx in front of
|
||
# every surface and the only thing exposed to the outside); there is
|
||
# no enable flag. An operator who wants their own reverse proxy in
|
||
# front points it at the gateway's `port`.
|
||
{
|
||
lib,
|
||
config,
|
||
...
|
||
}:
|
||
let
|
||
cfg = config.services.hyperhive.gateway;
|
||
in
|
||
{
|
||
imports = [
|
||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "gateway" "selfSignedTls" ] ''
|
||
Self-signed TLS is the implicit default whenever neither
|
||
tls.certDir nor tls.acme is configured, and there is no http-only
|
||
mode. Remove the setting; configure `tls.certDir` or `tls.acme`
|
||
to override the self-signed default.
|
||
'')
|
||
];
|
||
|
||
options.services.hyperhive.gateway = {
|
||
port = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 80;
|
||
example = 8080;
|
||
description = ''
|
||
TCP port the gateway listens on. Default 80 (canonical web
|
||
port). nginx runs on the host as root, so it can bind <1024;
|
||
if 80 is already taken on the host (existing nginx, traefik,
|
||
etc.) override to an unused port like 8080 or move the
|
||
conflicting service.
|
||
'';
|
||
};
|
||
|
||
upstreamHost = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "127.0.0.1";
|
||
description = ''
|
||
Host the gateway proxies non-static requests to. Defaults to
|
||
`127.0.0.1` because nginx runs on the host itself, so loopback
|
||
resolves directly to hive-c0re.
|
||
'';
|
||
};
|
||
|
||
upstreamPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 7000;
|
||
description = ''
|
||
TCP port the gateway proxies non-static requests to. Defaults
|
||
to `7000` (hive-c0re's out-of-the-box dashboard port). Operators
|
||
who change `services.hyperhive.c0re.dashboardPort` should set
|
||
`upstreamPort` to match — kept as a hardcoded default rather
|
||
than a cross-reference to keep this module's options eval
|
||
independent of c0re's option tree shape.
|
||
'';
|
||
};
|
||
|
||
openFirewall = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = false;
|
||
example = true;
|
||
description = ''
|
||
Open `port` in the host firewall. Off by default (secure-by-default).
|
||
Flip to `true` to expose the gateway to
|
||
the operator's browser / external clients — required for any
|
||
out-of-host reach, since the agents themselves talk to
|
||
hive-c0re via the per-agent unix sockets and don't need the
|
||
nginx vhost. Leave off when running behind another reverse
|
||
proxy (e.g. caddy / traefik on the host) that handles TLS
|
||
termination + forwards to `port`.
|
||
|
||
**Note**: this used to default to `true`. Add
|
||
`services.hyperhive.gateway.openFirewall = true;` to your host
|
||
config if external reach stopped working after a recent upgrade.
|
||
'';
|
||
};
|
||
|
||
localHostsEntry = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = false;
|
||
example = true;
|
||
description = ''
|
||
Add an `/etc/hosts` entry mapping `services.hyperhive.domain`
|
||
to `127.0.0.1` on the host. Useful for local deployments +
|
||
tests where there's no real DNS for `services.hyperhive.domain`
|
||
but the operator (or browser-based tests) want to hit
|
||
`http://''${services.hyperhive.domain}` to exercise the
|
||
gateway shape. Off by default — operators running with real
|
||
DNS shouldn't have a stale `/etc/hosts` entry sticking
|
||
around. Requires `services.hyperhive.domain` to be set.
|
||
|
||
`services.hyperhive.enableAllLocalDefaults` turns this on as
|
||
part of saying "this box is the whole deployment": that mode
|
||
means there is no real DNS for these names and the operator is
|
||
browsing them from the host itself. Set it here explicitly to
|
||
override in either direction.
|
||
'';
|
||
};
|
||
|
||
localNames = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
default = [ ];
|
||
internal = true;
|
||
description = ''
|
||
Extra hostnames the hive's resolver answers with the bridge IP,
|
||
contributed by the modules that own those names.
|
||
|
||
A service module says **which name**; the gateway decides
|
||
**where it points** — the same split as `lib.tlsFor`. A service
|
||
that hardcoded the bridge IP would be one more place to fix when
|
||
the network layout changes, and it has no business knowing it.
|
||
|
||
⚠️ Contribute a name only when THIS host actually serves it. The
|
||
list is not "names the swarm has" —
|
||
`services.hyperhive.swarm.serviceDomains` is that, and it is
|
||
deliberately broader (it drives certificate issuance, so it
|
||
includes names this hive may only be a client of). Publishing an
|
||
address record for a service you do not run points every agent
|
||
on the bridge at a door that isn't there.
|
||
'';
|
||
};
|
||
|
||
lib = {
|
||
listen = lib.mkOption {
|
||
type = lib.types.listOf (lib.types.attrsOf lib.types.raw);
|
||
internal = true;
|
||
readOnly = true;
|
||
description = ''
|
||
Read-only: the `listen` set every vhost in front of this
|
||
gateway shares (plain http on `port`, TLS on `httpsPort`).
|
||
Published so a service module can declare its own vhost
|
||
without restating the port pair — a vhost that binds a
|
||
different set is reachable on a port the gateway does not
|
||
consider its own, which is the kind of drift nobody notices
|
||
until one name behaves differently from the rest.
|
||
'';
|
||
};
|
||
|
||
tlsFor = lib.mkOption {
|
||
type = lib.types.functionTo (lib.types.attrsOf lib.types.raw);
|
||
internal = true;
|
||
readOnly = true;
|
||
description = ''
|
||
Read-only: `host -> ssl attrs` for a vhost of that name.
|
||
|
||
Which issuer covers a name is **gateway** knowledge, not the
|
||
service's: a swarm service's name can sit outside this hive's
|
||
domain, and the hive CA is name-constrained out of it, so that
|
||
vhost must serve the swarm-services leaf while everything else
|
||
keeps the hive leaf. A service module calls this instead of
|
||
deciding — deciding is how the vhost and the cert stop
|
||
agreeing.
|
||
'';
|
||
};
|
||
|
||
errorPages = lib.mkOption {
|
||
type = lib.types.attrsOf lib.types.path;
|
||
internal = true;
|
||
readOnly = true;
|
||
description = ''
|
||
Read-only: the gateway's styled static error pages, by name
|
||
(`notFound`, `unreachable`, `unauthorized`, `ssoUnavailable`).
|
||
|
||
Published so a service module can aim an `error_page` at one
|
||
instead of rendering its own — a service that built its own
|
||
would drift from the rest of the gateway the first time the
|
||
theme changed, and the operator would meet two different
|
||
error styles on one hive.
|
||
'';
|
||
};
|
||
|
||
securityHeaders = lib.mkOption {
|
||
type = lib.types.lines;
|
||
internal = true;
|
||
readOnly = true;
|
||
description = ''
|
||
Read-only: the server-scope security headers every vhost in
|
||
front of this gateway sets.
|
||
|
||
⚠️ nginx does not merge `add_header`: a location that sets one
|
||
of its own inherits **none** of these, so such a location must
|
||
repeat them. That rule is why this is published rather than
|
||
left implicit — a service module writing its own `locations`
|
||
needs the text, not a description of it.
|
||
'';
|
||
};
|
||
};
|
||
|
||
useSelfSigned = lib.mkOption {
|
||
type = lib.types.bool;
|
||
internal = true;
|
||
readOnly = true;
|
||
default = cfg.tls.certDir == null && !cfg.tls.acme.enable;
|
||
defaultText = lib.literalExpression "tls.certDir == null && !tls.acme.enable";
|
||
description = ''
|
||
Read-only derived flag: `true` when the gateway serves the
|
||
self-signed (hive-CA-signed) leaf — i.e. neither `tls.certDir` nor
|
||
`tls.acme.enable` is configured. Single source of truth for the
|
||
self-signed condition; consumed by the `hive-tls` and `hive-ci`
|
||
modules so the derivation isn't duplicated. Internal — not meant to
|
||
be set by operators (use `tls.certDir` / `tls.acme` to override the
|
||
self-signed default).
|
||
'';
|
||
};
|
||
|
||
httpsPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 443;
|
||
example = 8443;
|
||
description = ''
|
||
TCP port for the TLS-terminated vhosts. Default 443. The gateway
|
||
always terminates TLS (self-signed is the implicit floor when no
|
||
`tls.certDir` / ACME is configured), so this port is always active
|
||
alongside the plain-http `port`.
|
||
'';
|
||
};
|
||
|
||
tls = {
|
||
certDir = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.path;
|
||
default = null;
|
||
example = lib.literalExpression ''"/var/lib/acme/example.com"'';
|
||
description = ''
|
||
Path to a host directory containing a TLS certificate and
|
||
private key for nginx. When set, nginx listens on `httpsPort`
|
||
and uses this cert, overriding the self-signed default — the
|
||
auto-generated hive-CA-signed leaf is skipped entirely.
|
||
|
||
nginx reads `<certDir>/<tls.certName>` and
|
||
`<certDir>/<tls.keyName>` directly — it runs on the host, so
|
||
the directory needs no bind mount and no copy.
|
||
Default filenames (`cert.pem` / `key.pem`) match the output
|
||
layout of nixpkgs's `security.acme` module.
|
||
|
||
Typical ACME setup:
|
||
```nix
|
||
security.acme.certs."example.com" = { ... };
|
||
services.hyperhive.gateway.tls.certDir =
|
||
config.security.acme.certs."example.com".directory;
|
||
```
|
||
|
||
Mutual exclusion with `tls.acme.enable` — set one or the other,
|
||
not both.
|
||
'';
|
||
};
|
||
|
||
certName = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "cert.pem";
|
||
description = ''
|
||
Filename of the TLS certificate within `tls.certDir`. Defaults
|
||
to `cert.pem` which matches nixpkgs's `security.acme` output.
|
||
'';
|
||
};
|
||
|
||
keyName = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "key.pem";
|
||
description = ''
|
||
Filename of the TLS private key within `tls.certDir`. Defaults
|
||
to `key.pem` which matches nixpkgs's `security.acme` output.
|
||
'';
|
||
};
|
||
|
||
acme = {
|
||
enable = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = false;
|
||
example = true;
|
||
description = ''
|
||
Let the gateway's nginx obtain and renew TLS certificates
|
||
automatically via ACME (Let's Encrypt). When enabled, each
|
||
vhost calls out to Let's Encrypt using the HTTP-01
|
||
challenge on `port` (default 80) and stores certs in the
|
||
gateway's persistent state dir on the host.
|
||
|
||
Requirements:
|
||
- `services.hyperhive.domain` must be set and publicly
|
||
DNS-resolvable to this host.
|
||
- `services.hyperhive.gateway.openFirewall = true` so
|
||
Let's Encrypt can reach `/.well-known/acme-challenge/`.
|
||
- `tls.acme.email` must be set (ACME account contact).
|
||
|
||
Mutual exclusion: `tls.certDir` set together with
|
||
`tls.acme.enable = true` fails at eval — pick one TLS source.
|
||
|
||
Typical setup:
|
||
```nix
|
||
services.hyperhive.gateway = {
|
||
openFirewall = true;
|
||
tls.acme = {
|
||
enable = true;
|
||
email = "admin@example.com";
|
||
};
|
||
};
|
||
```
|
||
'';
|
||
};
|
||
|
||
email = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = null;
|
||
example = "admin@example.com";
|
||
description = ''
|
||
Email address for the ACME account registration with
|
||
Let's Encrypt. Required when `tls.acme.enable = true`.
|
||
Let's Encrypt sends expiry warnings to this address.
|
||
'';
|
||
};
|
||
};
|
||
};
|
||
|
||
auth = {
|
||
enable = lib.mkEnableOption ''
|
||
HTTP basic auth on the gateway using an htpasswd file. When
|
||
enabled, every request to the gateway's main vhost requires a
|
||
valid username and password. nginx's built-in `auth_basic`
|
||
module validates credentials against
|
||
`/var/lib/hive-gateway/conf/gateway.htpasswd`. Off by default.
|
||
|
||
Manage users with `hivectl gateway create-user`, `delete-user`,
|
||
and `list-users` — see `hivectl gateway --help` for usage.
|
||
The htpasswd file is created automatically when auth is enabled;
|
||
add at least one user before enabling to avoid locking everyone out.
|
||
'';
|
||
|
||
realm = lib.mkOption {
|
||
type = lib.types.strMatching "[^\"$]*";
|
||
default = "hyperhive";
|
||
example = "my-hive";
|
||
description = ''
|
||
HTTP Basic auth `realm` value sent in the `WWW-Authenticate`
|
||
header when credentials are absent or rejected. Must not
|
||
contain `"` or `$` (nginx string metacharacters).
|
||
'';
|
||
};
|
||
};
|
||
|
||
swaggerUiTheme = lib.mkOption {
|
||
type = lib.types.package;
|
||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swagger-ui-theme";
|
||
description = ''
|
||
Full Swagger UI static dist, hyperhive-themed (see
|
||
`nix/packages/swagger-ui-theme.nix`, built on
|
||
`nix/packages/swagger-ui-dist.nix`). The gateway serves this
|
||
whole tree directly at `/api/docs/` — hive-c0re hosts none of
|
||
it, only the dynamic `/api/openapi.json` route (proxied
|
||
through, unaffected by this option). Override to ship a
|
||
custom theme (or the plain vendored dist) without a gateway
|
||
rebuild.
|
||
'';
|
||
};
|
||
|
||
hsts = {
|
||
enable = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = false;
|
||
description = ''
|
||
Add `Strict-Transport-Security` to all gateway vhosts.
|
||
|
||
Disabled by default: HSTS pins HTTPS in the browser's HSTS
|
||
preload list; enabling it on a deployment that later loses TLS
|
||
will lock browsers out until the max-age expires. Only enable
|
||
this when you are certain TLS is permanent.
|
||
|
||
The gateway always terminates TLS (self-signed floor), so
|
||
HSTS is always served over https when enabled — but mind the
|
||
warning above: HSTS pins https in the browser, so only enable it
|
||
when TLS is permanent for this deployment.
|
||
'';
|
||
};
|
||
|
||
maxAge = lib.mkOption {
|
||
type = lib.types.ints.positive;
|
||
default = 31536000;
|
||
example = 86400;
|
||
description = ''
|
||
Value for the `max-age` directive in seconds.
|
||
Default: 31536000 (1 year), which is the value required for
|
||
HSTS preload list submission. Use a shorter value (e.g. 86400)
|
||
while testing so browsers forget the pin quickly.
|
||
'';
|
||
};
|
||
|
||
includeSubDomains = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = true;
|
||
description = ''
|
||
Whether to include `includeSubDomains` in the HSTS header.
|
||
Only disable this if the gateway host has sub-domains that
|
||
intentionally serve plain HTTP.
|
||
'';
|
||
};
|
||
};
|
||
};
|
||
}
|