# 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.` 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; }