Watch
0
0
Fork
You've already forked hyperhive
0

hive-tls: no hive leaf or hive identity on a gateway host without a hive

A self-signed gateway on a host with deploy.hive-controller.enable off
(a dedicated swarm-services host) needed the hive leaf for its catch-all,
and that leaf now comes only from hive-tls-resign with a per-hive store
identity. hive-tls-resign and its timer are now defined only where a hive
runs; elsewhere hive-gateway-self-signed-cert installs the swarm-services
leaf as the default cert, and the gateway waits for swarm-services-cert.
Also corrects the hive-gateway comments and option text that still said
the leaf is hive-CA-signed.

Refs #4588
This commit is contained in:
atlas 2026-10-04 16:56:10 +02:00 • committed by mara
commit 75a7dccee6
6 changed files with 90 additions and 24 deletions

View file

@ -25,6 +25,10 @@ let
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
matrixCfg = config.services.hyperhive.swarm.matrix;
matrixDeployCfg = config.services.hyperhive.deploy.matrix;
tlsStateDir = config.services.hyperhive.deploy.hive-controller.tls.stateDir;
# The hive leaf exists only where a hive runs; ../hive-tls.nix issues it
# under that same condition.
runsHive = config.services.hyperhive.deploy.hive-controller.enable;
# Every vhost claiming `default_server`, ours and the operator's
# alike. Computed once so the assertion below and the message it
@ -45,7 +49,7 @@ let
# Self-signed TLS is the implicit floor: when neither an operator cert
# (`tls.certDir`) nor ACME (`tls.acme.enable`) is configured, the gateway
# generates + serves a hive-CA-signed leaf (see hive-tls.nix). There is no
# serves a leaf the secret store issues (see hive-tls.nix). There is no
# explicit toggle and no http-only mode — matrix discovery requires https,
# so the gateway always terminates TLS.
# `cfg.useSelfSigned` (options.nix) is the derived single source of truth.
@ -57,15 +61,16 @@ let
# certs for no gain.
tlsDir = "/var/lib/hive-gateway/tls";
# TLS cert + key paths.
# - self-signed (default): the hive-CA-signed leaf, imported into the
# state dir by `hive-gateway-self-signed-cert` below.
# - self-signed (default): the hive leaf the store issues (the
# swarm-services leaf on a host with no hive), imported into the state
# dir by `hive-gateway-self-signed-cert` below.
# - tls.certDir set: the operator's own cert dir, read directly.
tlsCert =
if cfg.tls.certDir != null then "${cfg.tls.certDir}/${cfg.tls.certName}" else "${tlsDir}/cert.pem";
tlsKey =
if cfg.tls.certDir != null then "${cfg.tls.certDir}/${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
# The swarm-services pair, used only by the vhosts whose names the
# hive leaf cannot carry. Self-signed mode only: with an operator cert
# or ACME the operator owns every name and there is no second issuer.
svcCert = "${tlsDir}/swarm-services.pem";
svcKey = "${tlsDir}/swarm-services-key.pem";
@ -244,7 +249,7 @@ in
defaults.email = cfg.tls.acme.email;
};
# Import the hive-CA leaf into nginx's state dir before nginx starts.
# Import the gateway leaves into nginx's state dir before nginx starts.
#
# 🚨 DO NOT "simplify" this into pointing nginx at the CA dir. It does
# TWO jobs: it re-modes the leaf to the 0640 root:nginx copy below (a
@ -292,18 +297,27 @@ in
# read it — a 0600 root:root key passes the master load but
# fails the pre-start config test with `BIO_new_file() …
# Permission denied`, blocking the unit. Cert is world-read.
install -m 0644 ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/gateway.pem ${tlsCert}
install -m 0640 -g nginx ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/gateway-key.pem ${tlsKey}
${
if runsHive then
''
install -m 0644 ${tlsStateDir}/gateway.pem ${tlsCert}
install -m 0640 -g nginx ${tlsStateDir}/gateway-key.pem ${tlsKey}''
else
''
# No hive here, so no hive leaf: the catch-all and any other
# vhost on the hive cert path serve the swarm-services leaf.
install -m 0644 ${tlsStateDir}/swarm-services.pem ${tlsCert}
install -m 0640 -g nginx ${tlsStateDir}/swarm-services-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.
# because the hive leaf's role allows only 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, and issuance
# can also fail on a host that wants one.
# Absent is a normal state, not a failure: a host fronting
# no swarm name has none, and issuance can also fail on a
# host that wants one.
#
# ⚠️ When it is absent the HIVE leaf goes to this path
# anyway, and that fallback is load-bearing rather than

View file

@ -159,7 +159,7 @@ in
Which issuer covers a name is **gateway** knowledge, not the
service's: a swarm service's name can sit outside this hive's
domain, and the hive CA is name-constrained out of it, so that
domain, and the hive leaf's role refuses it, so that
vhost must serve the swarm-services leaf while everything else
keeps the hive leaf. A service module calls this instead of
deciding — deciding is how the vhost and the cert stop
@ -231,7 +231,7 @@ in
defaultText = lib.literalExpression "tls.certDir == null && !tls.acme.enable";
description = ''
Read-only derived flag: `true` when the gateway serves the
self-signed (hive-CA-signed) leaf — i.e. neither `tls.certDir` nor
self-signed leaf the secret store issues — i.e. neither `tls.certDir` nor
`tls.acme.enable` is configured. Single source of truth for the
self-signed condition; consumed by the `hive-tls` and `hive-ci`
modules so the derivation isn't duplicated. Internal — not meant to
@ -261,7 +261,7 @@ in
Path to a host directory containing a TLS certificate and
private key for nginx. When set, nginx listens on `httpsPort`
and uses this cert, overriding the self-signed default — the
auto-generated hive-CA-signed leaf is skipped entirely.
store-issued self-signed leaf is skipped entirely.
nginx reads `<certDir>/<tls.certName>` and
`<certDir>/<tls.keyName>` directly — it runs on the host, so

View file

@ -19,11 +19,11 @@
cfg, # services.hyperhive.gateway
tlsCert,
tlsKey,
svcCert, # swarm-services leaf, for names the hive CA cannot sign
svcCert, # swarm-services leaf, for names the hive leaf cannot carry
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)
caBundle, # hive TLS trust bundle on the host
}:
let
# nixos `services.nginx.virtualHosts.<name>` ssl attrs for a vhost
@ -68,8 +68,8 @@ in
];
# 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
# outside this hive's domain — and then the hive leaf's role
# refuses 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

