swarm-controller re-issues an agent's store leaf at half its validity, but the running container kept presenting the copy its hive delivered at start, so a re-issued leaf reached the agent only on its next restart. hive-agent-bao-identity now seeds a live copy of the pair in its own runtime directory from the delivered credentials, and re-runs every 10 minutes: it logs in with the live leaf, reads its own swarm/agents/<agent>/bao-mtls (a read render_agent already grants), and when the leaf changed, logs in with the new pair and swaps it in only if the store accepts it. Every unit in the container that logs in to the store presents the live pair: hive-agent (queue-identity.nix), hive-matrix-daemon, hive-subagent-daemon, hive-agent-forge-token, hive-agent-forge-accounts and hive-agent-github-token. They keep loading the server CA as a credential. The long-running daemons are ordered after the refresher so the live pair exists when they start. It renews only while the held leaf is still valid: the login that reads the new leaf is made with the current one. Refs #5017
148 lines
5.1 KiB
Nix
148 lines
5.1 KiB
Nix
# This agent's GitHub personal access token, fetched from the swarm secret
|
||
# store by the agent itself, into the file ./github.nix's readers use.
|
||
#
|
||
# An operator links the token in the swarm UI; `swarm-controller` stores it at
|
||
# `swarm/agents/<agent>/github-token` (`swarm_secret_client::github`). The
|
||
# agent's own read grant covers that path, so this unit reads it and writes
|
||
# `<state>/github-token`, which the `gh` wrapper, the git credential helper and
|
||
# `hive-github-notify` read.
|
||
#
|
||
# It never deletes. A `github-token` already in place stays when the store has
|
||
# none or cannot be read. The file is replaced by rename, and only when its
|
||
# bytes changed.
|
||
{
|
||
pkgs,
|
||
lib,
|
||
config,
|
||
...
|
||
}:
|
||
let
|
||
cfg = config.services.hyperhive.agent.bao;
|
||
|
||
agentName = config.services.hyperhive.agent.user.name;
|
||
stateDir = "/agents/${agentName}/state";
|
||
|
||
# The server CA id ./bao.nix loads.
|
||
serverCaCredential = "hive-agent-bao-server-ca";
|
||
|
||
unitName = "hive-agent-github-token";
|
||
|
||
# The nix half of `swarm_secret_client::github::account_path` plus
|
||
# `path::MOUNT`.
|
||
tokenPath = "secret/swarm/agents/${agentName}/github-token";
|
||
|
||
runtimeDir = unitName;
|
||
# The store's whole answer, token included: kept in the unit's own `0700`
|
||
# directory, never in the state dir.
|
||
rawFile = "/run/${runtimeDir}/account.json";
|
||
errFile = "/run/${runtimeDir}/bao.err";
|
||
|
||
tokenFile = "${stateDir}/github-token";
|
||
stagedFile = "${stateDir}/.github-token.new";
|
||
|
||
configured = cfg.addr != null && config.services.hyperhive.agent.github.enable;
|
||
|
||
storeRetry = import ../host-modules/lib/store-retry.nix { };
|
||
in
|
||
{
|
||
config = lib.mkIf configured {
|
||
systemd.services.${unitName} = {
|
||
description = "fetch this agent's GitHub token from the secret store";
|
||
after = [
|
||
"network.target"
|
||
"hive-agent-bao-identity.service"
|
||
];
|
||
# The poller reads the token once at start.
|
||
before = [ "hive-github-notify.service" ];
|
||
wantedBy = [ "multi-user.target" ];
|
||
path = [
|
||
pkgs.openbao
|
||
pkgs.coreutils
|
||
pkgs.diffutils
|
||
pkgs.jq
|
||
];
|
||
# ../host-modules/lib/store-retry.nix.
|
||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||
serviceConfig = storeRetry.serviceConfig // {
|
||
Type = "oneshot";
|
||
# Not `RemainAfterExit`, so the timer below can start it again.
|
||
RemainAfterExit = false;
|
||
TimeoutStartSec = 30;
|
||
User = agentName;
|
||
Group = agentName;
|
||
RuntimeDirectory = runtimeDir;
|
||
RuntimeDirectoryMode = "0700";
|
||
# `0600`, the mode `github-token` has.
|
||
UMask = "0077";
|
||
LoadCredential = [ serverCaCredential ];
|
||
};
|
||
environment = {
|
||
BAO_ADDR = cfg.addr;
|
||
BAO_CLIENT_CERT = cfg.clientCertFile;
|
||
BAO_CLIENT_KEY = cfg.clientKeyFile;
|
||
};
|
||
script = ''
|
||
set -euo pipefail
|
||
|
||
# No identity delivered: ./bao.nix's check reports that.
|
||
for f in "$BAO_CLIENT_CERT" "$BAO_CLIENT_KEY"; do
|
||
if [ ! -s "$f" ]; then
|
||
echo "this agent has no store identity, so it cannot fetch its GitHub token." >&2
|
||
exit 0
|
||
fi
|
||
done
|
||
|
||
if [ -s "$CREDENTIALS_DIRECTORY/${serverCaCredential}" ]; then
|
||
export BAO_CACERT="$CREDENTIALS_DIRECTORY/${serverCaCredential}"
|
||
fi
|
||
|
||
err=${lib.escapeShellArg errFile}
|
||
raw=${lib.escapeShellArg rawFile}
|
||
staged=${lib.escapeShellArg stagedFile}
|
||
token=${lib.escapeShellArg tokenFile}
|
||
trap 'rm -f "$err" "$raw" "$staged"' EXIT
|
||
|
||
if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then
|
||
echo "the swarm secret store at $BAO_ADDR did not accept this agent's certificate login:" >&2
|
||
if [ -s "$err" ]; then cat "$err" >&2; fi
|
||
exit 1
|
||
fi
|
||
export BAO_TOKEN
|
||
|
||
# No token linked and a store that cannot answer look alike here, and
|
||
# either way the file in place, if any, is kept.
|
||
if ! bao kv get -format=json ${lib.escapeShellArg tokenPath} >"$raw" 2>"$err"; then
|
||
echo "no GitHub token read from ${tokenPath}; github-token left as it is:" >&2
|
||
if [ -s "$err" ]; then cat "$err" >&2; fi
|
||
exit 0
|
||
fi
|
||
|
||
# ⚠️ The token goes from the store's answer straight into a file; it is
|
||
# never in a variable or an argument. A malformed object exits 0:
|
||
# failing the unit would only restart it into the same answer.
|
||
rm -f "$staged"
|
||
if ! jq -er '.data.data.value | strings' "$raw" >"$staged"; then
|
||
echo "${tokenPath} holds no string value; github-token left as it is." >&2
|
||
exit 0
|
||
fi
|
||
|
||
if cmp -s "$staged" "$token"; then
|
||
echo "this agent's GitHub token at ${tokenPath} is unchanged."
|
||
exit 0
|
||
fi
|
||
mv -f "$staged" "$token"
|
||
echo "fetched this agent's GitHub token from ${tokenPath}."
|
||
'';
|
||
};
|
||
|
||
# The same cadence as ./forge-accounts.nix.
|
||
systemd.timers.${unitName} = {
|
||
description = "re-fetch this agent's GitHub token from the secret store";
|
||
wantedBy = [ "timers.target" ];
|
||
timerConfig = {
|
||
OnUnitInactiveSec = "2min";
|
||
RandomizedDelaySec = "20s";
|
||
};
|
||
};
|
||
};
|
||
}
|