196 lines
7 KiB
Nix
196 lines
7 KiB
Nix
# The swarm's root certificate in this agent's trust, fetched from the store
|
|
# at boot by the agent itself.
|
|
#
|
|
# The root is the store's services `pki` mount's CA. It does not exist when the
|
|
# agent is built, so the meta flake names no CA file: this unit reads it from
|
|
# the store with the agent's own certificate (./bao.nix keeps it current) and
|
|
# writes two files, both public:
|
|
#
|
|
# - `bundle`: the system CAs plus the swarm root. `SSL_CERT_FILE` replaces the
|
|
# trust store of Rust (rustls platform verifier), Go and OpenSSL clients, so
|
|
# it carries both halves.
|
|
# - `root`: the swarm root alone, for `NODE_EXTRA_CA_CERTS`, which adds to
|
|
# Node's store (Claude Code).
|
|
#
|
|
# A process reads its trust when it starts, so a rotated root reaches the
|
|
# consumers below at their next start. They `Requires=` this unit, which keeps
|
|
# one from running with a store that lacks the root.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.agent.bao;
|
|
agentCfg = config.services.hyperhive.agent;
|
|
|
|
agentName = agentCfg.user.name;
|
|
|
|
# The server CA id ./bao.nix loads.
|
|
serverCaCredential = "hive-agent-bao-server-ca";
|
|
|
|
unitName = "hive-agent-trust-bundle";
|
|
runtimeDir = unitName;
|
|
bundleFile = "/run/${runtimeDir}/bundle.pem";
|
|
rootFile = "/run/${runtimeDir}/root.pem";
|
|
|
|
systemBundle = "/etc/ssl/certs/ca-certificates.crt";
|
|
|
|
configured = cfg.addr != null;
|
|
|
|
storeRetry = import ../host-modules/lib/store-retry.nix { };
|
|
|
|
# Units that dial a swarm service over TLS. The first group is defined
|
|
# whenever the agent module is; the rest exist under the same condition as
|
|
# their definitions, because ordering on a unit that is not defined would
|
|
# generate an inert one.
|
|
consumers = [
|
|
"hive-agent"
|
|
"hive-bash-daemon"
|
|
"hive-subagent-daemon"
|
|
"hive-mcp-http"
|
|
"hive-forge-notify"
|
|
]
|
|
++ lib.optional (agentCfg.matrixAccounts != { }) "hive-matrix-daemon"
|
|
++ lib.optional (agentCfg.github.enable) "hive-github-notify"
|
|
++ lib.optional (agentCfg.icon != null && agentCfg.forge.url != null) "forge-avatar-sync";
|
|
|
|
trustEnv = {
|
|
SSL_CERT_FILE = bundleFile;
|
|
NODE_EXTRA_CA_CERTS = rootFile;
|
|
};
|
|
in
|
|
{
|
|
options.services.hyperhive.agent.bao.servicesPkiMount = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "pki";
|
|
example = "pki";
|
|
description = ''
|
|
Mount path of the store's services `pki` engine, whose root is the one
|
|
anchor every swarm service certificate chains to. Set by the generated
|
|
meta flake from the host's
|
|
`services.hyperhive.deploy.bao.servicesPkiMountPath`.
|
|
`${unitName}.service` reads `<mount>/ca/pem` from it.
|
|
'';
|
|
};
|
|
|
|
config = lib.mkIf configured {
|
|
# Every process, including a bash task's children, resolves trust through
|
|
# these, as it resolves `HIVE_FORGE_TOKEN_FILE`.
|
|
systemd.globalEnvironment = trustEnv;
|
|
environment.variables = trustEnv;
|
|
|
|
systemd.services = {
|
|
${unitName} = {
|
|
description = "fetch the swarm root certificate from the secret store";
|
|
after = [
|
|
"network.target"
|
|
"hive-agent-bao-identity.service"
|
|
];
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = [
|
|
pkgs.coreutils
|
|
pkgs.curl
|
|
pkgs.openssl
|
|
];
|
|
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
|
serviceConfig = storeRetry.serviceConfig // {
|
|
Type = "oneshot";
|
|
RemainAfterExit = false;
|
|
TimeoutStartSec = 60;
|
|
User = agentName;
|
|
Group = agentName;
|
|
RuntimeDirectory = runtimeDir;
|
|
# Public certificates, read by every consumer's user.
|
|
RuntimeDirectoryMode = "0755";
|
|
RuntimeDirectoryPreserve = "yes";
|
|
UMask = "0022";
|
|
LoadCredential = [ serverCaCredential ];
|
|
};
|
|
# Overrides the global value: this unit runs before the bundle exists,
|
|
# and its own TLS is checked against the credential below or the
|
|
# system bundle.
|
|
environment = {
|
|
SSL_CERT_FILE = lib.mkForce systemBundle;
|
|
BAO_ADDR = cfg.addr;
|
|
BAO_CLIENT_CERT = cfg.clientCertFile;
|
|
BAO_CLIENT_KEY = cfg.clientKeyFile;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
for f in "$BAO_CLIENT_CERT" "$BAO_CLIENT_KEY"; do
|
|
if [ ! -s "$f" ]; then
|
|
echo "this agent has no store identity at $f, so it cannot fetch the swarm root certificate." >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
cacert=
|
|
if [ -s "$CREDENTIALS_DIRECTORY/${serverCaCredential}" ]; then
|
|
cacert="$CREDENTIALS_DIRECTORY/${serverCaCredential}"
|
|
echo "trusting bao via the delivered server CA credential" >&2
|
|
else
|
|
echo "no bao server CA credential delivered; trusting the system bundle" >&2
|
|
fi
|
|
|
|
dir=/run/${runtimeDir}
|
|
err=$(mktemp -p "$dir" err.XXXXXX)
|
|
raw=$(mktemp -p "$dir" raw.XXXXXX)
|
|
root=$(mktemp -p "$dir" root.XXXXXX)
|
|
bundle=$(mktemp -p "$dir" bundle.XXXXXX)
|
|
trap 'rm -f "$err" "$raw" "$root" "$bundle"' EXIT
|
|
|
|
# The listener demands a client certificate and the endpoint takes no
|
|
# token, so no login precedes this.
|
|
if ! curl --fail --silent --show-error \
|
|
--connect-timeout 10 --max-time 30 \
|
|
''${cacert:+--cacert "$cacert"} \
|
|
--cert "$BAO_CLIENT_CERT" --key "$BAO_CLIENT_KEY" \
|
|
--output "$raw" \
|
|
${lib.escapeShellArg "${cfg.addr}/v1/${cfg.servicesPkiMount}/ca/pem"} 2>"$err"; then
|
|
echo "could not read the store's ${cfg.servicesPkiMount} root at $BAO_ADDR." >&2
|
|
cat "$err" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Re-encoded rather than copied: it fails on a body that is not a
|
|
# certificate and ends the file in the newline the concatenation
|
|
# below relies on.
|
|
if ! openssl x509 -in "$raw" -out "$root" 2>"$err"; then
|
|
echo "the store's ${cfg.servicesPkiMount} root does not parse as a certificate." >&2
|
|
cat "$err" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cat ${systemBundle} "$root" >"$bundle"
|
|
|
|
# Neither file moves unless it changed, so a reader never sees a half
|
|
# write and the timer's rerun is silent.
|
|
if cmp -s "$bundle" ${bundleFile} && cmp -s "$root" ${rootFile}; then
|
|
echo "the store's ${cfg.servicesPkiMount} root is unchanged"
|
|
exit 0
|
|
fi
|
|
chmod 0644 "$root" "$bundle"
|
|
mv -f "$root" ${rootFile}
|
|
mv -f "$bundle" ${bundleFile}
|
|
echo "wrote the swarm root certificate from the store's ${cfg.servicesPkiMount} mount"
|
|
'';
|
|
};
|
|
}
|
|
// lib.genAttrs consumers (_: {
|
|
after = [ "${unitName}.service" ];
|
|
requires = [ "${unitName}.service" ];
|
|
});
|
|
|
|
systemd.timers.${unitName} = {
|
|
description = "re-read the swarm root certificate from the secret store";
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnUnitInactiveSec = "1h";
|
|
RandomizedDelaySec = "1min";
|
|
};
|
|
};
|
|
};
|
|
}
|