Watch
0
0
Fork
You've already forked hyperhive
0

tls: a CA root change no longer restarts the store or bounces containers

This commit is contained in:
atlas 2026-10-17 12:58:48 +02:00 • committed by mara
commit 041779d910
4 changed files with 136 additions and 23 deletions

View file

@ -139,9 +139,23 @@ in
bundlePath = bundlePathFor name;
unit = "${name}-ca-bundle";
source = if hostUnit then caHostPath else caContainerPath;
# The source as the bundle was last built from it.
anchor = "${dir}/anchor.pem";
refresh = "${unit}-refresh";
in
{
_file = "hive-ca-trust.nix#trustBundle:${name}";
# `hive-tls-ca` rewrites the source in place, so a watch on it sees the
# write from inside a container: inotify is per inode, and a bind mount
# shares the inode. `PathChanged` fires on close-after-write and on
# `chmod`, so `${refresh}` is idempotent.
config.systemd.paths = lib.optionalAttrs (useSelfSigned && enable) {
${refresh} = {
description = "trigger ${refresh} when the trust bundle source is written";
wantedBy = [ "multi-user.target" ];
pathConfig.PathChanged = source;
};
};
config.systemd.services = lib.optionalAttrs (useSelfSigned && enable) (
{
${unit} = {
@ -167,6 +181,10 @@ in
set -euo pipefail
install -d -m 0755 ${dir}
tmp=${bundlePath}.tmp
# One snapshot of the source feeds the count, the bundle and
# the copy `${refresh}` compares the live source against, so
# the three cannot describe different roots.
cp -f ${source} ${anchor}.tmp 2>/dev/null || : > ${anchor}.tmp
# Count the HIVE half on its own, before assembling.
#
# Inspecting the assembled file cannot work: the system store
@ -193,13 +211,13 @@ in
if [ -z "$n" ]; then n=0; fi
printf '%s' "$n"
}
contributed=$(certs ${source})
contributed=$(certs ${anchor}.tmp)
if [ "$contributed" -eq 0 ]; then
echo "${unit}: ${source} contributed no certificate to the bundle" >&2
exit 1
fi
system=$(certs /etc/ssl/certs/ca-certificates.crt)
cat /etc/ssl/certs/ca-certificates.crt ${source} > "$tmp"
cat /etc/ssl/certs/ca-certificates.crt ${anchor}.tmp > "$tmp"
# `cat` of a source truncated between the count and the copy still
# exits 0, so the result is checked against what both halves
# brought rather than merely for being non-empty.
@ -215,9 +233,45 @@ in
# handshake can. Do not read a green assembly as a working trust
# store.
chmod 0644 "$tmp"
mv ${anchor}.tmp ${anchor}
mv "$tmp" ${bundlePath}
'';
};
# Re-runs the bundle unit, and with it every consumer, when the
# source holds a root the bundle was not built from. Separate from
# the bundle unit because that one is `Before=` its consumers: a
# restart issued from inside it would wait on jobs that wait on it.
# `--no-block` keeps this one from waiting on them too.
#
# A restart of `${unit}` restarts every active unit that
# `Requires=` it; the explicit `start` is for the consumers that are
# not active, which a first boot leaves failed on the dependency.
${refresh} = {
description = "rebuild ${name} TLS trust bundle when its source changes";
wantedBy = [ "multi-user.target" ];
after = [ "${unit}.service" ];
serviceConfig = {
Type = "oneshot";
SyslogIdentifier = refresh;
};
path = [
pkgs.coreutils
pkgs.diffutils
pkgs.gnugrep
pkgs.systemd
];
script = ''
set -euo pipefail
# No root yet: nothing to converge on. The write that fills the
# source fires this unit again.
if ! grep -q 'BEGIN CERTIFICATE' ${source}; then exit 0; fi
if cmp -s ${source} ${anchor}; then exit 0; fi
echo "${source} changed — rebuilding the ${name} trust bundle"
systemctl restart --no-block ${unit}.service
systemctl start --no-block ${lib.concatMapStringsSep " " (c: "${c}.service") consumers}
'';
};
}
// lib.genAttrs consumers (_: {
requires = [ "${unit}.service" ];