agents fetch the hive CA from bao at boot instead of the meta flake embedding it
This commit is contained in:
parent
0831b85b73
commit
d547b521f6
12 changed files with 329 additions and 246 deletions
|
|
@ -43,6 +43,7 @@ in
|
|||
./frontend.nix
|
||||
./github.nix
|
||||
./github-token.nix
|
||||
./hive-ca.nix
|
||||
./hive-facts.nix
|
||||
./logs.nix
|
||||
./matrix.nix
|
||||
|
|
|
|||
193
nix/agent-modules/hive-ca.nix
Normal file
193
nix/agent-modules/hive-ca.nix
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# 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}"
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue