Watch
0
0
Fork
You've already forked hyperhive
0

hive-tls: issue the gateway leaf from bao with a per-hive identity

Every hive gets its own store identity (CN
<deploy.bao.hiveLeafCommonNamePrefix>-<hive>) and its own role on the
existing pki mount (<deploy.bao.hiveLeafPkiRolePrefix>-<hive>), whose
allowed_domains is the hive's domain and *.<domain>. The policy grants
update on that one issue path. swarm-bao-hive-leaf-policy writes role,
policy and cert-auth role for every hive in swarm.hives; the store's host
mints its own hive's identity in glue-bao-tls.nix and a new glue file
points deploy.hive-controller.tls.baoHiveIdentity{Cert,Key}File at it.
Hives on other hosts place it by hand. swarm.nix reserves the composed CN.

hive-tls-ca no longer generates a hive CA or signs a leaf: caGenScript,
signLeafScript/signHiveLeaf and the adoption block are gone, ca-key.pem
is deleted where it exists, and the unit only assembles trust-bundle.pem.
The retired ca.pem/ca-previous.pem stay in the bundle so leaves they
signed keep verifying.

hive-tls-resign becomes the bao issuance: at boot, on deploy and from a
daily timer it re-issues when the leaf is missing, past half the role's
lifetime, missing a name, or not chained to the store's pki root. Nothing
orders the gateway after it, so a valid leaf on disk never waits on the
store; a failed renewal leaves the leaf in place. A missing identity
fails the unit naming both options, as does the root fetch on a hive
fronting no swarm name, which now presents this identity instead of the
reader cert.

caValidityDays and leafValidityDays are removed (both spellings), since
nothing reads them.

Refs #4588
This commit is contained in:
atlas 2026-10-04 15:11:48 +02:00 • committed by mara
commit 5526daab97
16 changed files with 859 additions and 628 deletions

View file

