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
|
|
@ -22,6 +22,8 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo
|
||||||
| `<queue domain>/metrics` | the queue exporter's `/metrics` on loopback, behind an authelia `auth_request`; every other path, `/` included, returns `403`, so `/varz` and `/connz` are never reachable | `swarm-nats.nix`, the queue's host |
|
| `<queue domain>/metrics` | the queue exporter's `/metrics` on loopback, behind an authelia `auth_request`; every other path, `/` included, returns `403`, so `/varz` and `/connz` are never reachable | `swarm-nats.nix`, the queue's host |
|
||||||
| `grafana.<swarm>`, `metrics.<swarm>`, `logs.<swarm>`, `otel.<swarm>`, `bao.<swarm>` | the matching swarm service | that service's module → [`swarm/services.md`](../swarm/services.md) |
|
| `grafana.<swarm>`, `metrics.<swarm>`, `logs.<swarm>`, `otel.<swarm>`, `bao.<swarm>` | the matching swarm service | that service's module → [`swarm/services.md`](../swarm/services.md) |
|
||||||
|
|
||||||
|
A service module declares its vhost as `services.hyperhive.gateway.vhosts.<name>`: the locations, `forceSSL` and any `extraConfig` it needs. The gateway renders it into `services.nginx.virtualHosts.<name>` and adds the `listen` set, the certificate for that name and the [security headers](#security-headers) (ahead of the vhost's own `extraConfig`), and drops `addSSL` from a vhost that sets `forceSSL`. The eval fails for a vhost that sets `listen`, `addSSL`, `onlySSL`, `enableACME`, `useACMEHost`, `sslCertificate` or `sslCertificateKey` itself, and for any vhost declared while `gateway.enable` is false. A name with a certificate from outside the gateway's leaves (the queue's) sets `certificate = { cert; key; }` instead.
|
||||||
|
|
||||||
**The hive's own vhosts**: the dashboard's, named `<name>.hives.<swarm>` (`swarm.dashboardDomain`), and one named for the hive domain. They exist only on a host that runs a hive (`deploy.hive-controller.enable`); a host without a hive has none of these rows:
|
**The hive's own vhosts**: the dashboard's, named `<name>.hives.<swarm>` (`swarm.dashboardDomain`), and one named for the hive domain. They exist only on a host that runs a hive (`deploy.hive-controller.enable`); a host without a hive has none of these rows:
|
||||||
|
|
||||||
| URL | upstream | when |
|
| URL | upstream | when |
|
||||||
|
|
|
||||||
|
|
@ -261,9 +261,8 @@ in
|
||||||
# This service's own gateway surface: the vhost that fronts it and
|
# This service's own gateway surface: the vhost that fronts it and
|
||||||
# the name the hive resolver answers for. Declared here rather than
|
# the name the hive resolver answers for. Declared here rather than
|
||||||
# in the gateway so the forge's public face lives with the forge —
|
# in the gateway so the forge's public face lives with the forge —
|
||||||
# the gateway supplies the primitives (`lib.listen`, `lib.tlsFor`,
|
# the gateway adds `listen`, the certificate and the security headers
|
||||||
# `lib.securityHeaders`) and never needs to know this service by
|
# (`gateway.vhosts`) and never needs to know this service by name.
|
||||||
# name.
|
|
||||||
services.hyperhive.gateway.localNames = [ cfg.domain ];
|
services.hyperhive.gateway.localNames = [ cfg.domain ];
|
||||||
services.hyperhive.gateway.enable = lib.mkDefault true;
|
services.hyperhive.gateway.enable = lib.mkDefault true;
|
||||||
|
|
||||||
|
|
@ -274,10 +273,8 @@ in
|
||||||
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h` (multi-GB
|
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h` (multi-GB
|
||||||
# clones). SSH stays direct on `forge.sshPort`. See
|
# clones). SSH stays direct on `forge.sshPort`. See
|
||||||
# `docs/networking/gateway.md`.
|
# `docs/networking/gateway.md`.
|
||||||
services.nginx.virtualHosts = {
|
services.hyperhive.gateway.vhosts = {
|
||||||
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
"${cfg.domain}" = {
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
locations = {
|
locations = {
|
||||||
"/" = {
|
"/" = {
|
||||||
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/";
|
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/";
|
||||||
|
|
|
||||||
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.
|
# namespace of its own.
|
||||||
# Full vhost map + discovery flow + design rationale in
|
# Full vhost map + discovery flow + design rationale in
|
||||||
# `docs/networking/gateway.md`. Layout: ./options.nix (option declarations),
|
# `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
|
# (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
|
# has an enable of its own because a host can need hive names to resolve
|
||||||
# without serving a vhost).
|
# without serving a vhost).
|
||||||
|
|
@ -134,6 +135,7 @@ in
|
||||||
imports = [
|
imports = [
|
||||||
./options.nix
|
./options.nix
|
||||||
./dns.nix
|
./dns.nix
|
||||||
|
./declared-vhosts.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
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 = {
|
lib = {
|
||||||
listen = lib.mkOption {
|
listen = lib.mkOption {
|
||||||
type = lib.types.listOf (lib.types.attrsOf lib.types.raw);
|
type = lib.types.listOf (lib.types.attrsOf lib.types.raw);
|
||||||
|
|
@ -144,11 +199,10 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Read-only: the `listen` set every vhost in front of this
|
Read-only: the `listen` set every vhost in front of this
|
||||||
gateway shares (plain http on `port`, TLS on `httpsPort`).
|
gateway shares (plain http on `port`, TLS on `httpsPort`).
|
||||||
Published so a service module can declare its own vhost
|
Applied to every vhost declared under `vhosts`, and read by
|
||||||
without restating the port pair — a vhost that binds a
|
the gateway's own vhosts, so no vhost binds a different
|
||||||
different set is reachable on a port the gateway does not
|
set — one that did would be reachable on a port the gateway
|
||||||
consider its own, which is the kind of drift nobody notices
|
does not consider its own.
|
||||||
until one name behaves differently from the rest.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -163,8 +217,8 @@ in
|
||||||
service's: a swarm service's name can sit outside this hive's
|
service's: a swarm service's name can sit outside this hive's
|
||||||
domain, and the hive leaf's role refuses it, so that
|
domain, and the hive leaf's role refuses it, so that
|
||||||
vhost must serve the swarm-services leaf while everything else
|
vhost must serve the swarm-services leaf while everything else
|
||||||
keeps the hive leaf. A service module calls this instead of
|
keeps the hive leaf. The gateway calls this for every vhost it
|
||||||
deciding — deciding is how the vhost and the cert stop
|
fronts — a vhost choosing its own is how the vhost and the cert stop
|
||||||
agreeing.
|
agreeing.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,17 @@
|
||||||
# attrs, and the security headers every vhost in front of this gateway
|
# attrs, and the security headers every vhost in front of this gateway
|
||||||
# needs.
|
# needs.
|
||||||
#
|
#
|
||||||
# Split out of ./vhosts.nix so it has one home and two consumers. Today
|
# Split out of ./vhosts.nix so it has one home and several consumers.
|
||||||
# only ./vhosts.nix reads it; it is published as
|
# ./vhosts.nix and ./declared-vhosts.nix (which renders the vhosts service
|
||||||
# `services.hyperhive.gateway.lib` (see ./options.nix) so a service
|
# modules declare under `gateway.vhosts`) read it through
|
||||||
# module can declare its OWN vhost without the gateway having to know
|
# `services.hyperhive.gateway.lib` (see ./options.nix) rather than calling
|
||||||
# that service by name. `vhosts.nix` reads the published value rather
|
# this file directly — one source, not a copy that agrees by inspection. A
|
||||||
# than calling this file directly — one source, not a copy that agrees
|
# service module reads `verifiedProxyTo`, `errorPages` and `securityHeaders`
|
||||||
# by inspection.
|
# from there for the locations it writes.
|
||||||
#
|
#
|
||||||
# Pure function of the gateway's config + resolved cert paths; returns
|
# Pure function of the gateway's config + resolved cert paths; returns
|
||||||
# an attrset, evaluates no options itself. Everything here is *gateway*
|
# an attrset, evaluates no options itself. Everything here is *gateway*
|
||||||
# knowledge — which port pair to bind, which issuer covers a name — and
|
# knowledge — which port pair to bind, which issuer covers a name.
|
||||||
# stays here even once the service vhosts move out to their own modules.
|
|
||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
cfg, # services.hyperhive.gateway
|
cfg, # services.hyperhive.gateway
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,9 @@
|
||||||
# dashboard's name.
|
# dashboard's name.
|
||||||
#
|
#
|
||||||
# Scope: a swarm service declares its own vhost and dns name in its own
|
# Scope: a swarm service declares its own vhost and dns name in its own
|
||||||
# module, built from the kit this file also consumes
|
# module, as `services.hyperhive.gateway.vhosts.<name>` (rendered by
|
||||||
# (`services.hyperhive.gateway.lib`, ./vhost-lib.nix). That keeps a
|
# ./declared-vhosts.nix). That keeps a service's gateway surface next to
|
||||||
# service's gateway surface next to the service, and keeps this file to
|
# the service, and keeps this file to the surface the hive itself serves.
|
||||||
# the surface the hive itself serves.
|
|
||||||
#
|
#
|
||||||
# Pure function — called from ./default.nix with the outer-scope config
|
# Pure function — called from ./default.nix with the outer-scope config
|
||||||
# values as arguments; returns `{ virtualHosts }`.
|
# values as arguments; returns `{ virtualHosts }`.
|
||||||
|
|
|
||||||
|
|
@ -477,10 +477,8 @@ in
|
||||||
# be served at the *server name*, which is the hive domain — it is
|
# be served at the *server name*, which is the hive domain — it is
|
||||||
# the hive answering "where is my homeserver", not the homeserver
|
# the hive answering "where is my homeserver", not the homeserver
|
||||||
# answering for itself.
|
# answering for itself.
|
||||||
services.nginx.virtualHosts = lib.optionalAttrs (cfg.gatewayHost != null) {
|
services.hyperhive.gateway.vhosts = lib.optionalAttrs (cfg.gatewayHost != null) {
|
||||||
"${cfg.gatewayHost}" = (gatewayCfg.lib.tlsFor cfg.gatewayHost) // {
|
"${cfg.gatewayHost}" = {
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
locations = {
|
locations = {
|
||||||
"/_matrix/" = {
|
"/_matrix/" = {
|
||||||
# No URI after the socket, so tuwunel gets the request URI as the
|
# No URI after the socket, so tuwunel gets the request URI as the
|
||||||
|
|
|
||||||
|
|
@ -800,9 +800,7 @@ in
|
||||||
#
|
#
|
||||||
# ⚠️ And deliberately NO `auth_request` here: it would put the login
|
# ⚠️ And deliberately NO `auth_request` here: it would put the login
|
||||||
# page behind the login it serves.
|
# page behind the login it serves.
|
||||||
services.nginx.virtualHosts."${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
services.hyperhive.gateway.vhosts."${cfg.domain}" = {
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://127.0.0.1:${toString cfg.port}/";
|
proxyPass = "http://127.0.0.1:${toString cfg.port}/";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
|
|
|
||||||
|
|
@ -2716,14 +2716,9 @@ in
|
||||||
# netns), so no port opens anywhere. `auth_request` only proves a
|
# netns), so no port opens anywhere. `auth_request` only proves a
|
||||||
# session; restricting it to `admins` is ./swarm-authelia.nix's rule
|
# session; restricting it to `admins` is ./swarm-authelia.nix's rule
|
||||||
# for this name. `forceSSL` because authelia answers a plain-http
|
# for this name. `forceSSL` because authelia answers a plain-http
|
||||||
# subrequest with 400, and `removeAttrs` because nixos refuses
|
# subrequest with 400.
|
||||||
# `addSSL` beside it.
|
services.hyperhive.gateway.vhosts.${cfg.ui.domain} = {
|
||||||
services.nginx.virtualHosts.${cfg.ui.domain} =
|
|
||||||
(builtins.removeAttrs (hyperhiveCfg.gateway.lib.tlsFor cfg.ui.domain) [ "addSSL" ])
|
|
||||||
// {
|
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
listen = hyperhiveCfg.gateway.lib.listen;
|
|
||||||
extraConfig = hyperhiveCfg.gateway.lib.securityHeaders;
|
|
||||||
locations = {
|
locations = {
|
||||||
"/" = {
|
"/" = {
|
||||||
proxyPass = "http://127.0.0.1:${toString baoDeploy.uiProxyPort}";
|
proxyPass = "http://127.0.0.1:${toString baoDeploy.uiProxyPort}";
|
||||||
|
|
|
||||||
|
|
@ -558,9 +558,7 @@ in
|
||||||
# Websockets: Grafana Live streams panel updates over one, and without
|
# Websockets: Grafana Live streams panel updates over one, and without
|
||||||
# the upgrade headers dashboards load and then never refresh — which
|
# the upgrade headers dashboards load and then never refresh — which
|
||||||
# reads as stale data rather than as a proxy fault.
|
# reads as stale data rather than as a proxy fault.
|
||||||
services.nginx.virtualHosts."${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
services.hyperhive.gateway.vhosts."${cfg.domain}" = {
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
# Grafana serves its Prometheus exposition to anyone, signed in or not
|
# Grafana serves its Prometheus exposition to anyone, signed in or not
|
||||||
# (`metrics.enabled` below, no `basic_auth_*`), so this location's auth
|
# (`metrics.enabled` below, no `basic_auth_*`), so this location's auth
|
||||||
# is the only gate on it. `=` is enough: nginx matches the decoded,
|
# is the only gate on it. `=` is enough: nginx matches the decoded,
|
||||||
|
|
|
||||||
|
|
@ -465,28 +465,19 @@ in
|
||||||
# nothing else on this name: the monitoring endpoint (`/varz`, `/connz`)
|
# nothing else on this name: the monitoring endpoint (`/varz`, `/connz`)
|
||||||
# is never proxied, and every other path is 403.
|
# is never proxied, and every other path is 403.
|
||||||
#
|
#
|
||||||
# ⚠️ Not `gateway.lib.tlsFor`, and the name is not in
|
# ⚠️ In self-signed mode this vhost serves the queue's own leaf
|
||||||
# `swarm.serviceDomains`. In self-signed mode this vhost serves the
|
# (`certificate`; issued for exactly this name, `server_flag` set), not
|
||||||
# queue's own leaf (issued for exactly this name, `server_flag` set), so
|
# the gateway's pick for the name, and the name is not in
|
||||||
# a host whose only swarm service is the queue never fronts a services
|
# `swarm.serviceDomains`.
|
||||||
# name and its gateway does not wait for the swarm-services leaf.
|
|
||||||
#
|
#
|
||||||
# `forceSSL` for the reason ./swarm-victorialogs.nix gives: authelia
|
# `forceSSL` for the reason ./swarm-victorialogs.nix gives: authelia
|
||||||
# answers a plain-http auth subrequest with 400.
|
# answers a plain-http auth subrequest with 400.
|
||||||
services.nginx.virtualHosts."${cfg.domain}" =
|
services.hyperhive.gateway.vhosts."${cfg.domain}" = {
|
||||||
(
|
certificate = lib.mkIf gatewayServesQueueLeaf {
|
||||||
if gatewayServesQueueLeaf then
|
cert = "${gatewayTlsDir}/cert.pem";
|
||||||
{
|
key = "${gatewayTlsDir}/key.pem";
|
||||||
sslCertificate = "${gatewayTlsDir}/cert.pem";
|
};
|
||||||
sslCertificateKey = "${gatewayTlsDir}/key.pem";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ]
|
|
||||||
)
|
|
||||||
// {
|
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
locations = {
|
locations = {
|
||||||
"/".return = "403";
|
"/".return = "403";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -358,9 +358,7 @@ in
|
||||||
# every sibling swarm service: TLS terminates here, then plain http to
|
# every sibling swarm service: TLS terminates here, then plain http to
|
||||||
# the co-located container over loopback (shared netns, like the store
|
# the co-located container over loopback (shared netns, like the store
|
||||||
# this collector writes to).
|
# this collector writes to).
|
||||||
services.nginx.virtualHosts."${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
services.hyperhive.gateway.vhosts."${cfg.domain}" = {
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
locations =
|
locations =
|
||||||
# One name for the whole collector, and the hive is a path under
|
# One name for the whole collector, and the hive is a path under
|
||||||
# it. The alternative — a vhost per hive — needs a certificate,
|
# it. The alternative — a vhost per hive — needs a certificate,
|
||||||
|
|
|
||||||
|
|
@ -179,13 +179,9 @@ in
|
||||||
# on. Serving forge or matrix over http is merely insecure rather
|
# on. Serving forge or matrix over http is merely insecure rather
|
||||||
# than broken, so they keep `addSSL` and the asymmetry stays local
|
# than broken, so they keep `addSSL` and the asymmetry stays local
|
||||||
# to the vhost whose correctness depends on the scheme.
|
# to the vhost whose correctness depends on the scheme.
|
||||||
# `removeAttrs` because nixos asserts on a vhost declaring both.
|
# The gateway drops `addSSL` from a vhost that sets `forceSSL`.
|
||||||
services.nginx.virtualHosts."${apex}" =
|
services.hyperhive.gateway.vhosts."${apex}" = {
|
||||||
(builtins.removeAttrs (gatewayCfg.lib.tlsFor apex) [ "addSSL" ])
|
|
||||||
// {
|
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
locations = {
|
locations = {
|
||||||
"/" = {
|
"/" = {
|
||||||
root = "${deployCfg.swarm-controller.ui.package}";
|
root = "${deployCfg.swarm-controller.ui.package}";
|
||||||
|
|
|
||||||
|
|
@ -313,17 +313,13 @@ in
|
||||||
# Authenticated front door onto the loopback-only store — see the
|
# Authenticated front door onto the loopback-only store — see the
|
||||||
# file-top comment for why this is safe to add without touching the
|
# file-top comment for why this is safe to add without touching the
|
||||||
# store's own (still unauthenticated, still loopback) listener at all.
|
# store's own (still unauthenticated, still loopback) listener at all.
|
||||||
# `removeAttrs`/`forceSSL`: same asymmetry `swarm-ui.nix` documents —
|
# `forceSSL`: same asymmetry `swarm-ui.nix` documents —
|
||||||
# authelia answers a plain-http auth subrequest with 400, which
|
# authelia answers a plain-http auth subrequest with 400, which
|
||||||
# `auth_request` cannot read as anything but a broken check, so this
|
# `auth_request` cannot read as anything but a broken check, so this
|
||||||
# vhost needs `forceSSL` rather than the `addSSL` every unauthenticated
|
# vhost needs `forceSSL` rather than the `addSSL` every unauthenticated
|
||||||
# sibling vhost uses.
|
# sibling vhost uses.
|
||||||
services.nginx.virtualHosts."${cfg.domain}" =
|
services.hyperhive.gateway.vhosts."${cfg.domain}" = {
|
||||||
(builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ])
|
|
||||||
// {
|
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
locations = {
|
locations = {
|
||||||
"/" = {
|
"/" = {
|
||||||
proxyPass = "http://127.0.0.1:${toString cfg.port}/";
|
proxyPass = "http://127.0.0.1:${toString cfg.port}/";
|
||||||
|
|
|
||||||
|
|
@ -252,15 +252,11 @@ in
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# `removeAttrs`/`forceSSL`: authelia answers a plain-http auth subrequest
|
# `forceSSL`: authelia answers a plain-http auth subrequest
|
||||||
# with 400, which `auth_request` cannot read as anything but a broken
|
# with 400, which `auth_request` cannot read as anything but a broken
|
||||||
# check. Same as ./swarm-victorialogs.nix's vhost.
|
# check. Same as ./swarm-victorialogs.nix's vhost.
|
||||||
services.nginx.virtualHosts."${cfg.domain}" =
|
services.hyperhive.gateway.vhosts."${cfg.domain}" = {
|
||||||
(builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ])
|
|
||||||
// {
|
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
listen = gatewayCfg.lib.listen;
|
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
|
||||||
locations =
|
locations =
|
||||||
readLocations
|
readLocations
|
||||||
// uiLocations
|
// uiLocations
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ let
|
||||||
deploy.grafana.plugins = [ ];
|
deploy.grafana.plugins = [ ];
|
||||||
deploy.grafana.package = pkgs.emptyDirectory;
|
deploy.grafana.package = pkgs.emptyDirectory;
|
||||||
swarm.authelia.url = "https://auth.example.invalid";
|
swarm.authelia.url = "https://auth.example.invalid";
|
||||||
|
# Declares a vhost that binds its own `listen`, which the gateway refuses.
|
||||||
|
gateway.vhosts."own-listen.t.local".listen = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Did ./host-modules/swarm-grafana.nix refuse this host, and for which of its
|
# Did ./host-modules/swarm-grafana.nix refuse this host, and for which of its
|
||||||
|
|
@ -479,6 +481,39 @@ let
|
||||||
&& builtins.length selectors == 12
|
&& builtins.length selectors == 12
|
||||||
&& lib.all (e: lib.hasInfix "\"service.name\"=\"${stamped.value}\"" e) selectors;
|
&& lib.all (e: lib.hasInfix "\"service.name\"=\"${stamped.value}\"" e) selectors;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# A module declares its vhost under `gateway.vhosts` and the gateway adds
|
||||||
|
# what it owns: the listen set, the cert for the name and the security
|
||||||
|
# headers ahead of the module's own `extraConfig`.
|
||||||
|
name = "grafana's vhost is rendered with the gateway's listen set, TLS and security headers";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
gw = grafanaOldPath.services.hyperhive.gateway;
|
||||||
|
v =
|
||||||
|
grafanaOldPath.services.nginx.virtualHosts.${grafanaOldPath.services.hyperhive.swarm.grafana.domain};
|
||||||
|
# nginx's `listen` entries come back with defaults filled in
|
||||||
|
# (`extraParameters`, `proxyProtocol`), so compare the fields set.
|
||||||
|
listenOf = map (l: {
|
||||||
|
inherit (l) addr port;
|
||||||
|
ssl = l.ssl or false;
|
||||||
|
});
|
||||||
|
in
|
||||||
|
listenOf v.listen == listenOf gw.lib.listen
|
||||||
|
&& lib.hasPrefix gw.lib.securityHeaders v.extraConfig
|
||||||
|
&& v.addSSL
|
||||||
|
&& !(v ? certificate);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
# A vhost that sets `listen` itself would bind a port the gateway does
|
||||||
|
# not consider its own, so the eval refuses it and names the vhost.
|
||||||
|
name = "a gateway vhost that sets its own listen is refused";
|
||||||
|
ok = lib.any (
|
||||||
|
a:
|
||||||
|
!a.assertion
|
||||||
|
&& lib.hasInfix "sets attributes the gateway owns" a.message
|
||||||
|
&& lib.hasInfix "own-listen.t.local.listen" a.message
|
||||||
|
) grafanaNoIdentity.assertions;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
runGroup "grafana" cases
|
runGroup "grafana" cases
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue