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.
This commit is contained in:
parent
7b50ae57e4
commit
ddb583cc22
17 changed files with 652 additions and 513 deletions
85
nix/host-modules/hive-gateway/declared-vhosts.nix
Normal file
85
nix/host-modules/hive-gateway/declared-vhosts.nix
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Renders `services.hyperhive.gateway.vhosts` into
|
||||
# `services.nginx.virtualHosts`: each declared vhost keeps what its module
|
||||
# wrote and gets the gateway's `listen`, the TLS attributes for its name and
|
||||
# the security headers.
|
||||
#
|
||||
# Its own module rather than part of ./default.nix because the
|
||||
# "declared while the gateway is off" assertion has to hold when
|
||||
# `gateway.enable` is false, where ./default.nix defines nothing.
|
||||
{ lib, config, ... }:
|
||||
let
|
||||
cfg = config.services.hyperhive.gateway;
|
||||
|
||||
# What the gateway owns on a vhost. A declared vhost naming one of these
|
||||
# fails the eval.
|
||||
gatewayOwned = [
|
||||
"listen"
|
||||
"addSSL"
|
||||
"onlySSL"
|
||||
"enableACME"
|
||||
"useACMEHost"
|
||||
"sslCertificate"
|
||||
"sslCertificateKey"
|
||||
];
|
||||
|
||||
offenders = lib.concatMap (
|
||||
name: map (attr: "${name}.${attr}") (lib.filter (a: cfg.vhosts.${name} ? ${a}) gatewayOwned)
|
||||
) (lib.attrNames cfg.vhosts);
|
||||
|
||||
render =
|
||||
name: declared:
|
||||
let
|
||||
own = removeAttrs declared [
|
||||
"_module"
|
||||
"certificate"
|
||||
];
|
||||
base =
|
||||
if declared.certificate != null then
|
||||
{
|
||||
sslCertificate = declared.certificate.cert;
|
||||
sslCertificateKey = declared.certificate.key;
|
||||
}
|
||||
else
|
||||
cfg.lib.tlsFor name;
|
||||
# nixos asserts on a vhost declaring both `addSSL` and `forceSSL`.
|
||||
tls = if own.forceSSL or false then removeAttrs base [ "addSSL" ] else base;
|
||||
in
|
||||
tls
|
||||
// own
|
||||
// {
|
||||
listen = cfg.lib.listen;
|
||||
extraConfig = cfg.lib.securityHeaders + (own.extraConfig or "");
|
||||
};
|
||||
in
|
||||
{
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.enable || cfg.vhosts == { };
|
||||
message = ''
|
||||
services.hyperhive.gateway.vhosts declares ${lib.concatStringsSep ", " (lib.attrNames cfg.vhosts)}
|
||||
while services.hyperhive.gateway.enable is false, so nothing
|
||||
would serve them. Enable the gateway, or drop the service that
|
||||
declares the vhost.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = offenders == [ ];
|
||||
message = ''
|
||||
services.hyperhive.gateway.vhosts sets attributes the gateway owns:
|
||||
${lib.concatStringsSep ", " offenders}
|
||||
|
||||
The gateway sets the listen set and the TLS attributes of every
|
||||
vhost it fronts. Declare only the vhost's own locations and
|
||||
options; to serve a certificate the gateway does not pick, set
|
||||
`certificate` on the vhost.
|
||||
'';
|
||||
}
|
||||
];
|
||||
}
|
||||
(lib.mkIf cfg.enable {
|
||||
services.nginx.virtualHosts = lib.mapAttrs render cfg.vhosts;
|
||||
})
|
||||
];
|
||||
}
|
||||
|
|
@ -6,7 +6,8 @@
|
|||
# namespace of its own.
|
||||
# Full vhost map + discovery flow + design rationale in
|
||||
# `docs/networking/gateway.md`. Layout: ./options.nix (option declarations),
|
||||
# ./vhosts.nix (the nginx virtual-host tree), ./error-pages.nix
|
||||
# ./vhosts.nix (the gateway's own nginx virtual hosts), ./declared-vhosts.nix
|
||||
# (renders the vhosts service modules declare under `gateway.vhosts`), ./error-pages.nix
|
||||
# (styled static pages), ./dns.nix + ./dnsmasq.nix (the resolver, which
|
||||
# has an enable of its own because a host can need hive names to resolve
|
||||
# without serving a vhost).
|
||||
|
|
@ -134,6 +135,7 @@ in
|
|||
imports = [
|
||||
./options.nix
|
||||
./dns.nix
|
||||
./declared-vhosts.nix
|
||||
];
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
|
|||
|
|
@ -136,6 +136,61 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
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);
|
||||
|
|
@ -144,11 +199,10 @@ in
|
|||
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.
|
||||
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.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -163,8 +217,8 @@ in
|
|||
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. A service module calls this instead of
|
||||
deciding — deciding is how the vhost and the cert stop
|
||||
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.
|
||||
'';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,18 +2,17 @@
|
|||
# attrs, and the security headers every vhost in front of this gateway
|
||||
# needs.
|
||||
#
|
||||
# Split out of ./vhosts.nix so it has one home and two consumers. Today
|
||||
# only ./vhosts.nix reads it; it is published as
|
||||
# `services.hyperhive.gateway.lib` (see ./options.nix) so a service
|
||||
# module can declare its OWN vhost without the gateway having to know
|
||||
# that service by name. `vhosts.nix` reads the published value rather
|
||||
# than calling this file directly — one source, not a copy that agrees
|
||||
# by inspection.
|
||||
# Split out of ./vhosts.nix so it has one home and several consumers.
|
||||
# ./vhosts.nix and ./declared-vhosts.nix (which renders the vhosts service
|
||||
# modules declare under `gateway.vhosts`) read it through
|
||||
# `services.hyperhive.gateway.lib` (see ./options.nix) rather than calling
|
||||
# this file directly — one source, not a copy that agrees by inspection. A
|
||||
# service module reads `verifiedProxyTo`, `errorPages` and `securityHeaders`
|
||||
# from there for the locations it writes.
|
||||
#
|
||||
# Pure function of the gateway's config + resolved cert paths; returns
|
||||
# an attrset, evaluates no options itself. Everything here is *gateway*
|
||||
# knowledge — which port pair to bind, which issuer covers a name — and
|
||||
# stays here even once the service vhosts move out to their own modules.
|
||||
# knowledge — which port pair to bind, which issuer covers a name.
|
||||
{
|
||||
lib,
|
||||
cfg, # services.hyperhive.gateway
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@
|
|||
# dashboard's name.
|
||||
#
|
||||
# Scope: a swarm service declares its own vhost and dns name in its own
|
||||
# module, built from the kit this file also consumes
|
||||
# (`services.hyperhive.gateway.lib`, ./vhost-lib.nix). That keeps a
|
||||
# service's gateway surface next to the service, and keeps this file to
|
||||
# the surface the hive itself serves.
|
||||
# module, as `services.hyperhive.gateway.vhosts.<name>` (rendered by
|
||||
# ./declared-vhosts.nix). That keeps a service's gateway surface next to
|
||||
# the service, and keeps this file to the surface the hive itself serves.
|
||||
#
|
||||
# Pure function — called from ./default.nix with the outer-scope config
|
||||
# values as arguments; returns `{ virtualHosts }`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue