Watch
0
0
Fork
You've already forked hyperhive
0

hive-tls: the gateway leaf uses the services-issuer identity, not a hive's

swarm-services-cert issues the swarm-services leaf a self-signed gateway
serves, and on a host fronting no swarm name fetches the store's pki root.
It is gated on the gateway, not on a hive, yet its leaf pair was named
under deploy.hive-controller.tls and defaulted from the hive reader's cert
directory, and the root fetch presented the hive's own identity. A gateway
host with no hive had no such identity, so that branch was red by
construction.

- The pair moves to deploy.bao.servicesIssuerClient{Cert,Key}File, beside
  the store's other host-unit readers, and glue-bao-tls.nix defaults it on
  the store's host from the leaf it already mints. The old
  deploy.hive-controller.tls.baoClient{Cert,Key}File paths are
  mkRenamedOptionModule aliases. glue-services-issuer-bao-identity.nix,
  which derived the default from deploy.bao.clientCertFile, is gone.
- The no-swarm-name root fetch presents the services-issuer pair. The hive
  identity is now only hive-tls-resign's.
- A self-signed gateway with no pair fails evaluation, naming both options.
  A pair named but unreadable still fails the unit, naming the paths.

Refs #4921
This commit is contained in:
atlas 2026-10-06 09:48:16 +02:00 • committed by mara
commit 5761398d60
13 changed files with 319 additions and 173 deletions

View file

