Watch
0
0
Fork
You've already forked hyperhive
0

hive-tls: move the controllers' CA env settings into glue

This commit is contained in:
atlas 2026-10-17 15:49:19 +02:00 • committed by mara
commit e21a03d6ff
7 changed files with 85 additions and 41 deletions

View file

@ -37,6 +37,7 @@
./glue-grafana-oidc-client.nix
./glue-hive-otel-authelia.nix
./glue-hive-tls-bao-identity.nix
./glue-hive-tls-controllers.nix
./glue-logs-auth-bao-identity.nix
./glue-metrics-auth-bao-identity.nix
./glue-matrix-ctl-bao-identity.nix

View file

@ -0,0 +1,55 @@
# Glue: hand the hive's trust bundle to the daemons that dial the swarm over TLS.
#
# ONE PAIRING PER FILE — hive-c0re and the swarm controller ← the trust bundle
# ./hive-tls.nix assembles. Deleting this leaves daemons that trust only the
# platform roots: hive-c0re's lifecycle cannot hand agents the bundle, and the
# swarm-queue clients die at `invalid peer certificate: UnknownIssuer` against
# an authelia signed by the swarm CA.
#
# `HIVE_TLS_CA_PATH` signals the hive-c0re lifecycle that a trust bundle exists:
# it bind-mounts the file (read-only, public certs ONLY — never a key) into each
# agent container, and the meta flake wires the per-agent trust bundle.
# `*_OIDC_CA_FILE` names the same anchor for the swarm-queue clients that mint a
# token from authelia over TLS. `swarm-queue-client` builds a bare
# `reqwest::Client`, so it trusts only the platform roots without it. Where the
# gateway is not self-signed ./hive-tls.nix contributes no bundle and nothing is
# set here, which is correct for a swarm fronted by a public certificate.
#
# ⚠️ One gate per daemon, each ANDed with the bundle existing: the gateway also
# runs on hosts with no hive and no controller, and defining an environment key
# on a unit that does not exist CREATES a unit fragment for it — inert (no
# `ExecStart`, empty `wantedBy`, never activated), and it evaluates and builds
# clean.
#
# ⚠️ Files like this are the only place a `deploy.<foo>` value may derive from
# a `deploy.<bar>.enable`. Everywhere else that is forbidden.
{
lib,
config,
...
}:
let
hyperhiveCfg = config.services.hyperhive;
deployCfg = hyperhiveCfg.deploy;
gatewayCfg = hyperhiveCfg.gateway;
# ./hive-tls.nix writes the bundle only where the gateway serves its own
# self-signed cert.
haveBundle = gatewayCfg.enable && gatewayCfg.useSelfSigned;
trustBundle = "${deployCfg.hive-controller.tls.stateDir}/trust-bundle.pem";
in
{
config = lib.mkIf haveBundle (
lib.mkMerge [
(lib.mkIf deployCfg.hive-controller.enable {
systemd.services.hive-c0re.environment = {
HIVE_TLS_CA_PATH = trustBundle;
HIVE_C0RE_OIDC_CA_FILE = trustBundle;
};
})
(lib.mkIf deployCfg.swarm-controller.enable {
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE = trustBundle;
})
]
);
}

View file

@ -131,7 +131,7 @@ in
# gateway vhost, which serves a leaf chaining to the store's pki root —
# a CA generated at runtime, which no build-time trust store can
# contain. The queue's TLS is verified against `HIVE_C0RE_OIDC_CA_FILE`
# (../hive-tls.nix), not this bundle.
# (../glue-hive-tls-controllers.nix), not this bundle.
#
# ⚠️ `SSL_CERT_FILE` REPLACES the trust store, and unlike the other
# consumers of this helper, hive-c0re already makes a *public*-TLS call:

View file

