gateway: reject unmatched Host instead of serving the dashboard
The `_` vhost was serving the hive's own surface, so every dashboard and agent-UI request matched the default server rather than a named vhost -- and so did a request for any name at all, including a raw IP. Split it: `_` keeps only `return 444`, and the hive surface moves to a vhost named for the hive domain. `_` is `mkDefault` so an operator can claim default_server themselves, plus an assertion for the case where they add one without turning ours off -- nginx refuses to start on a duplicate default_server and nixpkgs asserts nothing, so that would otherwise surface as a gateway outage at rebuild time.
This commit is contained in:
parent
786e4610f0
commit
c32a9367e4
2 changed files with 74 additions and 4 deletions
|
|
@ -24,6 +24,13 @@ let
|
||||||
matrixCfg = config.services.hyperhive.swarm.matrix;
|
matrixCfg = config.services.hyperhive.swarm.matrix;
|
||||||
networkCfg = config.services.hyperhive.network;
|
networkCfg = config.services.hyperhive.network;
|
||||||
|
|
||||||
|
# Every vhost claiming `default_server`, ours and the operator's
|
||||||
|
# alike. Computed once so the assertion below and the message it
|
||||||
|
# prints cannot disagree about what they found.
|
||||||
|
defaultVhosts = lib.filter (n: config.services.nginx.virtualHosts.${n}.default or false) (
|
||||||
|
lib.attrNames config.services.nginx.virtualHosts
|
||||||
|
);
|
||||||
|
|
||||||
# Dashboard SPA dist, static-served by nginx.
|
# Dashboard SPA dist, static-served by nginx.
|
||||||
dashboardDist = "${config.services.hyperhive.c0re.servedFrontend}/dashboard";
|
dashboardDist = "${config.services.hyperhive.c0re.servedFrontend}/dashboard";
|
||||||
|
|
||||||
|
|
@ -143,6 +150,39 @@ in
|
||||||
rather than letting dnsmasq pick.
|
rather than letting dnsmasq pick.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# nginx refuses to start with two `default_server`s on one
|
||||||
|
# address ("a duplicate default server for 0.0.0.0:<port>",
|
||||||
|
# exit 1) and nixpkgs asserts nothing — `vhost.default` is a
|
||||||
|
# plain bool rendered straight into the listen line. So without
|
||||||
|
# this, an operator adding their own default vhost gets a
|
||||||
|
# gateway that fails its config test at rebuild time, which
|
||||||
|
# takes the forge, dashboard, matrix and swarm UI with it, and
|
||||||
|
# reports a port rather than a cause.
|
||||||
|
#
|
||||||
|
# Not covered by our `mkDefault`: an operator's own vhost is a
|
||||||
|
# different option path, so nothing merges and nothing
|
||||||
|
# conflicts — priority only helps someone who already knows
|
||||||
|
# ours exists. Fail at eval and name both, so the fix
|
||||||
|
# (`services.nginx.virtualHosts.<ours>.default = false`) is
|
||||||
|
# readable from the error.
|
||||||
|
assertion = lib.length defaultVhosts <= 1;
|
||||||
|
message = ''
|
||||||
|
More than one nginx virtual host is marked `default = true`:
|
||||||
|
${lib.concatStringsSep ", " defaultVhosts}
|
||||||
|
|
||||||
|
nginx allows exactly one default server per listen address
|
||||||
|
and refuses to start otherwise, so this would fail at
|
||||||
|
service start rather than here — taking every site behind
|
||||||
|
the gateway down with it.
|
||||||
|
|
||||||
|
The gateway's own catch-all (`_`, which returns 444) is set
|
||||||
|
with `mkDefault`, so to make yours the default server turn
|
||||||
|
ours off explicitly:
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."_".default = false;
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
# Ensure the gateway state dirs exist at host boot, before anything
|
# Ensure the gateway state dirs exist at host boot, before anything
|
||||||
|
|
|
||||||
|
|
@ -219,11 +219,41 @@ let
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
virtualHosts = {
|
virtualHosts = {
|
||||||
# `tlsFor "_"`, not a separate binding: the default server is a
|
# The catch-all, and now *only* a catch-all: anything whose `Host`
|
||||||
# vhost named `_`, and a name that is not a swarm service domain
|
# matches no vhost gets an immediate 444 (close without a response)
|
||||||
# (`_` never is) resolves to the hive's own leaf — which is what
|
# rather than being served the hive's dashboard.
|
||||||
# this vhost has always served.
|
#
|
||||||
|
# `_` is the idiomatic spelling because it is not a legal hostname,
|
||||||
|
# so it can never match a request by name — it serves traffic solely
|
||||||
|
# by being `default_server`.
|
||||||
|
#
|
||||||
|
# ⚠️ It still needs TLS attrs. It listens on the https port, so a
|
||||||
|
# client connecting by IP completes a TLS handshake *before* nginx
|
||||||
|
# can look at `Host` and reject it; with no cert the vhost fails to
|
||||||
|
# load. The certificate will not match what such a client asked for
|
||||||
|
# — that is unavoidable and correct: nothing can present a valid
|
||||||
|
# cert for a name the operator never issued one for.
|
||||||
|
#
|
||||||
|
# `mkDefault` per the operator: an operator with their own
|
||||||
|
# `default = true` vhost must be able to win without fighting
|
||||||
|
# priorities. The assertion in ./default.nix catches the case where
|
||||||
|
# they add one *without* turning this off, which nginx would
|
||||||
|
# otherwise only report at runtime as a failed config test.
|
||||||
"_" = (vhostTlsFor "_") // {
|
"_" = (vhostTlsFor "_") // {
|
||||||
|
listen = vhostListen;
|
||||||
|
default = lib.mkDefault true;
|
||||||
|
extraConfig = ''
|
||||||
|
return 444;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# The hive's own surface, now reachable by NAME. This used to be
|
||||||
|
# served by the `_` vhost above: no vhost was named for the hive
|
||||||
|
# domain, so every dashboard and agent-UI request matched the
|
||||||
|
# default server instead (confirmed against 24h of nginx's access
|
||||||
|
# log — `server: _` on requests whose Host *was* the hive domain).
|
||||||
|
# Naming it is what lets the catch-all start rejecting.
|
||||||
|
${hyperhiveDomain} = (vhostTlsFor hyperhiveDomain) // {
|
||||||
listen = vhostListen;
|
listen = vhostListen;
|
||||||
locations =
|
locations =
|
||||||
matrixRedirectLocations
|
matrixRedirectLocations
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue