verifiedProxyTo (43ae164d) verified TLS but left Host to nixpkgs'
recommendedProxySettings, which sets Host to the CALLING vhost, not
the target. Since every consumer resolves back to this same gateway,
nginx picks the vhost to answer by Host header (not by the SNI
proxy_ssl_name already sends) — so every auth subrequest looped back
into its own vhost's auth_request, recursing until nginx's subrequest
depth limit turned it into a 500, on every domain gated by SSO.
Pin Host (and the rest of the header set nixpkgs' recommended include
would otherwise still be the one to set) inside verifiedProxyTo, and
set recommendedProxySettings = false on each of the four call sites so
nixpkgs' own copy — appended after a location's extraConfig — can't
clobber it back.
164 lines
7 KiB
Nix
164 lines
7 KiB
Nix
# 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)
|
|
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
|
|
caBundle, # hive CA trust bundle on the host (root + intermediate)
|
|
}:
|
|
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;
|
|
|
|
# Dial another service on this hive BY NAME over https, verified.
|
|
#
|
|
# One definition rather than a copy per module: nginx verifies
|
|
# nothing by default (`proxy_ssl_verify` is off), so a `proxy_pass
|
|
# https://…` without these lines is encrypted and unauthenticated.
|
|
# That failure is invisible — it works, and keeps working, against
|
|
# any certificate at all.
|
|
#
|
|
# Every line earns its place, each confirmed against a real nginx
|
|
# with the opposite arm run as a control:
|
|
# verify + depth — the chain is leaf -> intermediate -> root
|
|
# trusted_cert — the bundle; nginx reads ALL certs in the file,
|
|
# which the bundle's own doc warns is not true of
|
|
# every consumer
|
|
# ssl_name — checks the HOSTNAME too. Without it a chain-only
|
|
# check accepts any certificate this CA ever
|
|
# signed, and for an internal CA that is every
|
|
# service on the hive
|
|
# server_name on — sends SNI, or the far end cannot pick a cert
|
|
#
|
|
# ⚠️ `proxy_ssl_session_reuse` is left at its default (on) and that is
|
|
# deliberate: this is used on per-request auth subrequests, so the
|
|
# handshake it avoids is paid on every request. Worth knowing when
|
|
# testing though — the session cache is keyed by upstream address and
|
|
# NOT by trust config, so two locations pointing at one upstream with
|
|
# different trust do not verify independently.
|
|
#
|
|
# ⚠️ Also pins `Host` (and reinstates the rest of nginx's
|
|
# `recommendedProxySettings` header set) to `name` rather than leaving
|
|
# it to be filled in later. `name` here resolves back to THIS gateway
|
|
# — every consumer dials another vhost on the same nginx, not a
|
|
# separate host — and nginx picks the vhost to answer an HTTPS request
|
|
# from the `Host` header, not from the TLS SNI that `proxy_ssl_name`
|
|
# above sends. `recommendedProxySettings`'s own `Host $host` (the
|
|
# CALLER's host, not the target) is textually appended by nixpkgs
|
|
# AFTER a location's `extraConfig` — so it always wins over a
|
|
# `proxy_set_header Host` written in the location body, and the
|
|
# subrequest loops back into the ORIGINAL vhost instead of reaching
|
|
# the target, recursing on its own `auth_request` until nginx's
|
|
# subrequest-depth limit turns it into a plain 500. Every call site
|
|
# sets `recommendedProxySettings = false` on the location for exactly
|
|
# this reason — nixpkgs' version would still clobber this one.
|
|
verifiedProxyTo = name: ''
|
|
proxy_ssl_verify on;
|
|
proxy_ssl_verify_depth 3;
|
|
proxy_ssl_trusted_certificate ${caBundle};
|
|
proxy_ssl_name ${name};
|
|
proxy_ssl_server_name on;
|
|
proxy_set_header Host ${name};
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Server $hostname;
|
|
'';
|
|
|
|
# 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;''}
|
|
'';
|
|
|
|
# The gateway's styled error pages, re-exported so a service module
|
|
# can point an `error_page` at one. Republished rather than imported
|
|
# per module for the same reason as everything else in this kit: these
|
|
# carry the hive's branding, and a service rendering its own would
|
|
# drift from the rest of the gateway the first time the theme changes.
|
|
inherit errorPages;
|
|
}
|