View file

@ -46,8 +46,16 @@ let
# Nor on a host fronting no swarm name: it asks the store for the root
# alone, which nginx never reads, and a sealed store would otherwise hold
# its gateway down.
#
# On a host with no hive there is no hive leaf to start on: the services
# leaf is the only certificate its gateway has, so it always waits.
gatewayWaitsForServicesLeaf =
localServiceDomains != [ ] && localServiceDomains != [ baoCfg.ui.domain ];
localServiceDomains != [ ] && (!runsHive || localServiceDomains != [ baoCfg.ui.domain ]);
# The hive leaf, `hive-tls-resign` and the hive identity it needs exist only
# where a hive runs. A gateway host without one serves the swarm-services
# leaf in their place (../hive-gateway/default.nix).
runsHive = hyperhiveCfg.deploy.hive-controller.enable;
# The gateway's own leaf is only issued when the gateway actually serves a
# self-signed cert: the gateway must run here and be in self-signed mode.
@ -458,7 +466,7 @@ in
# propagation failure means the leaf rotates on disk while nginx keeps
# serving the old copy until it expires, with this unit reporting
# success the whole time.
systemd.services.hive-tls-resign = {
systemd.services.hive-tls-resign = lib.mkIf runsHive {
description = "Issue or renew the gateway TLS leaf from the secret store's PKI";
wantedBy = [ "multi-user.target" ];
# The store's container and its granting unit where they run here;
@ -941,7 +949,7 @@ in
# Daily, for the reason `swarm-services-cert-renew` below is: the leaf
# needs the store, which can be sealed or unreachable.
systemd.timers.hive-tls-resign = {
systemd.timers.hive-tls-resign = lib.mkIf runsHive {
description = "Daily renewal check for the gateway TLS leaf";
wantedBy = [ "timers.target" ];
timerConfig = {

View file

@ -30,6 +30,14 @@ let
deploy.allSwarmServices = true;
};
# A services host with hives elsewhere and the store elsewhere too: no hive
# and no store identity minted here.
swarmServicesOnlyNoStore = hive {
deploy.hive-controller.enable = false;
deploy.allSwarmServices = true;
deploy.bao.enable = false;
};
# The same switch with every service it owns placed elsewhere, so the only
# thing that can enable the gateway/resolver/bridge here is that toggle's
# own module — each service module asserts them from its own `enable`.
@ -187,6 +195,36 @@ let
&& swarmServicesSwitchAlone.services.hyperhive.gateway.dns.enable
&& swarmServicesSwitchAlone.services.hyperhive.network.enable;
}
{
# No hive, so no hive leaf and no hive identity: the gateway's default
# certificate is the swarm-services leaf, and its start waits for that
# leaf. The control is a hive host with the same services, which keeps
# the hive leaf and the unit that issues it.
name = "a gateway host without a hive serves the swarm-services leaf as its default cert and has no hive-leaf unit";
ok =
let
noHive =
m:
let
s = m.systemd.services;
imp = s.hive-gateway-self-signed-cert.script;
in
!(s ? hive-tls-resign)
&& !(m.systemd.timers ? hive-tls-resign)
&& lib.hasInfix "install -m 0644 /var/lib/hive-tls/swarm-services.pem /var/lib/hive-gateway/tls/cert.pem" imp
&& !(lib.hasInfix "/var/lib/hive-tls/gateway.pem /var/lib/hive-gateway/tls/cert.pem" imp)
&& lib.elem "hive-gateway-self-signed-cert.service" s.swarm-services-cert.requiredBy
&& lib.elem "hive-gateway-self-signed-cert.service" s.swarm-services-cert.before
# Fronting swarm names, so the root-only branch that presents the
# hive identity is not the one that runs.
&& lib.hasInfix "want_svc=1" s.swarm-services-cert.script;
hs = swarmServicesHere.systemd.services;
in
noHive swarmServicesOnly
&& noHive swarmServicesOnlyNoStore
&& hs ? hive-tls-resign
&& lib.hasInfix "install -m 0644 /var/lib/hive-tls/gateway.pem /var/lib/hive-gateway/tls/cert.pem" hs.hive-gateway-self-signed-cert.script;
}
{
# Each service is gated on its own deployment toggle alone, so a host
# that runs no hive still runs every shared service the switch turns