diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index ba9d7b37..fc484b13 100644 --- a/nix/host-modules/hive-gateway/default.nix +++ b/nix/host-modules/hive-gateway/default.nix @@ -15,6 +15,10 @@ let cfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; + # Derived once in ../swarm.nix; the vhosts that get the swarm-services + # cert are exactly the names that cert is issued for, so both read the + # same list rather than each deciding what "a swarm service" means. + swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains; matrixCfg = config.services.hyperhive.swarm.matrix; forgeCfg = config.services.hyperhive.swarm.forge; networkCfg = config.services.hyperhive.network; @@ -263,6 +267,12 @@ in if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.certName}" else "${tlsDir}/cert.pem"; tlsKey = if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.keyName}" else "${tlsDir}/key.pem"; + # The swarm-services pair, used only by the vhosts whose names + # this hive's CA cannot sign. Self-signed mode only: with an + # operator cert or ACME the operator owns every name and there + # is no second issuer in the picture. + svcCert = "${tlsDir}/swarm-services.pem"; + svcKey = "${tlsDir}/swarm-services-key.pem"; nginxTree = import ./vhosts.nix { inherit lib @@ -274,6 +284,9 @@ in swaggerUiTheme tlsCert tlsKey + svcCert + svcKey + swarmServiceDomains ; errorPages = import ./error-pages.nix { inherit pkgs; }; }; @@ -362,6 +375,23 @@ in # Permission denied`, blocking the unit. Cert is world-read. install -m 0644 /run/hive-ca/gateway.pem ${tlsCert} install -m 0640 -g nginx /run/hive-ca/gateway-key.pem ${tlsKey} + + # The swarm-services leaf, when this host issues one. It is + # a separate pair rather than more SANs on the one above + # because no hive CA can sign these names — each is + # constrained to its own hive's domain and the service + # names are siblings of it. + # + # Absent is a normal state, not a failure: the leaf exists + # only where the swarm CA is autoconfigured. Copying it + # conditionally keeps a hive whose certs come from its + # operator working unchanged. + if [ -s /run/hive-ca/swarm-services.pem ]; then + install -m 0644 /run/hive-ca/swarm-services.pem ${svcCert} + install -m 0640 -g nginx /run/hive-ca/swarm-services-key.pem ${svcKey} + else + rm -f ${svcCert} ${svcKey} + fi ''; }; diff --git a/nix/host-modules/hive-gateway/vhosts.nix b/nix/host-modules/hive-gateway/vhosts.nix index f94395b5..8cf647d7 100644 --- a/nix/host-modules/hive-gateway/vhosts.nix +++ b/nix/host-modules/hive-gateway/vhosts.nix @@ -15,6 +15,9 @@ errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized } tlsCert, tlsKey, + svcCert, # swarm-services leaf, for names the hive CA cannot sign + svcKey, + swarmServiceDomains, # which vhosts those are (../swarm.nix derives it) }: let # The gateway always terminates TLS: self-signed is the implicit @@ -51,6 +54,25 @@ let sslCertificateKey = tlsKey; }; + # 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. + vhostTlsFor = + host: + if !cfg.tls.acme.enable && cfg.tls.certDir == null && builtins.elem host swarmServiceDomains then + { + addSSL = true; + sslCertificate = svcCert; + sslCertificateKey = svcKey; + } + else + vhostTls; + # Public-facing scheme + port-suffix for URLs the gateway # mints into responses (well-known JSON, the deprecated # `/matrix/*` 301 redirect, future absolute-URL needs): @@ -85,7 +107,7 @@ let # `forge.sshPort`. See `docs/gateway.md`. Empty attrset when the # forge isn't behind the gateway. forgeVhost = lib.optionalAttrs (forgeCfg.behindGateway or false) { - "${forgeCfg.domain}" = vhostTls // { + "${forgeCfg.domain}" = (vhostTlsFor forgeCfg.domain) // { listen = vhostListen; extraConfig = securityHeaders; locations."/" = { @@ -107,7 +129,7 @@ let # longer-prefix-wins puts `/_matrix/` ahead of `/`. See # `docs/gateway.md`. Empty attrset when matrix has no gateway host. matrixVhost = lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) { - "${matrixCfg.gatewayHost}" = vhostTls // { + "${matrixCfg.gatewayHost}" = (vhostTlsFor matrixCfg.gatewayHost) // { listen = vhostListen; extraConfig = securityHeaders; locations = {