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:
parent
52037133e2
commit
5526daab97
16 changed files with 859 additions and 628 deletions
|
|
@ -49,167 +49,68 @@ let
|
|||
gatewayWaitsForServicesLeaf =
|
||||
localServiceDomains != [ ] && localServiceDomains != [ baoCfg.ui.domain ];
|
||||
|
||||
# The host-managed hive CA is the trust anchor for self-signed mode.
|
||||
# It is only stood up when the gateway actually serves a self-signed
|
||||
# cert: the gateway must run here and be in self-signed mode. The
|
||||
# self-signed condition is the gateway module's single source of truth
|
||||
# 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.
|
||||
# The self-signed condition is the gateway module's single source of truth
|
||||
# (`gateway.useSelfSigned`): true when neither an operator cert
|
||||
# (`tls.certDir`) nor ACME is set.
|
||||
active = gatewayCfg.enable && gatewayCfg.useSelfSigned;
|
||||
|
||||
# How this hive's CA comes into existence when it is missing — and it
|
||||
# is one of exactly two things, chosen by config rather than by what
|
||||
# happens to be on disk.
|
||||
#
|
||||
# Issuing a sub-CA requires the swarm root's PRIVATE key, so it can
|
||||
# only happen where that key legitimately lives: the single-host
|
||||
# deployment that `swarm.ca.autoConfigure` describes. A host cannot
|
||||
# infer that it is that host — a swarm's services and its hives can
|
||||
# sit anywhere — so with the flag off this hive self-signs exactly as
|
||||
# it always has, and an operator who wants it in the hierarchy
|
||||
# installs the CA themselves. Falling back to self-signed rather than
|
||||
# failing keeps a plain hive working out of the box; what it loses is
|
||||
# membership of a swarm's trust, which is the correct thing to lose
|
||||
# for a hive nobody has federated.
|
||||
caGenScript =
|
||||
if swarmCaCfg.autoConfigure then
|
||||
# The role this hive's gateway leaf is issued through. ./swarm-bao.nix
|
||||
# writes one per hive in the swarm directory, under the same prefix, and
|
||||
# narrows it to that hive's domain and `*.` that domain. So the leaf carries
|
||||
# those two names and no others: a configured service name goes on the
|
||||
# swarm-services leaf below, and asking this role for one is a refusal.
|
||||
hiveLeafRole =
|
||||
if hyperhiveCfg.hiveName == null then
|
||||
null
|
||||
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 =
|
||||
purpose:
|
||||
if haveHiveIdentity then
|
||||
''
|
||||
# The root should exist — `swarm-ca.service` runs before this and
|
||||
# is required by it. If it doesn't, something upstream failed and
|
||||
# signing with a half-provisioned root would be worse than stopping.
|
||||
if [ ! -s "$root" ] || [ ! -s "$rootk" ]; then
|
||||
echo "swarm.ca.autoConfigure is set but there is no root CA key at $rootk" >&2
|
||||
echo "(swarm-ca.service should have generated it) — refusing to issue a hive CA." >&2
|
||||
missing=""
|
||||
for f in ${
|
||||
lib.escapeShellArgs [
|
||||
cfg.baoHiveIdentityCertFile
|
||||
cfg.baoHiveIdentityKeyFile
|
||||
]
|
||||
}; 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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "issuing fresh hive CA at $ca under the swarm root"
|
||||
cacsr="$(mktemp "$d/ca.csr.XXXXXX")"
|
||||
caext="$(mktemp "$d/ca.ext.XXXXXX")"
|
||||
trap 'rm -f "$cacsr" "$caext"' EXIT
|
||||
|
||||
openssl req -newkey rsa:4096 -nodes -sha256 \
|
||||
-keyout "$cak" -out "$cacsr" \
|
||||
-subj "/CN=hive-ca ${domain}"
|
||||
|
||||
# printf (not a heredoc) so the ext-file lines carry no leading
|
||||
# whitespace once nix has stripped the indented-string indent.
|
||||
{
|
||||
printf 'basicConstraints=critical,CA:TRUE,pathlen:0\n'
|
||||
printf 'keyUsage=critical,keyCertSign,cRLSign\n'
|
||||
printf 'subjectKeyIdentifier=hash\n'
|
||||
printf 'authorityKeyIdentifier=keyid:always\n'
|
||||
# The constraint is the point of the hierarchy, not a
|
||||
# flourish: without it a leaked hive CA mints any name in
|
||||
# the swarm, and it is verifiers that enforce this, not our
|
||||
# good behaviour. The IP exclusions are not redundant — a
|
||||
# DNS constraint says nothing about an iPAddress SAN, and a
|
||||
# name type nobody constrained is a name type this CA is
|
||||
# unconstrained for.
|
||||
printf 'nameConstraints=critical,permitted;DNS:%s,excluded;IP:0.0.0.0/0.0.0.0,excluded;IP:0:0:0:0:0:0:0:0/0:0:0:0:0:0:0:0\n' \
|
||||
${lib.escapeShellArg domain}
|
||||
} > "$caext"
|
||||
|
||||
openssl x509 -req -in "$cacsr" -CA "$root" -CAkey "$rootk" \
|
||||
-CAcreateserial -days ${toString cfg.caValidityDays} -sha256 \
|
||||
-extfile "$caext" -out "$ca"
|
||||
rm -f "$cacsr" "$caext"
|
||||
trap - EXIT
|
||||
''
|
||||
else
|
||||
''
|
||||
echo "generating fresh self-signed hive CA at $ca"
|
||||
openssl req -x509 -newkey rsa:4096 -nodes -sha256 \
|
||||
-days ${toString cfg.caValidityDays} \
|
||||
-keyout "$cak" -out "$ca" \
|
||||
-subj "/CN=hive-ca ${domain}" \
|
||||
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
|
||||
-addext "keyUsage=critical,keyCertSign,cRLSign"
|
||||
echo "this hive has no store identity, so it cannot ${purpose}." >&2
|
||||
echo "set ${hiveIdentityOptions} and place both files." >&2
|
||||
exit 1
|
||||
'';
|
||||
|
||||
# Sign one leaf. Parameterised rather than hardcoded to `gateway.*`
|
||||
# because there are now two: the hive's own leaf, issued by the hive
|
||||
# CA, and the swarm-services leaf, issued by the services sub-CA that
|
||||
# ./swarm-ca.nix maintains. Same ceremony, different issuer and names
|
||||
# — and one script means the two cannot drift in how they are built.
|
||||
#
|
||||
# $1 stateDir $2 basename $3 CN $4 SAN list $5 issuer cert $6 issuer key
|
||||
signLeafScript = pkgs.writeShellScript "hive-tls-sign-leaf" ''
|
||||
set -euo pipefail
|
||||
d="$1"
|
||||
base="$2"
|
||||
cn="$3"
|
||||
sans="$4"
|
||||
ca="$5"
|
||||
cak="$6"
|
||||
leaf="$d/$base.pem"
|
||||
leafk="$d/$base-key.pem"
|
||||
csr="$(mktemp "$d/$base.csr.XXXXXX")"
|
||||
ext="$(mktemp "$d/$base.ext.XXXXXX")"
|
||||
only="$(mktemp "$d/$base.leaf.XXXXXX")"
|
||||
trap 'rm -f "$csr" "$ext" "$only"' EXIT
|
||||
|
||||
openssl req -newkey rsa:4096 -nodes -sha256 \
|
||||
-keyout "$leafk" -out "$csr" \
|
||||
-subj "/CN=$cn"
|
||||
|
||||
# printf (not a heredoc) so the ext-file lines carry no leading
|
||||
# whitespace once nix has stripped the indented-string indent.
|
||||
{
|
||||
printf 'subjectAltName=%s\n' "$sans"
|
||||
printf 'basicConstraints=critical,CA:FALSE\n'
|
||||
printf 'keyUsage=critical,digitalSignature,keyEncipherment\n'
|
||||
printf 'extendedKeyUsage=serverAuth\n'
|
||||
} > "$ext"
|
||||
|
||||
openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \
|
||||
-CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \
|
||||
-extfile "$ext" -out "$only"
|
||||
|
||||
# nginx serves this file verbatim, so it must carry the leaf AND its
|
||||
# issuer: the hive CA is an intermediate under the swarm root, and a
|
||||
# client that anchors on the root cannot build the middle of the
|
||||
# chain by itself. Agents anchor on the hive CA directly and
|
||||
# validated either way — the appended cert is what makes a swarm
|
||||
# peer, or anything else holding only the root, work.
|
||||
# Leaf first: both `ssl_certificate` and the `openssl x509 -in`
|
||||
# expiry checks read the first cert in the file.
|
||||
cat "$only" "$ca" > "$leaf"
|
||||
chmod 0600 "$leafk"
|
||||
chmod 0644 "$leaf"
|
||||
'';
|
||||
|
||||
# The hive's own leaf: signed by the hive CA, covering the hive domain
|
||||
# and its sub-domains.
|
||||
#
|
||||
# `forge.` and `matrix.` were listed here explicitly and both were
|
||||
# redundant with the wildcard beside them — a wildcard covers exactly
|
||||
# one label, and those are one label. Naming them read as policy
|
||||
# ("these two services live under this leaf"), which is the reason they
|
||||
# outlived it: a configured service name is no longer necessarily under
|
||||
# this domain, and when it isn't, this is the one list it cannot join.
|
||||
# The hive CA is `nameConstraints=permitted;DNS:<hive domain>`, and a
|
||||
# violation invalidates the CERTIFICATE rather than the offending name
|
||||
# — measured with openssl, not reasoned about — so one sibling name
|
||||
# added here takes the leaf down for the dashboard and every other
|
||||
# vhost sharing it. Configured service names get the swarm-services
|
||||
# leaf below, whose sub-CA is constrained to exactly those names.
|
||||
signHiveLeaf = ''
|
||||
${signLeafScript} "$d" gateway ${lib.escapeShellArg domain} \
|
||||
${lib.escapeShellArg "DNS:${domain},DNS:*.${domain}"} \
|
||||
"$d/ca.pem" "$d/ca-key.pem"
|
||||
'';
|
||||
|
||||
# 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 at different times
|
||||
# (`hive-tls-ca` at service activation, i.e. on the deploy;
|
||||
# `hive-tls-resign` from a weekly timer for a host that stays up long
|
||||
# enough to drift; `swarm-services-cert` for the leaf the store
|
||||
# issues). A rule implemented in one and not the others is worse than
|
||||
# one implemented in none — it looks fixed and only fires on whichever
|
||||
# path you did not take, which is exactly how a corrected
|
||||
# `serviceDomains` still served a stale leaf.
|
||||
# agree: they answer the same question for different leaves
|
||||
# (`hive-tls-resign` for the hive leaf, at boot, on the deploy and from
|
||||
# its timer; `swarm-services-cert` for the services leaf). A rule
|
||||
# implemented in one and not the others is worse than one implemented
|
||||
# in none — it looks fixed and only fires on whichever path you did not
|
||||
# take, which is exactly how a corrected `serviceDomains` still served a
|
||||
# stale leaf.
|
||||
#
|
||||
# Expects `$d` (state dir) to be set; defines `$leaf`-adjacent names and
|
||||
# `covers`.
|
||||
|
|
@ -254,18 +155,13 @@ let
|
|||
[ "$_missing" = 0 ]
|
||||
}
|
||||
|
||||
# True when the leaf this host SIGNS is present and carries its
|
||||
# configured names. Expiry is the callers' own business — they use
|
||||
# different windows.
|
||||
# True when the hive leaf is present and carries its configured
|
||||
# names. Expiry is the callers' own business — they use different
|
||||
# windows.
|
||||
#
|
||||
# ⚠️ The services leaf is deliberately NOT part of this any more, and
|
||||
# the omission is load-bearing rather than a simplification. Its two
|
||||
# callers sign under the hive CA's key; the services leaf comes out
|
||||
# of the secret store, which neither of them can reach. A guard that
|
||||
# went on reporting it stale would make `hive-tls-ca` re-sign the
|
||||
# hive leaf forever over a file it has no way to fix. The
|
||||
# `swarm-services-cert` unit below holds the same rule for that leaf,
|
||||
# using the `covers` helper above so the two cannot drift.
|
||||
# The services leaf is not part of this: `swarm-services-cert` holds
|
||||
# the same rule for it, using the `covers` helper above so the two
|
||||
# cannot drift.
|
||||
leavesCoverNames() {
|
||||
covers "$leaf" "$hiveNames"
|
||||
}
|
||||
|
|
@ -306,9 +202,9 @@ let
|
|||
// lib.optionalAttrs (baoDeploy.serverCaFile != null) {
|
||||
BAO_CACERT = baoDeploy.serverCaFile;
|
||||
};
|
||||
haveReaderIdentity = baoDeploy.clientCertFile != null && baoDeploy.clientKeyFile != null;
|
||||
|
||||
# The root-only fetch. Same store and server CA as `servicesCertEnvironment`,
|
||||
# but the hive's reader identity: a host fronting no swarm name holds no
|
||||
# but the hive's own identity: a host fronting no swarm name holds no
|
||||
# services-issuer pair.
|
||||
storeRootCurlTls = lib.escapeShellArgs (
|
||||
lib.optionals (baoDeploy.serverCaFile != null) [
|
||||
|
|
@ -317,59 +213,53 @@ let
|
|||
]
|
||||
++ [
|
||||
"--cert"
|
||||
baoDeploy.clientCertFile
|
||||
cfg.baoHiveIdentityCertFile
|
||||
"--key"
|
||||
baoDeploy.clientKeyFile
|
||||
cfg.baoHiveIdentityKeyFile
|
||||
]
|
||||
);
|
||||
|
||||
# What `hive-tls-resign` issues the hive leaf with: the services leaf's
|
||||
# store and server CA, presenting the hive's own identity.
|
||||
hiveLeafEnvironment = {
|
||||
inherit (servicesCertEnvironment) BAO_ADDR;
|
||||
}
|
||||
// lib.optionalAttrs (cfg.baoHiveIdentityCertFile != null) {
|
||||
BAO_CLIENT_CERT = cfg.baoHiveIdentityCertFile;
|
||||
}
|
||||
// lib.optionalAttrs (cfg.baoHiveIdentityKeyFile != null) {
|
||||
BAO_CLIENT_KEY = cfg.baoHiveIdentityKeyFile;
|
||||
}
|
||||
// lib.optionalAttrs (baoDeploy.serverCaFile != null) {
|
||||
BAO_CACERT = baoDeploy.serverCaFile;
|
||||
};
|
||||
storeRetry = import ./lib/store-retry.nix { };
|
||||
in
|
||||
{
|
||||
# Host-side TLS trust root for the self-signed gateway mode.
|
||||
# Host-side TLS for the self-signed gateway mode.
|
||||
#
|
||||
# A bare self-signed leaf would be its own trust anchor, so every
|
||||
# regeneration would be a new anchor and every consumer (agents,
|
||||
# federation peers) would have to re-trust on each rotation — and a
|
||||
# runtime-generated, in-container cert can't be wired into an agent's
|
||||
# build-time trust store at all.
|
||||
# The gateway serves a short-lived **leaf** issued by the secret store's
|
||||
# `pki` mount through this hive's own role, and consumers (agents,
|
||||
# federation peers) anchor on that mount's root, which ships in
|
||||
# `trust-bundle.pem`. Leaf rotation never re-breaks them, and no key that
|
||||
# signs a certificate exists on this host. See
|
||||
# `docs/networking/gateway.md` ("Self-signed TLS") and docs/swarm/ca.md.
|
||||
#
|
||||
# So the issuer is a long-lived **hive CA** held on the host. The
|
||||
# gateway serves a **leaf** signed by that CA (via the `tls.certDir`
|
||||
# bind-mount path); agents and federation peers trust the *CA* once,
|
||||
# and leaf rotation never re-breaks them. See `docs/networking/gateway.md`
|
||||
# ("Self-signed TLS").
|
||||
#
|
||||
# That CA is self-signed by default. Under
|
||||
# `swarm.ca.autoConfigure` — the all-on-one-host case — it is instead
|
||||
# an intermediate issued under the swarm root (./swarm-ca.nix owns the
|
||||
# why) and name-constrained to this hive's domain, so it stays the
|
||||
# anchor agents pin while a peer holding only the root can validate
|
||||
# everything this hive serves. Either way an existing CA is left
|
||||
# alone; see the issuance comment below.
|
||||
# The trust bundle still carries the retired hive CA's public `ca.pem`
|
||||
# where one exists, so a leaf it signed keeps verifying until it is
|
||||
# replaced.
|
||||
|
||||
options.services.hyperhive.deploy.hive-controller.tls = {
|
||||
stateDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/var/lib/hive-tls";
|
||||
description = ''
|
||||
Host directory holding the hive CA + gateway leaf cert for the
|
||||
self-signed gateway mode. `ca.pem` (the anchor agents and
|
||||
federation peers trust), `ca-key.pem` (0600, never leaves the
|
||||
host), `gateway.pem` / `gateway-key.pem` (the leaf the gateway
|
||||
container bind-mounts and nginx serves). Persistent so the CA
|
||||
survives reboots — re-deriving it would re-break every consumer.
|
||||
'';
|
||||
};
|
||||
|
||||
caValidityDays = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 7300;
|
||||
description = ''
|
||||
Validity window of the hive CA in days (default ~20y). Kept long
|
||||
and well beyond `leafValidityDays` so the CA outlives many leaf
|
||||
rotations — the whole point of the CA is to be a stable anchor
|
||||
that consumers trust once. The CA is regenerated only if missing
|
||||
or already expired.
|
||||
Host directory holding the gateway leaf and trust bundle for the
|
||||
self-signed gateway mode: `gateway.pem` / `gateway-key.pem` (the
|
||||
leaf the store issues and nginx serves) and `trust-bundle.pem` (the
|
||||
anchors agents and federation peers trust). `ca.pem` and
|
||||
`ca-previous.pem`, where present, are the retired hive CA's public
|
||||
certificates and stay in the bundle; no CA key is kept here.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -389,9 +279,9 @@ in
|
|||
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. `swarm-services-cert` never logs in with that one:
|
||||
on a hive fronting no swarm name it presents it only for the mTLS
|
||||
handshake of the root fetch, since `pki/ca/pem` takes no token.
|
||||
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
|
||||
|
|
@ -412,21 +302,39 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
leafValidityDays = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 30;
|
||||
baoHiveIdentityCertFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/swarm-bao-pki/hive-leaf.pem";
|
||||
description = ''
|
||||
Validity window of the gateway leaf cert in days (default 30).
|
||||
Short-lived by design — ahead of the CA/Browser-Forum's move
|
||||
toward ~47-day max lifetimes — which bounds the blast radius of a
|
||||
leaf-key compromise. The leaf is re-signed by the (stable) CA
|
||||
when it is missing or near expiry; because it shares the CA
|
||||
anchor, a rotation does not disturb consumer trust. Agents and
|
||||
federation peers validate against the CA, not browser CA/B-forum
|
||||
limits. The weekly `hive-tls-resign` timer re-signs the leaf once
|
||||
it is within half its validity of expiry and propagates the new
|
||||
leaf into the running gateway, so a long-uptime host renews
|
||||
automatically without a reboot.
|
||||
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.
|
||||
|
||||
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
|
||||
{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.
|
||||
|
||||
A path, never a value.
|
||||
'';
|
||||
};
|
||||
|
||||
baoHiveIdentityKeyFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/swarm-bao-pki/hive-leaf-key.pem";
|
||||
description = ''
|
||||
Private key for
|
||||
{option}`services.hyperhive.deploy.hive-controller.tls.baoHiveIdentityCertFile`.
|
||||
Both or neither — a certificate with no key authenticates nothing.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
@ -444,24 +352,18 @@ in
|
|||
# rebuild of every hive that is not the CA host, about a fallback
|
||||
# that no longer happens.
|
||||
|
||||
# Generate (and rotate) the hive CA + gateway leaf before anything
|
||||
# serves it. Idempotent: the CA is created once and reused; the leaf
|
||||
# is re-signed on expiry under the same CA so the anchor is stable.
|
||||
# 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.
|
||||
systemd.services.hive-tls-ca = {
|
||||
description = "Generate hive CA + gateway leaf TLS cert (self-signed mode)";
|
||||
description = "Assemble the hive TLS trust bundle (self-signed mode)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
# The consumer is `hive-gateway-self-signed-cert`, which copies the
|
||||
# leaf into the gateway's state dir at the mode nginx can read, and
|
||||
# which nginx in turn `Requires=`. So this must run first or that
|
||||
# copy fails under `set -eu` and takes nginx down with it — order
|
||||
# against the unit that reads the file.
|
||||
# leaf out of the state dir this unit creates, and which nginx in
|
||||
# turn `Requires=`. Order against the unit that reads the dir.
|
||||
before = [ "hive-gateway-self-signed-cert.service" ];
|
||||
requiredBy = [ "hive-gateway-self-signed-cert.service" ];
|
||||
# The issuance below needs the swarm root key on disk. When this
|
||||
# host generates it (single-host swarm) that unit must have run
|
||||
# first; when the operator provides the material there is no unit
|
||||
# to wait for, so the dependency is conditional rather than a unit
|
||||
# that exists and does nothing.
|
||||
# The bundle carries the swarm root where this host generates it.
|
||||
#
|
||||
# `swarm-services-cert` is NOT ordered against this unit in either
|
||||
# direction, and the absence is load-bearing — see the cycle spelled
|
||||
|
|
@ -477,8 +379,6 @@ in
|
|||
# and `swarm-services-cert` restarts this unit once the root it wrote
|
||||
# is new — so the bundle converges instead of being a boot behind.
|
||||
after = lib.optionals swarmCaCfg.autoConfigure [ "swarm-ca.service" ];
|
||||
requires = lib.optionals swarmCaCfg.autoConfigure [ "swarm-ca.service" ];
|
||||
path = [ pkgs.openssl ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
|
|
@ -492,164 +392,103 @@ in
|
|||
install -d -m 0755 "$d"
|
||||
|
||||
ca="$d/ca.pem"
|
||||
cak="$d/ca-key.pem"
|
||||
leaf="$d/gateway.pem"
|
||||
leafk="$d/gateway-key.pem"
|
||||
root=${lib.escapeShellArg "${swarmCaCfg.stateDir}/root.pem"}
|
||||
rootk=${lib.escapeShellArg "${swarmCaCfg.stateDir}/root-key.pem"}
|
||||
|
||||
prev="$d/ca-previous.pem"
|
||||
marker="$d/.swarm-ca-adopted"
|
||||
root=${lib.escapeShellArg "${swarmCaCfg.stateDir}/root.pem"}
|
||||
svcroot="$d/swarm-services-root.pem"
|
||||
|
||||
# --- Adoption: a hive whose CA predates the swarm root.
|
||||
#
|
||||
# Runs ONLY where this host also owns the root (`autoConfigure`),
|
||||
# because adoption invalidates an anchor that consumers already
|
||||
# trust and they refresh on their own schedule. On one box that
|
||||
# schedule is knowable; across hosts it is not, so there the
|
||||
# operator does it and this unit only says so, loudly.
|
||||
#
|
||||
# Guarded by a marker rather than by the state of the world: the
|
||||
# marker's ABSENCE is the trigger, so this fires once per hive
|
||||
# instead of re-deciding every activation. That is the difference
|
||||
# between a migration and a kicking machine.
|
||||
if [ -s "$root" ] && [ -s "$ca" ] && [ ! -e "$marker" ] \
|
||||
&& ! openssl verify -CAfile "$root" "$ca" >/dev/null 2>&1; then
|
||||
${
|
||||
if swarmCaCfg.autoConfigure then
|
||||
''
|
||||
echo "adopting the swarm CA: $ca does not chain to $root" >&2
|
||||
# Keep the old CA as an anchor across the overlap. Consumers
|
||||
# read the bundle, so adoption is additive before it is
|
||||
# subtractive — agents pick up new trust only when their
|
||||
# container restarts, which is a window even on one host.
|
||||
cp "$ca" "$prev"
|
||||
chmod 0644 "$prev"
|
||||
rm -f "$ca" "$cak"
|
||||
touch "$marker"''
|
||||
else
|
||||
''
|
||||
echo "hive-tls: this hive's CA does not chain to the swarm root." >&2
|
||||
echo " hive CA: $ca" >&2
|
||||
echo " swarm root: $root" >&2
|
||||
echo "Adopting the hierarchy is not automatic here: it invalidates an" >&2
|
||||
echo "anchor that peers and agents on OTHER hosts still trust, and they" >&2
|
||||
echo "refresh on their own schedule — only you know when that is safe." >&2
|
||||
echo "To adopt: rm $ca $cak && systemctl restart hive-tls-ca.service" >&2
|
||||
echo "To keep the current CA deliberately: touch $marker" >&2
|
||||
exit 1''
|
||||
}
|
||||
# The gateway leaf comes from the store, so no key that signs a
|
||||
# certificate is kept here.
|
||||
if [ -e "$d/ca-key.pem" ]; then
|
||||
echo "removing the retired hive CA key $d/ca-key.pem"
|
||||
rm -f "$d/ca-key.pem"
|
||||
fi
|
||||
|
||||
# --- CA: generated once and reused across leaf rotations, in
|
||||
# whichever of the two shapes `caGenScript` selected.
|
||||
# Regenerated only if missing or already expired (checkend 0).
|
||||
# A new CA means every consumer must re-trust, so the leaf is
|
||||
# dropped to force a re-sign under the fresh CA.
|
||||
#
|
||||
# An existing CA is never re-rooted here. A hive predating the
|
||||
# swarm root carries a self-signed `ca.pem`, and swapping it for
|
||||
# a swarm-issued one would break every consumer already trusting
|
||||
# it — including agents, whose trust is refreshed only when their
|
||||
# container restarts. Adopting the hierarchy on such a hive is
|
||||
# therefore an operator step (drop the CA, let this unit
|
||||
# re-issue), which is also what keeps this change non-disruptive
|
||||
# to hives that never adopt it.
|
||||
if [ ! -s "$ca" ] || [ ! -s "$cak" ] \
|
||||
|| ! openssl x509 -in "$ca" -noout -checkend 0 >/dev/null 2>&1; then
|
||||
${caGenScript}
|
||||
chmod 0600 "$cak"
|
||||
chmod 0644 "$ca"
|
||||
rm -f "$leaf" "$leafk"
|
||||
fi
|
||||
|
||||
# --- Leaf: (re)sign when missing, within 30 days of expiry, or no
|
||||
# longer covering the configured names, always under the current
|
||||
# (stable) CA.
|
||||
${leafCoverage}
|
||||
|
||||
# ⚠️ This is the guard that runs ON THE DEPLOY — `hive-tls-resign`
|
||||
# only fires from a weekly timer, so a rule enforced only there is
|
||||
# up to a week late and does nothing for the rebuild that changed
|
||||
# the names in the first place.
|
||||
#
|
||||
resign=0
|
||||
{ [ -s "$leaf" ] && [ -s "$leafk" ]; } || resign=1
|
||||
openssl x509 -in "$leaf" -noout -checkend 2592000 >/dev/null 2>&1 || resign=1
|
||||
leavesCoverNames || resign=1
|
||||
|
||||
if [ "$resign" = 1 ]; then
|
||||
echo "signing gateway leaf at $leaf (missing, near expiry, or missing a configured name)"
|
||||
${signHiveLeaf}
|
||||
fi
|
||||
|
||||
# --- Trust bundle: what a consumer must TRUST, as opposed to
|
||||
# `ca.pem`, which is what this host SIGNS with. Why they stopped
|
||||
# being the same file, and why `ca-previous.pem` stays in the set
|
||||
# after an adoption, are in docs/swarm/ca.md. Three constraints
|
||||
# the code cannot state:
|
||||
# --- Trust bundle: what a consumer must TRUST. Why it is a set of
|
||||
# anchors rather than one file is in docs/swarm/ca.md. Three
|
||||
# constraints the code cannot state:
|
||||
#
|
||||
# Written IN PLACE, never renamed into position — containers
|
||||
# bind-mount this file and a bind mount follows the inode, so a
|
||||
# rename leaves every consumer holding the old one.
|
||||
#
|
||||
# Dropping `ca-previous.pem` is deliberately NOT done here:
|
||||
# "long enough" is a deployment fact, not a unit's call.
|
||||
# Dropping `ca.pem` and `ca-previous.pem` is deliberately NOT done
|
||||
# here: "long enough" is a deployment fact, not a unit's call.
|
||||
#
|
||||
# Built as an array with `if` guards, not
|
||||
# `[ -s f ] && anchors+=(f)`: under `set -e` an && list whose test
|
||||
# fails IS a failing command and kills the unit — on exactly the
|
||||
# hive where the optional file is legitimately absent. The
|
||||
# reflexive `|| true` is worse; it also swallows a real failure to
|
||||
# read the hive CA.
|
||||
# hive where the optional file is legitimately absent.
|
||||
#
|
||||
# `swarm-services-root.pem` joins the set for the reason the swarm
|
||||
# root is in it: it is an ANCHOR a consumer terminates at, not an
|
||||
# intermediate. It is also what keeps this change from being a
|
||||
# flag day — the swarm root stays in the bundle beside it, so a
|
||||
# leaf still chaining to the old services sub-CA and one issued by
|
||||
# the store both verify against the same file, and a hive can be
|
||||
# rebuilt before or after its peers.
|
||||
# `swarm-services-root.pem` is the store's `pki` root, which both
|
||||
# the gateway leaf and the swarm-services leaf chain to. `ca.pem`
|
||||
# and `ca-previous.pem` are the retired hive CA's public
|
||||
# certificates, kept so a leaf they signed verifies until replaced.
|
||||
bundle="$d/trust-bundle.pem"
|
||||
anchors=("$ca")
|
||||
anchors=()
|
||||
if [ -s "$ca" ]; then anchors+=("$ca"); fi
|
||||
if [ -s "$prev" ]; then anchors+=("$prev"); fi
|
||||
if [ -s "$root" ]; then anchors+=("$root"); fi
|
||||
if [ -s "$svcroot" ]; then anchors+=("$svcroot"); fi
|
||||
cat "''${anchors[@]}" > "$bundle"
|
||||
# `cat` with no arguments reads stdin, so no anchor at all is an
|
||||
# empty file until `swarm-services-cert` writes the root.
|
||||
if [ "''${#anchors[@]}" = 0 ]; then
|
||||
echo "no trust anchor on disk yet — $bundle is empty until the store's root is fetched" >&2
|
||||
: > "$bundle"
|
||||
else
|
||||
cat "''${anchors[@]}" > "$bundle"
|
||||
fi
|
||||
chmod 0644 "$bundle"
|
||||
'';
|
||||
};
|
||||
|
||||
# Weekly re-sign of the gateway leaf so short-lived leaves renew
|
||||
# without depending on a reboot.
|
||||
# The gateway leaf: issued by the secret store's `pki` mount through
|
||||
# this hive's own role, at boot, on every deploy and from the daily
|
||||
# timer below, whenever it is missing, past half its lifetime, missing
|
||||
# a configured name, or not chained to the store's root.
|
||||
#
|
||||
# `hive-tls-ca` only re-signs at service activation (boot/rebuild); a
|
||||
# long-uptime host would otherwise let a 30-day leaf lapse silently.
|
||||
# This service re-signs the leaf directly (not by bouncing hive-tls-ca)
|
||||
# and propagates the new leaf to nginx when the file actually changed.
|
||||
# ⚠️ Nothing orders the gateway against this unit, which is what lets a
|
||||
# hive holding a valid leaf start its gateway with the store sealed or
|
||||
# unreachable. Only a hive with no leaf yet has a gateway that waits
|
||||
# for the store: its cert import fails until this unit lands a leaf
|
||||
# and starts nginx.
|
||||
#
|
||||
# Failure leaves the current leaf alone: every file the script writes
|
||||
# is a `.new` until the store has answered with both fields.
|
||||
#
|
||||
# Propagation mechanism: nginx serves a *copy* of the leaf, written by
|
||||
# `hive-gateway-self-signed-cert` at the mode nginx can read. Re-signing
|
||||
# `hive-gateway-self-signed-cert` at the mode nginx can read. Issuing
|
||||
# the source therefore changes nothing on its own — the copy has to be
|
||||
# remade and nginx reloaded, which is what the two calls below do.
|
||||
#
|
||||
# ⚠️ Neither call is `|| true`: a swallowed 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.
|
||||
# A failure here must fail the timer.
|
||||
# remade and nginx reloaded, which is what the calls at the end do.
|
||||
# Neither of the running-nginx calls is `|| true`: a swallowed
|
||||
# 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 = {
|
||||
description = "Re-sign the gateway TLS leaf and reload nginx";
|
||||
# hive-tls-ca must have run first so the CA key exists before we try
|
||||
# to re-sign under it. On first boot `Persistent=true` on the weekly
|
||||
# timer fires immediately; without this ordering the resign could race
|
||||
# the CA initialisation and fail with "no such file" on the CA key.
|
||||
after = [ "hive-tls-ca.service" ];
|
||||
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;
|
||||
# ignored elsewhere, as for `swarm-services-cert`, which writes the
|
||||
# store's root this unit checks the leaf against.
|
||||
after = [
|
||||
"hive-tls-ca.service"
|
||||
"container@${baoCfg.machine}.service"
|
||||
"swarm-bao-pki.service"
|
||||
"swarm-bao-hive-leaf-policy.service"
|
||||
"swarm-services-cert.service"
|
||||
];
|
||||
wants = [ "container@${baoCfg.machine}.service" ];
|
||||
path = [
|
||||
baoDeploy.package
|
||||
pkgs.jq
|
||||
pkgs.openssl
|
||||
pkgs.coreutils
|
||||
pkgs.systemd
|
||||
];
|
||||
serviceConfig = {
|
||||
environment = hiveLeafEnvironment;
|
||||
# ./lib/store-retry.nix: the login below fails for as long as the
|
||||
# store is sealed or down.
|
||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||
serviceConfig = storeRetry.serviceConfig // {
|
||||
Type = "oneshot";
|
||||
UMask = "0077";
|
||||
SyslogIdentifier = "hive-tls-resign";
|
||||
|
|
@ -657,46 +496,93 @@ in
|
|||
script = ''
|
||||
set -euo pipefail
|
||||
d=${lib.escapeShellArg cfg.stateDir}
|
||||
install -d -m 0755 "$d"
|
||||
leaf="$d/gateway.pem"
|
||||
leafk="$d/gateway-key.pem"
|
||||
|
||||
# ⚠️ EVERY leaf this host SIGNS must be covered here. A leaf that
|
||||
# first-boot issuance creates and this unit does not know about
|
||||
# looks perfect for its entire validity and then expires with no
|
||||
# warning — the failure is invisible until it is total. The name
|
||||
# checks come from the shared snippet, so this unit and
|
||||
# `hive-tls-ca` cannot disagree about what a good leaf is.
|
||||
#
|
||||
# The services leaf is not one of them any longer: it is issued
|
||||
# by the secret store, whose key this unit does not hold and
|
||||
# cannot re-sign under. `swarm-services-cert-renew` renews it,
|
||||
# on its own daily timer.
|
||||
${leafCoverage}
|
||||
|
||||
# Re-sign only when a leaf is within half its validity of expiry.
|
||||
# The weekly cadence catches this window well before one lapses.
|
||||
halflife=$(( ${toString cfg.leafValidityDays} * 86400 / 2 ))
|
||||
|
||||
fresh() { # a leaf is fresh if it exists and is not near expiry
|
||||
[ -s "$1" ] && openssl x509 -in "$1" -noout -checkend "$halflife" >/dev/null 2>&1
|
||||
fail() {
|
||||
echo "$1" >&2
|
||||
if [ -s "$leaf" ]; then
|
||||
echo "the current gateway leaf at $leaf stays in place" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
if fresh "$leaf" && leavesCoverNames; then
|
||||
echo "leaves valid, and covering the configured names — no resign needed"
|
||||
# Checked before the leaf is: a hive without its identity is red
|
||||
# even while its current leaf is still valid.
|
||||
${
|
||||
if hiveLeafRole == null then
|
||||
''fail "services.hyperhive.hiveName is unset, so this hive has no gateway-leaf role to issue from."''
|
||||
else
|
||||
requireHiveIdentity "ask the store for its gateway leaf"
|
||||
}
|
||||
|
||||
${leafCoverage}
|
||||
|
||||
# Half the lifetime the role grants, as for the services leaf, so
|
||||
# the daily timer has half the lifetime to retry through a sealed
|
||||
# store.
|
||||
renewat=$(( ${toString baoDeploy.servicesPkiLeafTtlHours} * 3600 / 2 ))
|
||||
reissue=0
|
||||
{ [ -s "$leaf" ] && [ -s "$leafk" ]; } || reissue=1
|
||||
openssl x509 -in "$leaf" -noout -checkend "$renewat" >/dev/null 2>&1 || reissue=1
|
||||
leavesCoverNames || reissue=1
|
||||
# A leaf the retired hive CA signed, or one under a store root since
|
||||
# replaced, fails this and is re-issued.
|
||||
{ [ -s "$svcroot" ] && openssl verify -CAfile "$svcroot" "$leaf" >/dev/null 2>&1; } || reissue=1
|
||||
|
||||
if [ "$reissue" = 0 ]; then
|
||||
echo "gateway leaf valid, chained to the store's root and covering the configured names — leaving it alone"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "a leaf is missing, near expiry, or missing a configured name — re-signing"
|
||||
before="$(sha256sum "$leaf" 2>/dev/null || true)"
|
||||
err="$(mktemp)"
|
||||
resp="$(mktemp "$d/gateway.json.XXXXXX")"
|
||||
trap 'rm -f "$err" "$resp" "$leaf.new" "$leafk.new"' EXIT
|
||||
|
||||
${signHiveLeaf}
|
||||
# Cert auth is a login, not a transport setting; `-token-only`
|
||||
# skips the token helper, for the reason `swarm-services-cert` gives.
|
||||
if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then
|
||||
fail "could not log in to the swarm secret store with this hive's identity: $(cat "$err")"
|
||||
fi
|
||||
export BAO_TOKEN
|
||||
|
||||
after="$(sha256sum "$leaf" 2>/dev/null || true)"
|
||||
if [ "$before" != "$after" ]; then
|
||||
echo "requesting the gateway leaf from the store"
|
||||
if ! bao write -format=json \
|
||||
${lib.escapeShellArg "${baoServicesPkiMount}/issue/${toString hiveLeafRole}"} \
|
||||
common_name=${lib.escapeShellArg domain} \
|
||||
alt_names=${lib.escapeShellArg "*.${domain}"} \
|
||||
> "$resp" 2>"$err"; then
|
||||
fail "the store refused to issue the gateway leaf: $(cat "$err")"
|
||||
fi
|
||||
|
||||
# One issuance, split, for the reasons `swarm-services-cert` gives:
|
||||
# the key never enters an argv, and `// empty` keeps a missing field
|
||||
# from becoming the four characters `null`. The leaf alone, without
|
||||
# its issuer: the issuer is the self-signed root in the bundle.
|
||||
umask 077
|
||||
jq -r '.data.private_key // empty' < "$resp" > "$leafk.new"
|
||||
jq -r '.data.certificate // empty' < "$resp" > "$leaf.new"
|
||||
for f in "$leafk.new" "$leaf.new"; do
|
||||
[ -s "$f" ] || fail "the store's response was missing a field: $f is empty"
|
||||
done
|
||||
|
||||
mv -f "$leaf.new" "$leaf"
|
||||
mv -f "$leafk.new" "$leafk"
|
||||
chmod 0644 "$leaf"
|
||||
chmod 0600 "$leafk"
|
||||
|
||||
# A gateway already running gets the copy remade and a reload. One
|
||||
# that is not running — a hive that had no leaf — gets started,
|
||||
# `--no-block` so a start job still queued absorbs the request.
|
||||
if systemctl is-active --quiet nginx.service; then
|
||||
echo "gateway leaf rotated — re-importing and reloading nginx"
|
||||
systemctl restart hive-gateway-self-signed-cert.service
|
||||
systemctl reload nginx.service
|
||||
else
|
||||
echo "gateway leaf unchanged (already up to date)"
|
||||
elif [ "$(systemctl show -P LoadState nginx.service)" = loaded ]; then
|
||||
echo "gateway leaf landed with nginx not running — starting it"
|
||||
systemctl reset-failed nginx.service
|
||||
systemctl start --no-block nginx.service
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
|
@ -796,33 +682,18 @@ 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 reader identity and never logs in with it.
|
||||
# A hive without a readable reader identity fails here, naming 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.
|
||||
if [ "$want_svc" = 0 ]; then
|
||||
${
|
||||
if haveReaderIdentity then
|
||||
''
|
||||
missing=""
|
||||
for f in ${
|
||||
lib.escapeShellArgs [
|
||||
baoDeploy.clientCertFile
|
||||
baoDeploy.clientKeyFile
|
||||
]
|
||||
}; do
|
||||
[ -r "$f" ] || missing="$missing $f"
|
||||
done
|
||||
if [ -n "$missing" ]; then
|
||||
echo "this hive has no usable store identity: cannot read$missing." >&2
|
||||
echo "place the files named by services.hyperhive.deploy.bao.clientCertFile (${baoDeploy.clientCertFile}) and services.hyperhive.deploy.bao.clientKeyFile (${baoDeploy.clientKeyFile})." >&2
|
||||
exit 1
|
||||
fi
|
||||
if haveHiveIdentity then
|
||||
requireHiveIdentity "fetch the store's ${baoServicesPkiMount} root"
|
||||
+ ''
|
||||
echo "no swarm service domains configured — fetching only the store's pki root"
|
||||
err="$(mktemp)"
|
||||
raw="$(mktemp "$d/swarm-services-root.XXXXXX")"
|
||||
trap 'rm -f "$err" "$raw"' EXIT
|
||||
# Interim: the reader cert is the handshake identity until each hive
|
||||
# has its own narrow store identity for issuing its leaf.
|
||||
if ! curl --fail --silent --show-error \
|
||||
${storeRootCurlTls} --output "$raw" \
|
||||
"$BAO_ADDR/v1/${baoServicesPkiMount}/ca/pem" 2>"$err"; then
|
||||
|
|
@ -851,14 +722,7 @@ in
|
|||
fi
|
||||
exit 0''
|
||||
else
|
||||
''
|
||||
echo "this hive has no store identity, so it cannot fetch the store's ${baoServicesPkiMount} root." >&2
|
||||
echo "set services.hyperhive.deploy.bao.clientCertFile (${
|
||||
if baoDeploy.clientCertFile == null then "unset" else baoDeploy.clientCertFile
|
||||
}) and services.hyperhive.deploy.bao.clientKeyFile (${
|
||||
if baoDeploy.clientKeyFile == null then "unset" else baoDeploy.clientKeyFile
|
||||
}) and place both files." >&2
|
||||
exit 1''
|
||||
requireHiveIdentity "fetch the store's ${baoServicesPkiMount} root"
|
||||
}
|
||||
fi
|
||||
|
||||
|
|
@ -1075,19 +939,18 @@ 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 = {
|
||||
description = "Weekly gateway-leaf re-sign and propagation";
|
||||
description = "Daily renewal check for the gateway TLS leaf";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
# Run weekly; Persistent=true fires a missed run on next boot if
|
||||
# the timer was not active (e.g. the host was off on the scheduled
|
||||
# day), preventing a dormant timer from letting the leaf lapse.
|
||||
OnCalendar = "weekly";
|
||||
OnCalendar = "daily";
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Daily rather than `hive-tls-resign`'s weekly, for the reason
|
||||
# Daily rather than weekly, for the reason
|
||||
# ./swarm-nats.nix's `swarm-bao-nats-tls` timer is: this leaf needs the
|
||||
# store, which can be sealed or unreachable, so there is a retry per day
|
||||
# from half-life to expiry rather than two.
|
||||
|
|
@ -1100,7 +963,7 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
# Signal the hive-c0re lifecycle that a hive CA exists: it bind-mounts
|
||||
# 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
|
||||
|
|
@ -1130,7 +993,7 @@ in
|
|||
# variable name. The anchor was never missing; nothing pointed the queue
|
||||
# client at it.
|
||||
#
|
||||
# Where there is no hive CA this module contributes nothing at all, and
|
||||
# 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 =
|
||||
|
|
|
|||
Loading…
Reference in a new issue