@ -621,10 +621,9 @@ let
# also what an untuned mount clamps the ROOT to, so the two collided at
# exactly the length that makes issuance impossible; and the guard below
# compares the issuer's remaining life against this number, which it can
# only do if this number exists. 720h matches
# `deploy.hive-controller.tls.leafValidityDays`, the 30 days ./hive-tls.nix
# documents for every other leaf it holds. An option, read here and by
# ./hive-tls.nix's renewal threshold, so the two cannot disagree.
# only do if this number exists. Every per-hive gateway-leaf role carries it
# too. An option, read here and by ./hive-tls.nix's renewal thresholds, so
# the two cannot disagree.
servicesPkiLeafTtlHours = baoDeploy.servicesPkiLeafTtlHours;
servicesPkiLeafTtl = "${toString servicesPkiLeafTtlHours}h";
servicesPkiLeafTtlSeconds = servicesPkiLeafTtlHours * 3600;
@ -676,6 +675,15 @@ let
}
'';
# One gateway-leaf issuer per hive in the swarm directory, for the reason
# `perHiveReaders` below is per hive. One name for the pki role, the policy
# and the cert-auth role, as for the readers.
hiveLeafIssuers = lib.mapAttrsToList (hiveName: hive: {
name = "${baoDeploy.hiveLeafPkiRolePrefix}-${hiveName}";
cn = "${baoDeploy.hiveLeafCommonNamePrefix}-${hiveName}";
inherit (hive) domain;
}) hyperhiveCfg.swarm.hives;
# The PKI mount agent client certificates are issued from. Its root is
# generated inside the store, so the agent CA's key never exists outside it.
# A mount of its own because the services mount holds exactly one issuer; a
@ -1660,6 +1668,35 @@ in
'';
};
hiveLeafCommonNamePrefix = lib.mkOption {
type = lib.types.str;
default = "swarm-bao-hive-leaf";
example = "swarm-bao-hive-leaf.svc";
description = ''
Prefix of the subject the store's per-hive gateway-leaf roles accept.
The role for hive `h` accepts `<prefix>-h` and may only issue from
`<servicesPkiMountPath>/issue/<hiveLeafPkiRolePrefix>-h`, which issues
for `h`'s domain and `*.` that domain and nothing else.
Per hive for the reason
{option}`services.hyperhive.deploy.bao.matrixTokenCommonNamePrefix` is:
one subject for every hive could ask for any hive's certificate.
⚠️ Reserved as a hive name by ./swarm.nix, per hive, like its siblings.
'';
};
hiveLeafPkiRolePrefix = lib.mkOption {
type = lib.types.str;
default = "swarm-hive-leaf";
description = ''
Prefix of the per-hive roles on
{option}`services.hyperhive.deploy.bao.servicesPkiMountPath` that each
hive's gateway leaf is issued through: `<prefix>-<hive>`. The same name
is that hive's policy and cert-auth role. Must start with `swarm-`.
'';
};
grafanaOidcCommonName = lib.mkOption {
type = lib.types.str;
default = "swarm-bao-grafana-oidc";
@ -2067,14 +2104,18 @@ in
lib.hasPrefix "swarm-" servicesPkiRoleName
&& lib.hasPrefix "swarm-" natsPkiRoleName
&& lib.hasPrefix "swarm-" agentPkiRoleName
&& lib.hasPrefix "swarm-" baoDeploy.hiveLeafPkiRolePrefix
);
message = ''
services.hyperhive.deploy.bao.servicesPkiRoleName
(${servicesPkiRoleName}),
services.hyperhive.deploy.bao.natsPkiRoleName (${natsPkiRoleName})
and services.hyperhive.deploy.bao.agentPkiRoleName
(${agentPkiRoleName}) must all start with `swarm-`: the bao granter
that writes them may write pki roles under that prefix only.
services.hyperhive.deploy.bao.natsPkiRoleName (${natsPkiRoleName}),
services.hyperhive.deploy.bao.agentPkiRoleName
(${agentPkiRoleName}) and
services.hyperhive.deploy.bao.hiveLeafPkiRolePrefix
(${baoDeploy.hiveLeafPkiRolePrefix}) must all start with `swarm-`:
the bao granter that writes them may write pki roles under that
prefix only.
'';
}
];
@ -3014,6 +3055,71 @@ in
'';
};
# A SIXTH sibling, the nats unit's shape once per hive: each hive's
# gateway-leaf role, its policy and its login role. The role allows the
# hive's domain and `*.` that domain, and no IP SANs or localhost.
systemd.services.swarm-bao-hive-leaf-policy = lib.mkIf haveGranter {
description = "write the per-hive gateway-leaf pki roles, bao policies and cert-auth roles";
after = [
"container@${cfg.machine}.service"
"swarm-bao-controller-policy.service"
]
++ granterAfter;
requires = [ "swarm-bao-pki.service" ];
wantedBy = [ "multi-user.target" ];
path = [
baoCli
pkgs.coreutils
];
environment = granterEnv;
# Same unseal wait as its siblings above.
startLimitBurst = 2880;
startLimitIntervalSec = 90000;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Restart = "on-failure";
RestartSec = 30;
};
script = ''
set -euo pipefail
${granterLogin}
''
+ lib.concatMapStrings (obj: ''
bao write ${lib.escapeShellArg "${servicesPkiMountPath}/roles/${obj.name}"} \
allowed_domains=${lib.escapeShellArg "${obj.domain},*.${obj.domain}"} \
allow_bare_domains=true \
allow_subdomains=false \
allow_glob_domains=true \
allow_wildcard_certificates=true \
allow_localhost=false \
allow_any_name=false \
allow_ip_sans=false \
enforce_hostnames=true \
server_flag=true \
client_flag=false \
key_type=rsa \
key_bits=4096 \
ttl=${servicesPkiLeafTtl} \
max_ttl=${servicesPkiLeafTtl}
printf '%s' ${lib.escapeShellArg ''
path "${servicesPkiMountPath}/issue/${obj.name}" {
capabilities = ["update"]
}
''} |
bao policy write ${lib.escapeShellArg obj.name} -
bao write auth/cert/certs/${lib.escapeShellArg obj.name} \
certificate=@${tlsDir}/client-ca.pem \
allowed_common_names=${lib.escapeShellArg obj.cn} \
token_policies=${lib.escapeShellArg obj.name} \
display_name=${lib.escapeShellArg obj.cn}
'') hiveLeafIssuers;
};
# The agent PKI mount: its root, its one role, the host's copy of the
# root's certificate, and the listener's trust in it.
#
@ -3041,6 +3147,7 @@ in
"swarm-bao-forwarder-oidc-policy.service"
"swarm-bao-services-issuer-policy.service"
"swarm-bao-nats-tls-policy.service"
"swarm-bao-hive-leaf-policy.service"
]
++ granterAfter;
requires = [ "swarm-bao-pki.service" ];