@ -929,41 +929,5 @@ in
Persistent = true;
};
};
# Signal the hive-c0re lifecycle that a trust bundle exists: it bind-mounts
# this file (read-only, public certs ONLY — never a key) into each
# agent container so agents + their tools can trust the gateway's
# self-signed leaf, and the meta flake wires the per-agent trust
# bundle. No key path is ever exposed (an agent that could read one
# could mint trusted certs).
#
# ⚠️ Every line here that names another daemon's unit is gated on that
# daemon's own enable: the gateway also runs on hosts with no hive and no
# controller, and defining an environment key on a unit that does not
# exist CREATES a unit fragment for it — inert (no `ExecStart`, empty
# `wantedBy`, never activated), and it evaluates and builds clean.
systemd.services.hive-c0re.environment.HIVE_TLS_CA_PATH =
lib.mkIf hyperhiveCfg.deploy.hive-controller.enable "${cfg.stateDir}/trust-bundle.pem";
# The same anchor, named for the swarm-queue clients that need it when
# they mint a token from authelia over TLS. Declared HERE, beside the
# bundle, rather than in each consumer's module: the path is this
# module's fact, and two consumers re-deriving `${stateDir}/…` would be
# two places to fix when it changes.
#
# ⚠️ `swarm-queue-client` builds a bare `reqwest::Client`, so it trusts
# only the platform roots and dies at `invalid peer certificate:
# UnknownIssuer` against a swarm whose authelia is signed by the swarm
# CA. The bundle is already assembled on disk and handed to hive-c0re
# under a different variable name; this declaration is what points the
# queue client at it.
#
# Where the gateway is not self-signed this module contributes nothing, and
# the clients then fall back to the platform roots — which is correct for
# a swarm fronted by a public certificate.
systemd.services.hive-c0re.environment.HIVE_C0RE_OIDC_CA_FILE =
lib.mkIf hyperhiveCfg.deploy.hive-controller.enable "${cfg.stateDir}/trust-bundle.pem";
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_OIDC_CA_FILE =
lib.mkIf hyperhiveCfg.deploy.swarm-controller.enable "${cfg.stateDir}/trust-bundle.pem";
};
}

View file

@ -1011,7 +1011,7 @@ let
# controller, so the host that runs it says so itself.
#
# Probed by what the daemon needs in order to run, not by
# `? swarm-controller`: ./host-modules/hive-tls.nix defines an
# `? swarm-controller`: ./host-modules/glue-hive-tls-controllers.nix defines an
# environment key on that unit name, which leaves the attr
# present-but-inert (no `ExecStart`, empty `wantedBy`) on every hive
# that has a CA — see the comment there. The daemon's system user has no

View file

@ -684,6 +684,30 @@ let
&& (t.timerConfig.Unit or "hive-tls-resign.service") == "hive-tls-resign.service"
&& !(remainsAfterExit leafUnit);
}
{
# ./glue-hive-tls-controllers.nix. The controllers are gated apart: the
# hive host without the swarm controller sets the first two only, and a
# gateway with neither sets none. The first arm is the presence control.
name = "the trust bundle reaches each controller's environment only where that controller runs";
ok =
let
bundle = "/var/lib/hive-tls/trust-bundle.pem";
envOf = m: unit: m.systemd.services.${unit}.environment or { };
hiveVars = [
"HIVE_TLS_CA_PATH"
"HIVE_C0RE_OIDC_CA_FILE"
];
anyOf =
m: unit: vars:
lib.any (v: (envOf m unit) ? ${v}) vars;
in
lib.all (v: (envOf allLocal "hive-c0re").${v} == bundle) hiveVars
&& (envOf allLocal "swarm-controller").SWARM_CONTROLLER_OIDC_CA_FILE == bundle
&& lib.all (v: (envOf noService "hive-c0re").${v} == bundle) hiveVars
&& !(anyOf noService "swarm-controller" [ "SWARM_CONTROLLER_OIDC_CA_FILE" ])
&& !(anyOf gatewayNoHive "hive-c0re" hiveVars)
&& !(anyOf gatewayNoHive "swarm-controller" [ "SWARM_CONTROLLER_OIDC_CA_FILE" ]);
}
{
# A oneshot has no start timeout by default, so an attempt that never
# ends never fails into a retry.