@ -72,41 +72,77 @@ let
else
"${baoDeploy.hiveLeafPkiRolePrefix}-${hyperhiveCfg.hiveName}";
# The hive's own store identity: its subject is
# `<hiveLeafCommonNamePrefix>-<hiveName>`, and its policy may issue from
# `hiveLeafRole` alone. It also carries the root fetch's handshake.
haveHiveIdentity = cfg.baoHiveIdentityCertFile != null && cfg.baoHiveIdentityKeyFile != null;
showPath = p: if p == null then "unset" else p;
hiveIdentityOptions = "services.hyperhive.deploy.hive-controller.tls.baoHiveIdentityCertFile (${showPath cfg.baoHiveIdentityCertFile}) and services.hyperhive.deploy.hive-controller.tls.baoHiveIdentityKeyFile (${showPath cfg.baoHiveIdentityKeyFile})";
# Fails the unit, naming both options, when the identity is unset or a file
# is unreadable. Every hive must hold one.
requireHiveIdentity =
# Fails the unit, naming both options, when an identity is unset or a file
# is unreadable.
requireIdentity =
{
holder,
what,
certFile,
keyFile,
certOption,
keyOption,
unsetHint ? "",
}:
purpose:
if haveHiveIdentity then
let
options = "${certOption} (${showPath certFile}) and ${keyOption} (${showPath keyFile})";
in
if certFile != null && keyFile != null then
''
missing=""
for f in ${
lib.escapeShellArgs [
cfg.baoHiveIdentityCertFile
cfg.baoHiveIdentityKeyFile
certFile
keyFile
]
}; do
[ -r "$f" ] || missing="$missing $f"
done
if [ -n "$missing" ]; then
echo "this hive has no usable store identity, so it cannot ${purpose}: cannot read$missing." >&2
echo "place the files named by ${hiveIdentityOptions}." >&2
echo "${holder} has no usable ${what}, so it cannot ${purpose}: cannot read$missing." >&2
echo "place the files named by ${options}." >&2
exit 1
fi
''
else
''
echo "this hive has no store identity, so it cannot ${purpose}." >&2
echo "set ${hiveIdentityOptions} and place both files." >&2
echo "${holder} has no ${what}, so it cannot ${purpose}." >&2
echo "set ${options} and place both files.${unsetHint}" >&2
exit 1
'';
# The hive's own store identity: its subject is
# `<hiveLeafCommonNamePrefix>-<hiveName>`, and its policy may issue from
# `hiveLeafRole` alone. Every hive must hold one.
requireHiveIdentity = requireIdentity {
holder = "this hive";
what = "store identity";
certFile = cfg.baoHiveIdentityCertFile;
keyFile = cfg.baoHiveIdentityKeyFile;
certOption = "services.hyperhive.deploy.hive-controller.tls.baoHiveIdentityCertFile";
keyOption = "services.hyperhive.deploy.hive-controller.tls.baoHiveIdentityKeyFile";
};
# The swarm-wide services-issuer identity `swarm-services-cert` presents,
# whether or not a hive runs here. Its subject is
# `servicesIssuerCommonName`, and its policy may issue from
# `pki/issue/swarm-services` alone.
servicesIssuerCert = baoDeploy.servicesIssuerClientCertFile;
servicesIssuerKey = baoDeploy.servicesIssuerClientKeyFile;
haveServicesIssuer = servicesIssuerCert != null && servicesIssuerKey != null;
requireServicesIssuer = requireIdentity {
holder = "this host";
what = "services-issuer identity";
certFile = servicesIssuerCert;
keyFile = servicesIssuerKey;
certOption = "services.hyperhive.deploy.bao.servicesIssuerClientCertFile";
keyOption = "services.hyperhive.deploy.bao.servicesIssuerClientKeyFile";
unsetHint = " Its subject must be CN=${baoDeploy.servicesIssuerCommonName}.";
};
# Shared by every unit that decides whether to re-issue a leaf. It
# lives here rather than in one of them because the guards have to
# agree: they answer the same question for different leaves
@ -195,11 +231,11 @@ let
servicesCertEnvironment = {
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
}
// lib.optionalAttrs (cfg.baoClientCertFile != null) {
BAO_CLIENT_CERT = cfg.baoClientCertFile;
// lib.optionalAttrs (servicesIssuerCert != null) {
BAO_CLIENT_CERT = servicesIssuerCert;
}
// lib.optionalAttrs (cfg.baoClientKeyFile != null) {
BAO_CLIENT_KEY = cfg.baoClientKeyFile;
// lib.optionalAttrs (servicesIssuerKey != null) {
BAO_CLIENT_KEY = servicesIssuerKey;
}
# Absent means the system trust store, which is what a deployment
# with a real CA wants and what a self-signed one must not be left
@ -208,9 +244,8 @@ let
BAO_CACERT = baoDeploy.serverCaFile;
};
# The root-only fetch. Same store and server CA as `servicesCertEnvironment`,
# but the hive's own identity: a host fronting no swarm name holds no
# services-issuer pair.
# The root-only fetch: the same store, server CA and services-issuer pair
# as `servicesCertEnvironment`, handed to curl.
storeRootCurlTls = lib.escapeShellArgs (
lib.optionals (baoDeploy.serverCaFile != null) [
"--cacert"
@ -218,9 +253,9 @@ let
]
++ [
"--cert"
cfg.baoHiveIdentityCertFile
servicesIssuerCert
"--key"
cfg.baoHiveIdentityKeyFile
servicesIssuerKey
]
);
@ -262,62 +297,21 @@ in
'';
};
baoClientCertFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/swarm-bao-pki/services-issuer.pem";
description = ''
Certificate `swarm-services-cert` presents to the secret store when it
asks for the swarm-services leaf the gateway serves.
Its subject has to be
{option}`services.hyperhive.deploy.bao.servicesIssuerCommonName` — cert
auth matches on the common name, and this is the one role whose policy
permits `pki/issue/swarm-services`.
A **separate** credential from
{option}`services.hyperhive.deploy.bao.clientCertFile`, which is this
hive's own reader identity and whose policy reads every secret the
hive is entitled to, and from
{option}`services.hyperhive.deploy.hive-controller.tls.baoHiveIdentityCertFile`,
which the root fetch on a hive fronting no swarm name presents.
On a hive that mints its own store PKI a glue module supplies the leaf
as a `mkDefault`; everywhere else it is the credential an operator
places, for the reason the store's own client certificate is.
A path, never a value.
'';
};
baoClientKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/swarm-bao-pki/services-issuer-key.pem";
description = ''
Private key for
{option}`services.hyperhive.deploy.hive-controller.tls.baoClientCertFile`.
Both or neither — a certificate with no key authenticates nothing.
'';
};
baoHiveIdentityCertFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/var/lib/swarm-bao-pki/hive-leaf.pem";
description = ''
This hive's own store identity. `hive-tls-resign` presents it to ask
the secret store for the gateway leaf, and `swarm-services-cert` on a
hive fronting no swarm name presents it for the handshake of the root
fetch.
the secret store for the gateway leaf.
Its subject has to be
`<services.hyperhive.deploy.bao.hiveLeafCommonNamePrefix>-<hiveName>`;
that cert-auth role's policy may issue from this hive's gateway-leaf
role and nothing else.
Every hive with a self-signed gateway needs it: without it both units
fail, naming this option and
Every hive with a self-signed gateway needs it: without it that unit
fails, naming this option and
{option}`services.hyperhive.deploy.hive-controller.tls.baoHiveIdentityKeyFile`.
On the host that runs the store a glue module supplies the leaf as a
`mkDefault`; on every other host an operator places it.
@ -339,6 +333,30 @@ in
};
config = lib.mkIf active {
assertions = [
{
assertion = haveServicesIssuer;
message = ''
services.hyperhive.gateway serves the self-signed leaf on this host,
which needs the swarm-services issuer's store identity: set both
services.hyperhive.deploy.bao.servicesIssuerClientCertFile
services.hyperhive.deploy.bao.servicesIssuerClientKeyFile
swarm-services-cert.service presents it to ask the swarm secret
store for the swarm-services leaf and to fetch the store's pki root.
Both default to the pair glue-bao-tls.nix mints when
services.hyperhive.deploy.bao.enable is true (the host that runs
the store). Otherwise copy services-issuer.pem and
services-issuer-key.pem from the store host's /var/lib/swarm-bao-pki/
and name them in those two options, or give the gateway a
certificate of its own (services.hyperhive.gateway.tls.certDir or
services.hyperhive.gateway.tls.acme.enable).
'';
}
];
# The state dir and the trust bundle. Never waits on the store: the
# store's container is ordered after this unit, which writes the
# bundle it bind-mounts, so the leaf is `hive-tls-resign`'s.
@ -603,9 +621,9 @@ in
# store nothing local can order against that.
#
# `swarm-bao-pki` mints the services-issuer leaf the login below
# presents, and is absent on a hive holding an operator-placed one —
# presents, and is absent on a host holding an operator-placed one —
# ignored there for the same reason, rather than being a dependency
# only some hives may declare.
# only some hosts may declare.
after = [
"container@${baoCfg.machine}.service"
"swarm-bao-pki.service"
@ -643,13 +661,12 @@ in
# bundle: every other hive's swarm-service names chain to it.
# `ca/pem` takes no token; the TLS client certificate is the
# store's listener's demand, not bao's, so this presents the
# hive's own identity and never logs in with it.
# A hive without a readable identity fails here, naming the
# options: every hive must hold one.
# services-issuer pair and never logs in with it. Without a
# readable pair the unit fails here, naming the options.
if [ "$want_svc" = 0 ]; then
${
if haveHiveIdentity then
requireHiveIdentity "fetch the store's ${baoServicesPkiMount} root"
if haveServicesIssuer then
requireServicesIssuer "fetch the store's ${baoServicesPkiMount} root"
+ ''
echo "no swarm service domains configured — fetching only the store's pki root"
err="$(mktemp)"
@ -683,7 +700,7 @@ in
fi
exit 0''
else
requireHiveIdentity "fetch the store's ${baoServicesPkiMount} root"
requireServicesIssuer "fetch the store's ${baoServicesPkiMount} root"
}
fi
@ -725,17 +742,7 @@ in
exit 0
fi
${
if cfg.baoClientCertFile == null || cfg.baoClientKeyFile == null then
''
echo "no bao client certificate configured for this host, so the gateway's" >&2
echo "swarm-services certificate cannot be requested from the store." >&2
echo "Set services.hyperhive.deploy.hive-controller.tls.baoClient{Cert,Key}File" >&2
echo "to a leaf the store's CA signed with CN=${baoDeploy.servicesIssuerCommonName}." >&2
exit 1''
else
""
}
${requireServicesIssuer "ask the store for the swarm-services leaf"}
# `bao`'s own message is the only thing separating an unreachable
# store from a refused identity from a role that would not issue