refactor(3202): the gateway publishes its vhost construction kit
Slice 1 of #3202. The listen set, the per-name TLS attrs and the security headers move out of vhosts.nix into ./vhost-lib.nix and are published as `services.hyperhive.gateway.lib` (internal, readOnly). No behaviour change: vhosts.nix consumes the published value, so the rendered vhost tree is identical. The point is the next slice. Today a swarm service's vhost lives in the gateway because only the gateway knows the port pair, the issuer for a name, and the header block. Publishing those three is what lets a service module declare its own vhost without the gateway having to know that service by name.
This commit is contained in:
parent
272944b98f
commit
d5782965db
4 changed files with 189 additions and 82 deletions
|
|
@ -65,7 +65,25 @@ let
|
||||||
svcCert = "${tlsDir}/swarm-services.pem";
|
svcCert = "${tlsDir}/swarm-services.pem";
|
||||||
svcKey = "${tlsDir}/swarm-services-key.pem";
|
svcKey = "${tlsDir}/swarm-services-key.pem";
|
||||||
|
|
||||||
|
# The vhost construction kit (listen set / per-name TLS attrs /
|
||||||
|
# security headers). Computed here, published as `cfg.lib` below, and
|
||||||
|
# handed to ./vhosts.nix **as the published value** — so the tree the
|
||||||
|
# gateway renders and the kit a service module gets are the same
|
||||||
|
# object by construction, not by two call sites agreeing.
|
||||||
|
vhostLib = import ./vhost-lib.nix {
|
||||||
|
inherit
|
||||||
|
lib
|
||||||
|
cfg
|
||||||
|
tlsCert
|
||||||
|
tlsKey
|
||||||
|
svcCert
|
||||||
|
svcKey
|
||||||
|
swarmServiceDomains
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
nginxTree = import ./vhosts.nix {
|
nginxTree = import ./vhosts.nix {
|
||||||
|
gwLib = cfg.lib;
|
||||||
inherit
|
inherit
|
||||||
lib
|
lib
|
||||||
cfg
|
cfg
|
||||||
|
|
@ -77,11 +95,6 @@ let
|
||||||
hyperhiveDomain
|
hyperhiveDomain
|
||||||
dashboardDist
|
dashboardDist
|
||||||
swaggerUiTheme
|
swaggerUiTheme
|
||||||
tlsCert
|
|
||||||
tlsKey
|
|
||||||
svcCert
|
|
||||||
svcKey
|
|
||||||
swarmServiceDomains
|
|
||||||
;
|
;
|
||||||
errorPages = import ./error-pages.nix { inherit pkgs; };
|
errorPages = import ./error-pages.nix { inherit pkgs; };
|
||||||
};
|
};
|
||||||
|
|
@ -90,6 +103,12 @@ in
|
||||||
imports = [ ./options.nix ];
|
imports = [ ./options.nix ];
|
||||||
|
|
||||||
config = lib.mkIf config.services.hyperhive.enable {
|
config = lib.mkIf config.services.hyperhive.enable {
|
||||||
|
# Publish the kit. Defined here rather than as an option `default`
|
||||||
|
# so it stays a plain value computed once from resolved cert paths —
|
||||||
|
# `tlsFor` closes over `svcCert`/`svcKey`, which are derived in this
|
||||||
|
# file's `let` and are not option surface.
|
||||||
|
services.hyperhive.gateway.lib = vhostLib;
|
||||||
|
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = !(cfg.tls.acme.enable && cfg.tls.certDir != null);
|
assertion = !(cfg.tls.acme.enable && cfg.tls.certDir != null);
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,56 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
useSelfSigned = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
internal = true;
|
internal = true;
|
||||||
|
|
|
||||||
101
nix/host-modules/hive-gateway/vhost-lib.nix
Normal file
101
nix/host-modules/hive-gateway/vhost-lib.nix
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# The gateway's vhost construction kit: the listen set, the per-name TLS
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
cfg, # services.hyperhive.gateway
|
||||||
|
tlsCert,
|
||||||
|
tlsKey,
|
||||||
|
svcCert, # swarm-services leaf, for names the hive CA cannot sign
|
||||||
|
svcKey,
|
||||||
|
swarmServiceDomains, # which names those are (../swarm.nix derives it)
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
# nixos `services.nginx.virtualHosts.<name>` ssl attrs for a vhost
|
||||||
|
# covered by the hive's own cert. For ACME mode: `enableACME` +
|
||||||
|
# `addSSL` — NixOS's ACME integration manages the cert lifecycle and
|
||||||
|
# sets ssl_certificate automatically. For self-signed / certDir:
|
||||||
|
# explicit cert paths.
|
||||||
|
vhostTls =
|
||||||
|
if cfg.tls.acme.enable then
|
||||||
|
{
|
||||||
|
addSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
addSSL = true;
|
||||||
|
sslCertificate = tlsCert;
|
||||||
|
sslCertificateKey = tlsKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
hstsDirectives = lib.concatStringsSep "; " (
|
||||||
|
[ "max-age=${toString cfg.hsts.maxAge}" ]
|
||||||
|
++ lib.optional cfg.hsts.includeSubDomains "includeSubDomains"
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# The gateway always terminates TLS: self-signed is the implicit
|
||||||
|
# floor when neither `tls.certDir` nor ACME is set, so there is no
|
||||||
|
# http-only mode. Listen addresses every vhost shares — plain http
|
||||||
|
# on `cfg.port` plus TLS on `cfg.httpsPort`. See `docs/gateway.md`
|
||||||
|
# ("TLS modes").
|
||||||
|
listen = [
|
||||||
|
{
|
||||||
|
addr = "0.0.0.0";
|
||||||
|
port = cfg.port;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
addr = "0.0.0.0";
|
||||||
|
port = cfg.httpsPort;
|
||||||
|
ssl = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# TLS attrs for one vhost, by name. A swarm service's name may sit
|
||||||
|
# outside this hive's domain — and then the hive CA is
|
||||||
|
# name-constrained out of it, so its vhost must serve the
|
||||||
|
# swarm-services leaf instead. Everything else keeps the hive leaf.
|
||||||
|
#
|
||||||
|
# Only in self-signed mode: with ACME or an operator cert there is a
|
||||||
|
# single issuer that already covers every name, and a second pair
|
||||||
|
# would be a cert nobody asked for.
|
||||||
|
tlsFor =
|
||||||
|
host:
|
||||||
|
if !cfg.tls.acme.enable && cfg.tls.certDir == null && builtins.elem host swarmServiceDomains then
|
||||||
|
{
|
||||||
|
addSSL = true;
|
||||||
|
sslCertificate = svcCert;
|
||||||
|
sslCertificateKey = svcKey;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
vhostTls;
|
||||||
|
|
||||||
|
# Security headers added at the server scope on every vhost.
|
||||||
|
# nginx's add_header inheritance rule: a location that defines its
|
||||||
|
# own add_header does NOT inherit the server-level ones. Any
|
||||||
|
# location with its own add_header (e.g. CORS on /.well-known or
|
||||||
|
# /_matrix/) must repeat the security headers explicitly — see those
|
||||||
|
# locations in ./vhosts.nix. HTML-serving and proxy locations that
|
||||||
|
# carry no add_header of their own pick these up from the server
|
||||||
|
# scope automatically.
|
||||||
|
securityHeaders = ''
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
${lib.optionalString cfg.hsts.enable ''add_header Strict-Transport-Security "${hstsDirectives}" always;''}
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
|
@ -16,65 +16,17 @@
|
||||||
dashboardDist,
|
dashboardDist,
|
||||||
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
|
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
|
||||||
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
|
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
|
||||||
tlsCert,
|
gwLib, # `services.hyperhive.gateway.lib` — ./vhost-lib.nix's kit, via the option
|
||||||
tlsKey,
|
|
||||||
svcCert, # swarm-services leaf, for names the hive CA cannot sign
|
|
||||||
svcKey,
|
|
||||||
swarmServiceDomains, # which vhosts those are (../swarm.nix derives it)
|
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# The gateway always terminates TLS: self-signed is the implicit
|
# The kit's three members, bound to the names this file already used.
|
||||||
# floor when neither `tls.certDir` nor ACME is set, so there is no
|
# Read through `gwLib` (the published option) rather than importing
|
||||||
# http-only mode. Listen addresses every vhost shares — plain http
|
# ./vhost-lib.nix directly: a service module declaring its own vhost
|
||||||
# on `cfg.port` plus TLS on `cfg.httpsPort`. See `docs/gateway.md`
|
# gets the same object, so "the forge vhost listens where the gateway
|
||||||
# ("TLS modes").
|
# listens" is true by construction and not by review.
|
||||||
vhostListen = [
|
inherit (gwLib) securityHeaders;
|
||||||
{
|
vhostListen = gwLib.listen;
|
||||||
addr = "0.0.0.0";
|
vhostTlsFor = gwLib.tlsFor;
|
||||||
port = cfg.port;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
addr = "0.0.0.0";
|
|
||||||
port = cfg.httpsPort;
|
|
||||||
ssl = true;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
# nixos `services.nginx.virtualHosts.<name>` ssl attrs merged
|
|
||||||
# into each vhost. For ACME mode: `enableACME` + `addSSL` —
|
|
||||||
# NixOS's ACME integration manages the cert lifecycle and sets
|
|
||||||
# ssl_certificate automatically. For self-signed / certDir:
|
|
||||||
# explicit cert paths.
|
|
||||||
vhostTls =
|
|
||||||
if cfg.tls.acme.enable then
|
|
||||||
{
|
|
||||||
addSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
addSSL = true;
|
|
||||||
sslCertificate = tlsCert;
|
|
||||||
sslCertificateKey = tlsKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
# TLS attrs for one vhost, by name. A swarm service's name may sit
|
|
||||||
# outside this hive's domain — and then the hive CA is
|
|
||||||
# name-constrained out of it, so its vhost must serve the
|
|
||||||
# swarm-services leaf instead. Everything else keeps the hive leaf.
|
|
||||||
#
|
|
||||||
# Only in self-signed mode: with ACME or an operator cert there is a
|
|
||||||
# single issuer that already covers every name, and a second pair
|
|
||||||
# would be a cert nobody asked for.
|
|
||||||
vhostTlsFor =
|
|
||||||
host:
|
|
||||||
if !cfg.tls.acme.enable && cfg.tls.certDir == null && builtins.elem host swarmServiceDomains then
|
|
||||||
{
|
|
||||||
addSSL = true;
|
|
||||||
sslCertificate = svcCert;
|
|
||||||
sslCertificateKey = svcKey;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
vhostTls;
|
|
||||||
|
|
||||||
# Public-facing scheme + port-suffix for URLs the gateway
|
# Public-facing scheme + port-suffix for URLs the gateway
|
||||||
# mints into responses (well-known JSON, the deprecated
|
# mints into responses (well-known JSON, the deprecated
|
||||||
|
|
@ -85,25 +37,6 @@ let
|
||||||
publicPort = cfg.httpsPort;
|
publicPort = cfg.httpsPort;
|
||||||
publicPortSuffix = if publicPort == 443 then "" else ":${toString publicPort}";
|
publicPortSuffix = if publicPort == 443 then "" else ":${toString publicPort}";
|
||||||
|
|
||||||
# Security headers added at the server scope on every vhost.
|
|
||||||
# nginx's add_header inheritance rule: a location that defines its
|
|
||||||
# own add_header does NOT inherit the server-level ones. Any
|
|
||||||
# location with its own add_header (e.g. CORS on /.well-known or
|
|
||||||
# /_matrix/) must repeat the security headers explicitly — see those
|
|
||||||
# locations below. HTML-serving and proxy locations that carry no
|
|
||||||
# add_header of their own pick these up from the server scope
|
|
||||||
# automatically.
|
|
||||||
hstsDirectives = lib.concatStringsSep "; " (
|
|
||||||
[ "max-age=${toString cfg.hsts.maxAge}" ]
|
|
||||||
++ lib.optional cfg.hsts.includeSubDomains "includeSubDomains"
|
|
||||||
);
|
|
||||||
securityHeaders = ''
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
${lib.optionalString cfg.hsts.enable ''add_header Strict-Transport-Security "${hstsDirectives}" always;''}
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Forge sub-domain vhost. `server_name = forge.domain`, proxies
|
# Forge sub-domain vhost. `server_name = forge.domain`, proxies
|
||||||
# all `/` → forgejo. Tuned for git: `client_max_body_size 1G`,
|
# all `/` → forgejo. Tuned for git: `client_max_body_size 1G`,
|
||||||
# `proxy_read_timeout 1h` (multi-GB clones). SSH stays direct on
|
# `proxy_read_timeout 1h` (multi-GB clones). SSH stays direct on
|
||||||
|
|
@ -534,7 +467,11 @@ in
|
||||||
'';
|
'';
|
||||||
|
|
||||||
virtualHosts = {
|
virtualHosts = {
|
||||||
"_" = vhostTls // {
|
# `tlsFor "_"`, not a separate binding: the default server is a
|
||||||
|
# vhost named `_`, and a name that is not a swarm service domain
|
||||||
|
# (`_` never is) resolves to the hive's own leaf — which is what
|
||||||
|
# this vhost has always served.
|
||||||
|
"_" = (vhostTlsFor "_") // {
|
||||||
listen = vhostListen;
|
listen = vhostListen;
|
||||||
locations =
|
locations =
|
||||||
matrixRedirectLocations
|
matrixRedirectLocations
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue