Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/host-modules/hive-gateway/options.nix
atlas ddb583cc22 gateway: service modules declare their vhost under gateway.vhosts
Ten service modules (forge, matrix chat, authelia, grafana, otel, nats,
swarm-ui, victorialogs, victoriametrics, bao UI) wrote
services.nginx.virtualHosts themselves, each applying the gateway's
listen set, per-name TLS attrs and security headers by hand. They now
declare services.hyperhive.gateway.vhosts.<name> with only their own
locations and options; ./declared-vhosts.nix renders it into the nginx
vhost with listen, TLS and headers added.

An eval assertion refuses a declared vhost that sets listen, addSSL,
onlySSL, enableACME, useACMEHost, sslCertificate or sslCertificateKey,
and one declared while the gateway is off. nats' own leaf is the new
`certificate` field. The rendered nginx vhosts are unchanged.
2026-10-16 12:47:03 +02:00

478 lines
18 KiB
Nix
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Option declarations for `services.hyperhive.gateway.*`. The gateway is
# the single nginx in front of every surface and the only thing exposed
# to the outside. 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.
'')
(lib.mkRemovedOptionModule [ "services" "hyperhive" "gateway" "auth" ] ''
The dashboard is served on services.hyperhive.swarm.dashboardDomain
behind authelia, and only accounts in authelia's `admins` group can
open it. There is no HTTP Basic auth and no htpasswd file. Remove
the setting; add an operator with `swarmctl user add <name> --group admins`.
'')
];
options.services.hyperhive.gateway = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Run the hive's reverse proxy (nginx) on this host.";
};
dns.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Run the hive's resolver (dnsmasq) on this host.";
};
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`.
'';
};
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.deploy.singleHostSwarm` 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.
'';
};
vhosts = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
# Every other attribute is an nginx vhost option, passed through
# to `services.nginx.virtualHosts.<name>` as written.
freeformType = lib.types.attrsOf lib.types.anything;
options.certificate = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
options = {
cert = lib.mkOption {
type = lib.types.str;
description = "Path of the certificate nginx serves for this name.";
};
key = lib.mkOption {
type = lib.types.str;
description = "Path of the private key for `cert`. A path, never a value.";
};
};
}
);
default = null;
description = ''
A certificate pair to serve instead of the one the gateway
picks for this name (`gateway.lib.tlsFor`). For a name whose
certificate is issued outside the gateway's own leaves.
'';
};
}
);
default = { };
internal = true;
description = ''
The nginx vhosts that sit behind this gateway, by server name. A
service module declares its own — locations, `forceSSL`, extra
config — and the gateway renders it into
`services.nginx.virtualHosts.<name>` with the parts it owns:
- `listen`, the gateway's port pair;
- the TLS attributes for that name (`gateway.lib.tlsFor`, or
`certificate` below), with `addSSL` dropped when the vhost sets
`forceSSL`;
- `securityHeaders`, placed ahead of the vhost's own `extraConfig`.
A vhost that sets `listen`, `addSSL`, `onlySSL`, `enableACME`,
`useACMEHost`, `sslCertificate` or `sslCertificateKey` itself fails
the eval: those are the gateway's, and a service that sets them
can bind a port or present a certificate the rest of the gateway
does not.
A vhost declared while the gateway is off fails the eval too: the
name would otherwise be silently unserved.
'';
};
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`).
Applied to every vhost declared under `vhosts`, and read by
the gateway's own vhosts, so no vhost binds a different
set — one that did would be reachable on a port the gateway
does not consider its own.
'';
};
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 leaf's role refuses it, so that
vhost must serve the swarm-services leaf while everything else
keeps the hive leaf. The gateway calls this for every vhost it
fronts — a vhost choosing its own 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`, `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.
'';
};
verifiedProxyTo = lib.mkOption {
type = lib.types.functionTo lib.types.lines;
internal = true;
readOnly = true;
description = ''
Read-only: `name -> the proxy_ssl_* lines` for dialling
another service on this hive **by name over https**, with the
certificate actually verified against the hive's trust bundle.
Published rather than written per module because nginx
verifies nothing by default: `proxy_ssl_verify` is **off**, so
a `proxy_pass https://…` that omits these is encrypted and
unauthenticated — which is the half of "https and auth" that
is easy to believe you already have.
Measured, not copied: verification is confirmed to check both
the chain and the **hostname**, and the CA *bundle* (root +
intermediate) is confirmed to be accepted — the bundle's own
doc warns off consumers that read only one certificate, and
nginx is not one of those.
'';
};
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 leaf the secret store issues — 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-ca-trust`
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`.
'';
};
statusPort = lib.mkOption {
type = lib.types.port;
default = 8085;
description = ''
Loopback TCP port nginx serves its `stub_status` counters on, at
`http://127.0.0.1:''${statusPort}/stub_status`. The hive's collector
({option}`services.hyperhive.otel.enable`) reads it with its `nginx`
receiver.
TCP rather than a unix socket because that receiver can only dial
`http://`. Anything on the host can read these counters; they are
connection and request totals, nothing per-client.
'';
};
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
store-issued self-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.
'';
};
};
};
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.
'';
};
};
};
}