gateway: per-vhost public flag, private listen addresses, and IPv6 listeners
This commit is contained in:
parent
74754d1584
commit
fbbdf26a19
4 changed files with 364 additions and 25 deletions
|
|
@ -1,7 +1,7 @@
|
|||
# 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.
|
||||
# wrote and gets the gateway's `listen` (the private one when it sets
|
||||
# `public = false`), 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
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
{ lib, config, ... }:
|
||||
let
|
||||
cfg = config.services.hyperhive.gateway;
|
||||
listen = import ./listen.nix { inherit lib; };
|
||||
|
||||
# What the gateway owns on a vhost. A declared vhost naming one of these
|
||||
# fails the eval.
|
||||
|
|
@ -26,12 +27,16 @@ let
|
|||
name: map (attr: "${name}.${attr}") (lib.filter (a: cfg.vhosts.${name} ? ${a}) gatewayOwned)
|
||||
) (lib.attrNames cfg.vhosts);
|
||||
|
||||
privateOnly = lib.filter (name: !cfg.vhosts.${name}.public) (lib.attrNames cfg.vhosts);
|
||||
wildcardPrivate = lib.filter listen.isWildcard cfg.privateListenAddresses;
|
||||
|
||||
render =
|
||||
name: declared:
|
||||
let
|
||||
own = removeAttrs declared [
|
||||
"_module"
|
||||
"certificate"
|
||||
"public"
|
||||
"securityHeaders"
|
||||
];
|
||||
base =
|
||||
|
|
@ -48,7 +53,7 @@ let
|
|||
tls
|
||||
// own
|
||||
// {
|
||||
listen = cfg.lib.listen;
|
||||
listen = if declared.public then cfg.lib.listen else cfg.lib.privateListen;
|
||||
extraConfig =
|
||||
lib.optionalString declared.securityHeaders cfg.lib.securityHeaders + (own.extraConfig or "");
|
||||
}
|
||||
|
|
@ -83,6 +88,26 @@ in
|
|||
`certificate` on the vhost.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = privateOnly == [ ] || cfg.privateListenAddresses != [ ];
|
||||
message = ''
|
||||
services.hyperhive.gateway.vhosts sets `public = false` on
|
||||
${lib.concatStringsSep ", " privateOnly}
|
||||
while services.hyperhive.gateway.privateListenAddresses is empty,
|
||||
so these vhosts would listen nowhere. Set the addresses they
|
||||
should answer on, or drop `public = false`.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = wildcardPrivate == [ ];
|
||||
message = ''
|
||||
services.hyperhive.gateway.privateListenAddresses holds
|
||||
${lib.concatStringsSep ", " wildcardPrivate}, which is every address
|
||||
of the host, not a private one: the vhosts with `public = false`
|
||||
would answer on the public address too. List the host's private
|
||||
addresses (a WireGuard interface's, say).
|
||||
'';
|
||||
}
|
||||
];
|
||||
}
|
||||
(lib.mkIf cfg.enable {
|
||||
|
|
|
|||
52
nix/host-modules/hive-gateway/listen.nix
Normal file
52
nix/host-modules/hive-gateway/listen.nix
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# The gateway's listen sets, as pure functions of `lib` and the gateway's
|
||||
# port and address settings.
|
||||
#
|
||||
# nginx picks the server set for a connection by the address the connection
|
||||
# arrived on: a server that lists only `10.0.0.1:443` never sees a connection
|
||||
# to another address, and a server that lists only the wildcard never sees one
|
||||
# to `10.0.0.1:443` once some server lists that address. So a vhost reachable
|
||||
# on a private address must list it explicitly, and so must every vhost that
|
||||
# should keep answering there (including the `default_server`, or an unknown
|
||||
# `Host` on the private address lands on an arbitrary private vhost).
|
||||
{ lib }:
|
||||
rec {
|
||||
# nginx wants an IPv6 literal in brackets (`[fd00::1]:443`). Accept the bare
|
||||
# spelling too.
|
||||
bracket = addr: if lib.hasInfix ":" addr && !lib.hasPrefix "[" addr then "[${addr}]" else addr;
|
||||
|
||||
# The spellings of "every address". Listing one as a private address would
|
||||
# open the private vhosts on the public one.
|
||||
wildcards = [
|
||||
"0.0.0.0"
|
||||
"[::]"
|
||||
"[::0]"
|
||||
"*"
|
||||
];
|
||||
isWildcard = addr: lib.elem (bracket addr) wildcards;
|
||||
|
||||
# One address, both ports: plain http on `port`, TLS on `httpsPort`.
|
||||
pair = cfg: addr: [
|
||||
{
|
||||
inherit addr;
|
||||
port = cfg.port;
|
||||
}
|
||||
{
|
||||
inherit addr;
|
||||
port = cfg.httpsPort;
|
||||
ssl = true;
|
||||
}
|
||||
];
|
||||
|
||||
# `privateListenAddresses`, bracketed and deduplicated: nginx refuses a
|
||||
# duplicate `listen`.
|
||||
privateAddrs = cfg: lib.unique (map bracket cfg.privateListenAddresses);
|
||||
|
||||
# The listens of a `public = false` vhost.
|
||||
private = cfg: lib.concatMap (pair cfg) (privateAddrs cfg);
|
||||
|
||||
# The listens of a `public = true` vhost: the wildcard (IPv6 as well when
|
||||
# `listenIPv6`), plus the private addresses, which each have a server set of
|
||||
# their own.
|
||||
public =
|
||||
cfg: lib.concatMap (pair cfg) ([ "0.0.0.0" ] ++ lib.optional cfg.listenIPv6 "[::]") ++ private cfg;
|
||||
}
|
||||
|
|
@ -145,6 +145,16 @@ in
|
|||
request with a response, such as the catch-all.
|
||||
'';
|
||||
};
|
||||
options.public = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether this vhost answers on every address the gateway listens
|
||||
on. `false` binds it to `privateListenAddresses` only, so the
|
||||
public address never serves it (see that option). Needs a
|
||||
non-empty `privateListenAddresses`.
|
||||
'';
|
||||
};
|
||||
options.certificate = lib.mkOption {
|
||||
type = lib.types.nullOr (
|
||||
lib.types.submodule {
|
||||
|
|
@ -177,7 +187,9 @@ in
|
|||
config — and the gateway renders it into
|
||||
`services.nginx.virtualHosts.<name>` with the parts it owns:
|
||||
|
||||
- `listen`, the gateway's port pair;
|
||||
- `listen`, the gateway's port pair on its addresses
|
||||
(`gateway.lib.listen`, or `gateway.lib.privateListen` for a vhost
|
||||
that sets `public = false`);
|
||||
- the TLS attributes for that name (`gateway.lib.tlsFor`, or
|
||||
`certificate` below), with `addSSL` dropped when the vhost sets
|
||||
`forceSSL`;
|
||||
|
|
@ -203,12 +215,23 @@ in
|
|||
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.
|
||||
Read-only: the `listen` set of a vhost that answers on every
|
||||
address (plain http on `port`, TLS on `httpsPort`; on `0.0.0.0`,
|
||||
on `[::]` when `listenIPv6` is set, and on each of
|
||||
`privateListenAddresses`). Applied to every vhost declared under
|
||||
`vhosts` that keeps `public = true`, 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.
|
||||
'';
|
||||
};
|
||||
|
||||
privateListen = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.attrsOf lib.types.raw);
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
description = ''
|
||||
Read-only: the `listen` set of a vhost with `public = false`: the
|
||||
same port pair, on `privateListenAddresses` only.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -319,6 +342,45 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
privateListenAddresses = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"10.100.0.1"
|
||||
"fd00:100::1"
|
||||
];
|
||||
description = ''
|
||||
Addresses of this host, IPv4 or IPv6 (bare `fd00::1` or bracketed),
|
||||
that the vhosts with `public = false` listen on, on `port` and
|
||||
`httpsPort`. Not the wildcard: `0.0.0.0` and `::` are refused,
|
||||
since they would open those vhosts on the public address too.
|
||||
|
||||
The vhosts that stay public listen on these addresses as well as on
|
||||
the wildcard, so a client that reaches the host by a private address
|
||||
still gets them; and so does the catch-all, so an unknown `Host` on a
|
||||
private address closes the connection rather than being served by
|
||||
a private vhost.
|
||||
|
||||
The addresses must exist on the host when nginx starts (a
|
||||
WireGuard interface's address, say), or nginx fails to bind. Empty
|
||||
by default: every vhost is public, as before.
|
||||
'';
|
||||
};
|
||||
|
||||
listenIPv6 = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = config.networking.enableIPv6;
|
||||
defaultText = lib.literalExpression "config.networking.enableIPv6";
|
||||
description = ''
|
||||
Listen on `[::]` as well as `0.0.0.0`, on `port` and `httpsPort`.
|
||||
The IPv6 listener takes IPv6 connections only; the `0.0.0.0` one
|
||||
keeps the IPv4 ones. Follows `networking.enableIPv6`; set it to
|
||||
`false` on a host whose kernel cannot open an IPv6 socket.
|
||||
|
||||
`openFirewall` opens the ports for both address families.
|
||||
'';
|
||||
};
|
||||
|
||||
statusPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8085;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
#
|
||||
# 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.
|
||||
# knowledge — which addresses and port pair to bind, which issuer covers a
|
||||
# name.
|
||||
{
|
||||
lib,
|
||||
cfg, # services.hyperhive.gateway
|
||||
|
|
@ -24,6 +25,8 @@
|
|||
caBundle, # hive TLS trust bundle on the host
|
||||
}:
|
||||
let
|
||||
listenSets = import ./listen.nix { inherit lib; };
|
||||
|
||||
# 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
|
||||
|
|
@ -52,18 +55,13 @@ in
|
|||
# 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/4-implementation/networking/gateway.md`
|
||||
# ("TLS modes").
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = cfg.port;
|
||||
}
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = cfg.httpsPort;
|
||||
ssl = true;
|
||||
}
|
||||
];
|
||||
# ("TLS modes", "Listen addresses").
|
||||
#
|
||||
# `listen` is for a vhost that answers everywhere: the wildcard plus the
|
||||
# private addresses. `privateListen` is for one that answers only on the
|
||||
# private addresses.
|
||||
listen = listenSets.public cfg;
|
||||
privateListen = listenSets.private cfg;
|
||||
|
||||
# TLS attrs for one vhost, by name. A swarm service's name may sit
|
||||
# outside this hive's domain — and then the hive leaf's role
|
||||
|
|
|
|||
Loading…
Reference in a new issue