Compare commits
17 changed files with 172 additions and 430 deletions
|
|
@ -62,7 +62,7 @@ of the cell says how.
|
||||||
| `swarm/controller/swarm-controller/matrix/appservice-token` | `swarm-matrix-ctl`, inside the `hive-matrix` container, once | `swarm-controller`, under its own certificate | ❌ `swarm-matrix-ctl` mints it once; the container keeps its copy and republishes it when the store's differs | ✅ the controller reads it on every five-minute matrix pass |
|
| `swarm/controller/swarm-controller/matrix/appservice-token` | `swarm-matrix-ctl`, inside the `hive-matrix` container, once | `swarm-controller`, under its own certificate | ❌ `swarm-matrix-ctl` mints it once; the container keeps its copy and republishes it when the store's differs | ✅ the controller reads it on every five-minute matrix pass |
|
||||||
| `swarm/controller/swarm-controller/oidc/client` | authelia, at its first boot; `swarm-secret-publish` copies it in | `swarm-controller`, under its own certificate, once at start | ❌ authelia mints it once. A re-mint is republished by `swarm-secret-publish`'s path unit | ❌ read once at start; the controller holds the old value until it restarts |
|
| `swarm/controller/swarm-controller/oidc/client` | authelia, at its first boot; `swarm-secret-publish` copies it in | `swarm-controller`, under its own certificate, once at start | ❌ authelia mints it once. A re-mint is republished by `swarm-secret-publish`'s path unit | ❌ read once at start; the controller holds the old value until it restarts |
|
||||||
| `swarm/controller/swarm-controller/forge/token` | the forge, inside the `hive-forge` container, once; `swarm-bao-forge-publisher` on the forge's host copies it in under its own certificate, with a write-only grant | `swarm-controller`, under its own certificate, at start | ❌ the forge mints it once and keeps its copy in the container; the publisher writes that copy each time the forge container starts and each time the mint writes the file | ❌ the controller reads it at start, and in the background with backoff until it first finds it; a replaced token reaches it with the next restart |
|
| `swarm/controller/swarm-controller/forge/token` | the forge, inside the `hive-forge` container, once; `swarm-bao-forge-publisher` on the forge's host copies it in under its own certificate, with a write-only grant | `swarm-controller`, under its own certificate, at start | ❌ the forge mints it once and keeps its copy in the container; the publisher writes that copy each time the forge container starts and each time the mint writes the file | ❌ the controller reads it at start, and in the background with backoff until it first finds it; a replaced token reaches it with the next restart |
|
||||||
| `swarm/agents/<agent>/bao-mtls` | the store's agent PKI mount (`deploy.bao.agentPkiMountPath`), which generates the key, at `swarm-controller`'s request at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config; then `hive-agent-bao-identity` in the agent container, under the leaf it holds | ✅ `swarm-controller`'s five-minute pass re-issues a live agent's leaf once it's past half its validity (45 of 90 days, read from the certificate itself) | ✅ `hive-agent-bao-identity` re-reads it every 10 minutes and swaps a new leaf into `/run/hive-agent-bao-identity/`, which every store login in the container presents. It reads under the leaf it holds, so it renews only while that leaf is valid |
|
| `swarm/agents/<agent>/bao-mtls` | the store's agent PKI mount (`deploy.bao.agentPkiMountPath`), which generates the key, at `swarm-controller`'s request at agent creation | `hive-c0re`, under the hive's own certificate, when it writes the agent's container config | ✅ `swarm-controller`'s five-minute pass re-issues a live agent's leaf once it's past half its validity (45 of 90 days, read from the certificate itself) | ❌ `hive-c0re` reads it when it writes the container config, so the agent presents a new leaf from its next start; the old leaf stays valid until it expires |
|
||||||
| `swarm/agents/<agent>/queue` | `swarm-controller`, at agent creation | `hive-agent` in the agent container, under the agent's own certificate, held in memory — the identity it presents to the swarm queue, naming that one agent rather than its hive | ✅ `swarm-controller`'s five-minute pass re-mints a live agent's secret once it's 45 days old by `minted_at` on the stored object; a secret with no `minted_at` gets one stamped, value unchanged. The pass skips agents declared `Destroyed` — declaring an agent destroyed deletes every version of the path instead, the undo of the mint rather than another one | ✅ `hive-agent` reads the path before its first connect and again on every reconnect attempt, so a reconnect after a re-mint presents the new secret. An open connection keeps the secret it connected with; after a revocation the agent keeps retrying under the queue client's backoff |
|
| `swarm/agents/<agent>/queue` | `swarm-controller`, at agent creation | `hive-agent` in the agent container, under the agent's own certificate, held in memory — the identity it presents to the swarm queue, naming that one agent rather than its hive | ✅ `swarm-controller`'s five-minute pass re-mints a live agent's secret once it's 45 days old by `minted_at` on the stored object; a secret with no `minted_at` gets one stamped, value unchanged. The pass skips agents declared `Destroyed` — declaring an agent destroyed deletes every version of the path instead, the undo of the mint rather than another one | ✅ `hive-agent` reads the path before its first connect and again on every reconnect attempt, so a reconnect after a re-mint presents the new secret. An open connection keeps the secret it connected with; after a revocation the agent keeps retrying under the queue client's backoff |
|
||||||
| `swarm/agents/<agent>/forge-token` | `swarm-controller`, at agent creation and in a pass every 5 minutes over every agent with a store identity | the agent container itself, under its own certificate, fetched to `/run/hive-agent-forge-token/token` | ✅ the controller re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes) | ✅ the agent re-fetches on a 10-minute timer |
|
| `swarm/agents/<agent>/forge-token` | `swarm-controller`, at agent creation and in a pass every 5 minutes over every agent with a store identity | the agent container itself, under its own certificate, fetched to `/run/hive-agent-forge-token/token` | ✅ the controller re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes) | ✅ the agent re-fetches on a 10-minute timer |
|
||||||
| `swarm/agents/<agent>/forge/<label>` | `swarm-controller`, when an operator links an external forge account in the swarm UI | `hive-agent-forge-accounts` in the agent container, under the agent's own certificate, into `<state>/forge-<label>-token` and `forge-<label>.json` | ❌ an operator's token; replaced only by deleting it and linking the label again | ✅ the agent re-fetches on a 2-minute timer |
|
| `swarm/agents/<agent>/forge/<label>` | `swarm-controller`, when an operator links an external forge account in the swarm UI | `hive-agent-forge-accounts` in the agent container, under the agent's own certificate, into `<state>/forge-<label>-token` and `forge-<label>.json` | ❌ an operator's token; replaced only by deleting it and linking the label again | ✅ the agent re-fetches on a 2-minute timer |
|
||||||
|
|
@ -170,10 +170,7 @@ as root and re-exposes it under the consuming unit's own user.
|
||||||
Inside the container, `hive-agent-bao-identity.service` logs in with that
|
Inside the container, `hive-agent-bao-identity.service` logs in with that
|
||||||
certificate and reads this row back before reporting success, so an agent
|
certificate and reads this row back before reporting success, so an agent
|
||||||
locked out of its own identity says so at boot rather than at whichever pull
|
locked out of its own identity says so at boot rather than at whichever pull
|
||||||
needed the store first. It keeps a live copy of the pair in `/run/hive-agent-bao-identity/`,
|
needed the store first. The unit exists whenever
|
||||||
which every unit in the container that logs in to the store presents, and
|
|
||||||
re-runs every 10 minutes: a leaf the controller re-issued replaces that copy
|
|
||||||
once a login with it succeeds. The unit exists whenever
|
|
||||||
`services.hyperhive.agent.bao.addr` has a value, which the hive's meta flake
|
`services.hyperhive.agent.bao.addr` has a value, which the hive's meta flake
|
||||||
sets from its own store address — the same all-or-nothing gate the per-hive
|
sets from its own store address — the same all-or-nothing gate the per-hive
|
||||||
queue credential beside it uses, and the reason the delivery above never lands
|
queue credential beside it uses, and the reason the delivery above never lands
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
# This agent's own identity at the swarm secret store: the check that proves
|
# This agent's own identity at the swarm secret store, and the check that
|
||||||
# it works, and the live copy every store reader in the container presents.
|
# proves it works.
|
||||||
#
|
#
|
||||||
# `swarm-controller` mints the leaf at agent creation and publishes it; this
|
# `swarm-controller` mints the leaf at agent creation and publishes it; this
|
||||||
# agent's hive collects it under the hive's own certificate and hands it in as
|
# agent's hive collects it under the hive's own certificate and hands it in as
|
||||||
# systemd credentials (`hive_c0re::lifecycle::agent_identity`). That copy is
|
# systemd credentials (`hive_c0re::lifecycle::agent_identity`). Nothing here
|
||||||
# fixed for the life of the container, and the controller re-issues the leaf
|
# fetches anything from the store — by the time this container boots, its
|
||||||
# at half its validity, so this unit seeds a live copy from it and, on a timer,
|
# identity is already inside it.
|
||||||
# reads the current leaf back from the store under the one it holds. Because
|
|
||||||
# that login needs the held leaf to still be valid, a container whose leaf
|
|
||||||
# expired while every pull failed recovers only at its next start.
|
|
||||||
#
|
#
|
||||||
# ⚠️ The identity arrives as credentials and NOT as a bind mount, and the mode
|
# ⚠️ The identity arrives as credentials and NOT as a bind mount, and the mode
|
||||||
# is why: the host file is `0600` to the hive daemon and this unit runs as the
|
# is why: the host file is `0600` to the hive daemon and this unit runs as the
|
||||||
|
|
@ -49,17 +46,6 @@ let
|
||||||
|
|
||||||
unitName = "hive-agent-bao-identity";
|
unitName = "hive-agent-bao-identity";
|
||||||
|
|
||||||
runtimeDir = unitName;
|
|
||||||
# A symlink to the directory that holds the live pair. Each pair is written
|
|
||||||
# into a fresh directory and goes live with one rename of this link, so the
|
|
||||||
# link always names one whole pair, old or new.
|
|
||||||
liveLink = "/run/${runtimeDir}/current";
|
|
||||||
certFile = "${liveLink}/cert";
|
|
||||||
keyFile = "${liveLink}/key";
|
|
||||||
# The store's whole answer, key included, in the unit's own `0700` directory.
|
|
||||||
rawFile = "/run/${runtimeDir}/bao-mtls.json";
|
|
||||||
errFile = "/run/${runtimeDir}/bao.err";
|
|
||||||
|
|
||||||
# Where the identity lives in the store, spelled from the same pieces the
|
# Where the identity lives in the store, spelled from the same pieces the
|
||||||
# publisher uses. `swarm_secret_client::mtls::identity_path` builds
|
# publisher uses. `swarm_secret_client::mtls::identity_path` builds
|
||||||
# `swarm/agents/<agent>/bao-mtls` and `path::MOUNT` is `secret`; this
|
# `swarm/agents/<agent>/bao-mtls` and `path::MOUNT` is `secret`; this
|
||||||
|
|
@ -101,39 +87,16 @@ in
|
||||||
loudest possible way about a deployment that never asked for it.
|
loudest possible way about a deployment that never asked for it.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
clientCertFile = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
readOnly = true;
|
|
||||||
default = certFile;
|
|
||||||
description = ''
|
|
||||||
This agent's current store certificate, kept by `${unitName}.service`.
|
|
||||||
Read-only: it is where that unit writes, not a knob. Every unit that
|
|
||||||
logs in to the store as this agent points `BAO_CLIENT_CERT` here.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
clientKeyFile = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
readOnly = true;
|
|
||||||
default = keyFile;
|
|
||||||
description = ''
|
|
||||||
The private key for `clientCertFile`, `0400` to the agent user.
|
|
||||||
🩸 A PATH and never a value.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf configured {
|
config = lib.mkIf configured {
|
||||||
systemd.services.${unitName} = {
|
systemd.services.${unitName} = {
|
||||||
description = "keep this agent's swarm secret store identity current, and prove it works";
|
description = "prove this agent can authenticate to the swarm secret store as itself";
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
path = [
|
path = [
|
||||||
pkgs.openbao
|
pkgs.openbao
|
||||||
pkgs.coreutils
|
pkgs.coreutils
|
||||||
pkgs.diffutils
|
|
||||||
pkgs.jq
|
|
||||||
];
|
];
|
||||||
# ../host-modules/lib/store-retry.nix. From an agent the store is reached
|
# ../host-modules/lib/store-retry.nix. From an agent the store is reached
|
||||||
# through the gateway's stream passthrough, so either being down fails
|
# through the gateway's stream passthrough, so either being down fails
|
||||||
|
|
@ -141,16 +104,10 @@ in
|
||||||
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
||||||
serviceConfig = storeRetry.serviceConfig // {
|
serviceConfig = storeRetry.serviceConfig // {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
# Not `RemainAfterExit`, so the timer below can start it again.
|
RemainAfterExit = true;
|
||||||
# `RuntimeDirectoryPreserve` keeps the live pair between runs.
|
|
||||||
RemainAfterExit = false;
|
|
||||||
TimeoutStartSec = 30;
|
TimeoutStartSec = 30;
|
||||||
User = agentName;
|
User = agentName;
|
||||||
Group = agentName;
|
Group = agentName;
|
||||||
RuntimeDirectory = runtimeDir;
|
|
||||||
RuntimeDirectoryMode = "0700";
|
|
||||||
RuntimeDirectoryPreserve = "yes";
|
|
||||||
UMask = "0377";
|
|
||||||
# Bare ids, no paths: the terse `LoadCredential=` form that inherits a
|
# Bare ids, no paths: the terse `LoadCredential=` form that inherits a
|
||||||
# credential the service *manager* received, which is what the
|
# credential the service *manager* received, which is what the
|
||||||
# container manager passed in. ./queue.nix states the same shape.
|
# container manager passed in. ./queue.nix states the same shape.
|
||||||
|
|
@ -162,8 +119,9 @@ in
|
||||||
};
|
};
|
||||||
environment = {
|
environment = {
|
||||||
BAO_ADDR = cfg.addr;
|
BAO_ADDR = cfg.addr;
|
||||||
BAO_CLIENT_CERT = certFile;
|
# `%d` is `$CREDENTIALS_DIRECTORY`, per-unit and owned by `User=`.
|
||||||
BAO_CLIENT_KEY = keyFile;
|
BAO_CLIENT_CERT = "%d/${certCredential}";
|
||||||
|
BAO_CLIENT_KEY = "%d/${keyCredential}";
|
||||||
};
|
};
|
||||||
script = ''
|
script = ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -192,53 +150,8 @@ in
|
||||||
export BAO_CACERT="$CREDENTIALS_DIRECTORY/${serverCaCredential}"
|
export BAO_CACERT="$CREDENTIALS_DIRECTORY/${serverCaCredential}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# `UMask=0377` makes every file this script creates `0400`, so each one
|
err="$(mktemp)"
|
||||||
# is removed before the command that creates it.
|
trap 'rm -f "$err"' EXIT
|
||||||
err=${lib.escapeShellArg errFile}
|
|
||||||
raw=${lib.escapeShellArg rawFile}
|
|
||||||
trap 'rm -f "$err" "$raw"' EXIT
|
|
||||||
|
|
||||||
live=${lib.escapeShellArg liveLink}
|
|
||||||
dir=''${live%/*}
|
|
||||||
|
|
||||||
# A fresh directory for one pair. `chmod`, because the umask cuts
|
|
||||||
# mktemp's `0700` down to a `0400` no file can be created in.
|
|
||||||
new_pair() {
|
|
||||||
local d
|
|
||||||
d=$(mktemp -d -p "$dir" pair.XXXXXX)
|
|
||||||
chmod 0700 "$d"
|
|
||||||
echo "$d"
|
|
||||||
}
|
|
||||||
|
|
||||||
# The only step that changes the live pair: one rename of the link. A
|
|
||||||
# reader whose two opens straddle it gets one file of each pair for
|
|
||||||
# that one login; the next login reads a whole pair.
|
|
||||||
go_live() {
|
|
||||||
rm -f "$live.new"
|
|
||||||
ln -s "''${1##*/}" "$live.new"
|
|
||||||
mv -Tf "$live.new" "$live"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Every pair directory but the live one is left over: the pair the
|
|
||||||
# last swap replaced, kept until now for a reader that resolved the
|
|
||||||
# link before the swap, or a pair a stopped run staged.
|
|
||||||
current=$(readlink "$live" || true)
|
|
||||||
for d in "$dir"/pair.*; do
|
|
||||||
if [ -d "$d" ] && [ "''${d##*/}" != "$current" ]; then
|
|
||||||
chmod 0700 "$d"
|
|
||||||
rm -rf "$d"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
rm -f "$live.new"
|
|
||||||
|
|
||||||
# The delivered pair is the start of every container's life: /run is
|
|
||||||
# empty at boot and kept across this unit's runs.
|
|
||||||
if [ ! -s "$BAO_CLIENT_CERT" ] || [ ! -s "$BAO_CLIENT_KEY" ]; then
|
|
||||||
seed=$(new_pair)
|
|
||||||
cp "$CREDENTIALS_DIRECTORY/${keyCredential}" "$seed/key"
|
|
||||||
cp "$CREDENTIALS_DIRECTORY/${certCredential}" "$seed/cert"
|
|
||||||
go_live "$seed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cert auth is a login, not a transport setting. The `BAO_CLIENT_*`
|
# Cert auth is a login, not a transport setting. The `BAO_CLIENT_*`
|
||||||
# variables above only decide which certificate the TLS handshake
|
# variables above only decide which certificate the TLS handshake
|
||||||
|
|
@ -250,7 +163,6 @@ in
|
||||||
# it accepts, so this agent's leaf matches its own role and no other.
|
# it accepts, so this agent's leaf matches its own role and no other.
|
||||||
# Naming the role here would be a third copy of a string
|
# Naming the role here would be a third copy of a string
|
||||||
# `swarm_secret_client::policy::agent_object_name` already owns.
|
# `swarm_secret_client::policy::agent_object_name` already owns.
|
||||||
rm -f "$err"
|
|
||||||
if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then
|
if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then
|
||||||
echo "this agent's certificate was refused by the swarm secret store at $BAO_ADDR." >&2
|
echo "this agent's certificate was refused by the swarm secret store at $BAO_ADDR." >&2
|
||||||
if [ -s "$err" ]; then
|
if [ -s "$err" ]; then
|
||||||
|
|
@ -263,13 +175,16 @@ in
|
||||||
export BAO_TOKEN
|
export BAO_TOKEN
|
||||||
|
|
||||||
# A token is not yet an answer: the login proves the certificate, this
|
# A token is not yet an answer: the login proves the certificate, this
|
||||||
# proves the policy attached to it, and the answer is the current
|
# proves the policy attached to it. Reading this agent's own identity
|
||||||
# leaf. One read, so the certificate and key come from one version.
|
# back is the smallest read its document grants, and it is the same
|
||||||
|
# check `swarm-controller` runs against the leaf before it reports the
|
||||||
|
# creation done — so a policy that drifted apart from the path fails
|
||||||
|
# here rather than in whichever pull needed it first.
|
||||||
#
|
#
|
||||||
# ⚠️ Written by redirect, never printed: the object holds a private
|
# ⚠️ Output discarded, not printed: the field is a certificate and the
|
||||||
# key.
|
# object beside it is a private key. Nothing about this check needs a
|
||||||
rm -f "$err" "$raw"
|
# value, only whether the read succeeded.
|
||||||
if ! bao kv get -format=json ${lib.escapeShellArg identityPath} >"$raw" 2>"$err"; then
|
if ! bao kv get -field=cert ${lib.escapeShellArg identityPath} >/dev/null 2>"$err"; then
|
||||||
echo "this agent logged in to the swarm secret store but cannot read ${identityPath}, so its policy does not cover its own path." >&2
|
echo "this agent logged in to the swarm secret store but cannot read ${identityPath}, so its policy does not cover its own path." >&2
|
||||||
if [ -s "$err" ]; then
|
if [ -s "$err" ]; then
|
||||||
cat "$err" >&2
|
cat "$err" >&2
|
||||||
|
|
@ -279,46 +194,8 @@ in
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# `-j`: the bytes as the store holds them, which is what the hive
|
echo "authenticated to the swarm secret store at $BAO_ADDR as ${agentName} and read ${identityPath}."
|
||||||
# delivered, so an unchanged leaf compares equal to the seeded copy.
|
|
||||||
staged=$(new_pair)
|
|
||||||
if ! jq -ej '.data.data.cert | strings' "$raw" >"$staged/cert" \
|
|
||||||
|| ! jq -ej '.data.data.key | strings' "$raw" >"$staged/key"; then
|
|
||||||
rm -rf "$staged"
|
|
||||||
echo "${identityPath} holds no certificate and key; this agent keeps the one it has." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if cmp -s "$staged/cert" "$BAO_CLIENT_CERT" && cmp -s "$staged/key" "$BAO_CLIENT_KEY"; then
|
|
||||||
rm -rf "$staged"
|
|
||||||
echo "authenticated to the swarm secret store at $BAO_ADDR as ${agentName}; its certificate at ${identityPath} is unchanged."
|
|
||||||
else
|
|
||||||
# This unit's next login uses whatever goes live, so a pair the
|
|
||||||
# store refuses must never replace the one it accepts.
|
|
||||||
rm -f "$err"
|
|
||||||
if ! BAO_CLIENT_CERT="$staged/cert" BAO_CLIENT_KEY="$staged/key" \
|
|
||||||
bao login -method=cert -token-only >/dev/null 2>"$err"; then
|
|
||||||
rm -rf "$staged"
|
|
||||||
echo "the swarm secret store refused the certificate at ${identityPath}; this agent keeps the one it has." >&2
|
|
||||||
if [ -s "$err" ]; then cat "$err" >&2; fi
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
go_live "$staged"
|
|
||||||
echo "replaced this agent's store certificate with the one at ${identityPath}."
|
|
||||||
fi
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# The controller re-issues a leaf at half its validity, so ten minutes is
|
|
||||||
# small against the time the old one stays valid.
|
|
||||||
systemd.timers.${unitName} = {
|
|
||||||
description = "re-read this agent's store certificate from the secret store";
|
|
||||||
wantedBy = [ "timers.target" ];
|
|
||||||
timerConfig = {
|
|
||||||
OnUnitInactiveSec = "10min";
|
|
||||||
RandomizedDelaySec = "1min";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ let
|
||||||
agentName = config.services.hyperhive.agent.user.name;
|
agentName = config.services.hyperhive.agent.user.name;
|
||||||
stateDir = "/agents/${agentName}/state";
|
stateDir = "/agents/${agentName}/state";
|
||||||
|
|
||||||
# The server CA id ./bao.nix loads.
|
# The same three ids ./bao.nix and ./forge-token.nix load.
|
||||||
|
certCredential = "hive-agent-bao-cert";
|
||||||
|
keyCredential = "hive-agent-bao-key";
|
||||||
serverCaCredential = "hive-agent-bao-server-ca";
|
serverCaCredential = "hive-agent-bao-server-ca";
|
||||||
|
|
||||||
unitName = "hive-agent-forge-accounts";
|
unitName = "hive-agent-forge-accounts";
|
||||||
|
|
@ -71,19 +73,23 @@ in
|
||||||
RuntimeDirectoryMode = "0700";
|
RuntimeDirectoryMode = "0700";
|
||||||
# `0600`, the mode the files in the state dir have always had.
|
# `0600`, the mode the files in the state dir have always had.
|
||||||
UMask = "0077";
|
UMask = "0077";
|
||||||
LoadCredential = [ serverCaCredential ];
|
LoadCredential = [
|
||||||
|
certCredential
|
||||||
|
keyCredential
|
||||||
|
serverCaCredential
|
||||||
|
];
|
||||||
};
|
};
|
||||||
environment = {
|
environment = {
|
||||||
BAO_ADDR = cfg.addr;
|
BAO_ADDR = cfg.addr;
|
||||||
BAO_CLIENT_CERT = cfg.clientCertFile;
|
BAO_CLIENT_CERT = "%d/${certCredential}";
|
||||||
BAO_CLIENT_KEY = cfg.clientKeyFile;
|
BAO_CLIENT_KEY = "%d/${keyCredential}";
|
||||||
};
|
};
|
||||||
script = ''
|
script = ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# No identity delivered: ./bao.nix's check reports that.
|
# No identity delivered: ./bao.nix's check reports that.
|
||||||
for f in "$BAO_CLIENT_CERT" "$BAO_CLIENT_KEY"; do
|
for id in ${lib.escapeShellArg certCredential} ${lib.escapeShellArg keyCredential}; do
|
||||||
if [ ! -s "$f" ]; then
|
if [ ! -s "$CREDENTIALS_DIRECTORY/$id" ]; then
|
||||||
echo "this agent has no store identity, so it cannot fetch its external forge accounts." >&2
|
echo "this agent has no store identity, so it cannot fetch its external forge accounts." >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ let
|
||||||
# The name `swarm-controller` minted the token under — see ./queue-identity.nix.
|
# The name `swarm-controller` minted the token under — see ./queue-identity.nix.
|
||||||
agentName = config.services.hyperhive.agent.user.name;
|
agentName = config.services.hyperhive.agent.user.name;
|
||||||
|
|
||||||
# The server CA id ./bao.nix loads.
|
# The same three ids ./bao.nix and ./queue-identity.nix load.
|
||||||
|
certCredential = "hive-agent-bao-cert";
|
||||||
|
keyCredential = "hive-agent-bao-key";
|
||||||
serverCaCredential = "hive-agent-bao-server-ca";
|
serverCaCredential = "hive-agent-bao-server-ca";
|
||||||
|
|
||||||
unitName = "hive-agent-forge-token";
|
unitName = "hive-agent-forge-token";
|
||||||
|
|
@ -106,25 +108,29 @@ in
|
||||||
RuntimeDirectoryMode = "0700";
|
RuntimeDirectoryMode = "0700";
|
||||||
RuntimeDirectoryPreserve = "yes";
|
RuntimeDirectoryPreserve = "yes";
|
||||||
UMask = "0377";
|
UMask = "0377";
|
||||||
LoadCredential = [ serverCaCredential ];
|
LoadCredential = [
|
||||||
|
certCredential
|
||||||
|
keyCredential
|
||||||
|
serverCaCredential
|
||||||
|
];
|
||||||
};
|
};
|
||||||
environment = {
|
environment = {
|
||||||
BAO_ADDR = cfg.addr;
|
BAO_ADDR = cfg.addr;
|
||||||
BAO_CLIENT_CERT = cfg.clientCertFile;
|
BAO_CLIENT_CERT = "%d/${certCredential}";
|
||||||
BAO_CLIENT_KEY = cfg.clientKeyFile;
|
BAO_CLIENT_KEY = "%d/${keyCredential}";
|
||||||
};
|
};
|
||||||
script = ''
|
script = ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# No identity delivered: ./bao.nix's check reports that; saying it
|
# No identity delivered: ./bao.nix's check reports that; saying it
|
||||||
# twice adds nothing.
|
# twice adds nothing.
|
||||||
for f in "$BAO_CLIENT_CERT" "$BAO_CLIENT_KEY"; do
|
for id in ${lib.escapeShellArg certCredential} ${lib.escapeShellArg keyCredential}; do
|
||||||
if [ ! -s "$f" ]; then
|
if [ ! -s "$CREDENTIALS_DIRECTORY/$id" ]; then
|
||||||
echo "this agent has no store identity, so it cannot fetch its own forge token." >&2
|
echo "this agent has no store identity, so it cannot fetch its own forge token." >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
if [ ! -r "$f" ]; then
|
if [ ! -r "$CREDENTIALS_DIRECTORY/$id" ]; then
|
||||||
echo "cannot read $f, so this agent cannot present its store identity." >&2
|
echo "cannot read $CREDENTIALS_DIRECTORY/$id, so this agent cannot present its store identity." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ let
|
||||||
agentName = config.services.hyperhive.agent.user.name;
|
agentName = config.services.hyperhive.agent.user.name;
|
||||||
stateDir = "/agents/${agentName}/state";
|
stateDir = "/agents/${agentName}/state";
|
||||||
|
|
||||||
# The server CA id ./bao.nix loads.
|
# The same three ids ./bao.nix and ./forge-accounts.nix load.
|
||||||
|
certCredential = "hive-agent-bao-cert";
|
||||||
|
keyCredential = "hive-agent-bao-key";
|
||||||
serverCaCredential = "hive-agent-bao-server-ca";
|
serverCaCredential = "hive-agent-bao-server-ca";
|
||||||
|
|
||||||
unitName = "hive-agent-github-token";
|
unitName = "hive-agent-github-token";
|
||||||
|
|
@ -74,19 +76,23 @@ in
|
||||||
RuntimeDirectoryMode = "0700";
|
RuntimeDirectoryMode = "0700";
|
||||||
# `0600`, the mode `github-token` has.
|
# `0600`, the mode `github-token` has.
|
||||||
UMask = "0077";
|
UMask = "0077";
|
||||||
LoadCredential = [ serverCaCredential ];
|
LoadCredential = [
|
||||||
|
certCredential
|
||||||
|
keyCredential
|
||||||
|
serverCaCredential
|
||||||
|
];
|
||||||
};
|
};
|
||||||
environment = {
|
environment = {
|
||||||
BAO_ADDR = cfg.addr;
|
BAO_ADDR = cfg.addr;
|
||||||
BAO_CLIENT_CERT = cfg.clientCertFile;
|
BAO_CLIENT_CERT = "%d/${certCredential}";
|
||||||
BAO_CLIENT_KEY = cfg.clientKeyFile;
|
BAO_CLIENT_KEY = "%d/${keyCredential}";
|
||||||
};
|
};
|
||||||
script = ''
|
script = ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# No identity delivered: ./bao.nix's check reports that.
|
# No identity delivered: ./bao.nix's check reports that.
|
||||||
for f in "$BAO_CLIENT_CERT" "$BAO_CLIENT_KEY"; do
|
for id in ${lib.escapeShellArg certCredential} ${lib.escapeShellArg keyCredential}; do
|
||||||
if [ ! -s "$f" ]; then
|
if [ ! -s "$CREDENTIALS_DIRECTORY/$id" ]; then
|
||||||
echo "this agent has no store identity, so it cannot fetch its GitHub token." >&2
|
echo "this agent has no store identity, so it cannot fetch its GitHub token." >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,12 @@ let
|
||||||
accounts = config.services.hyperhive.agent.matrixAccounts;
|
accounts = config.services.hyperhive.agent.matrixAccounts;
|
||||||
# This agent's own identity at the swarm secret store (./bao.nix). The daemon
|
# This agent's own identity at the swarm secret store (./bao.nix). The daemon
|
||||||
# reads each account's token from the store itself, `main` included (the
|
# reads each account's token from the store itself, `main` included (the
|
||||||
# swarm mints that one), so the store's coordinates belong on its unit: the
|
# swarm mints that one), so the store's coordinates belong on its unit. The
|
||||||
# live certificate pair ./bao.nix keeps, and the server CA id it loads.
|
# same three credential ids ./bao.nix and ./forge-token.nix load.
|
||||||
baoCfg = config.services.hyperhive.agent.bao;
|
baoCfg = config.services.hyperhive.agent.bao;
|
||||||
storeConfigured = baoCfg.addr != null;
|
storeConfigured = baoCfg.addr != null;
|
||||||
|
certCredential = "hive-agent-bao-cert";
|
||||||
|
keyCredential = "hive-agent-bao-key";
|
||||||
serverCaCredential = "hive-agent-bao-server-ca";
|
serverCaCredential = "hive-agent-bao-server-ca";
|
||||||
# **The enable signal.** Matrix is on for this agent exactly when it has at
|
# **The enable signal.** Matrix is on for this agent exactly when it has at
|
||||||
# least one account, because an account is the only thing the daemon has to
|
# least one account, because an account is the only thing the daemon has to
|
||||||
|
|
@ -306,11 +308,7 @@ in
|
||||||
description = "long-running matrix-sdk Client + MCP daemon";
|
description = "long-running matrix-sdk Client + MCP daemon";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
before = [ "hive-agent.service" ];
|
before = [ "hive-agent.service" ];
|
||||||
# The live certificate pair exists once ./bao.nix's unit has run.
|
after = [ "network-online.target" ];
|
||||||
after = [
|
|
||||||
"network-online.target"
|
|
||||||
]
|
|
||||||
++ lib.optional storeConfigured "hive-agent-bao-identity.service";
|
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
environment = {
|
environment = {
|
||||||
# In-agent todo socket the harness serves (loose-ends v2): the
|
# In-agent todo socket the harness serves (loose-ends v2): the
|
||||||
|
|
@ -330,8 +328,8 @@ in
|
||||||
// lib.optionalAttrs storeConfigured {
|
// lib.optionalAttrs storeConfigured {
|
||||||
HIVE_AGENT_NAME = userName;
|
HIVE_AGENT_NAME = userName;
|
||||||
BAO_ADDR = baoCfg.addr;
|
BAO_ADDR = baoCfg.addr;
|
||||||
BAO_CLIENT_CERT = baoCfg.clientCertFile;
|
BAO_CLIENT_CERT = "%d/${certCredential}";
|
||||||
BAO_CLIENT_KEY = baoCfg.clientKeyFile;
|
BAO_CLIENT_KEY = "%d/${keyCredential}";
|
||||||
# Named even when no CA was delivered: a bare `LoadCredential=` is
|
# Named even when no CA was delivered: a bare `LoadCredential=` is
|
||||||
# non-fatal when absent, and the daemon treats a missing or empty file
|
# non-fatal when absent, and the daemon treats a missing or empty file
|
||||||
# as "use the container's own trust store".
|
# as "use the container's own trust store".
|
||||||
|
|
@ -402,10 +400,14 @@ in
|
||||||
User = userName;
|
User = userName;
|
||||||
Group = userName;
|
Group = userName;
|
||||||
}
|
}
|
||||||
# The store's server CA, imported by id so systemd materialises it under
|
# This agent's own certificate, imported by id so systemd materialises it
|
||||||
# this unit's `User=` — the same bare form ./forge-token.nix uses.
|
# under this unit's `User=` — the same bare form ./forge-token.nix uses.
|
||||||
// lib.optionalAttrs storeConfigured {
|
// lib.optionalAttrs storeConfigured {
|
||||||
LoadCredential = [ serverCaCredential ];
|
LoadCredential = [
|
||||||
|
certCredential
|
||||||
|
keyCredential
|
||||||
|
serverCaCredential
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -381,8 +381,6 @@ in
|
||||||
description = "subagent task runner + MCP daemon for hive-subagent";
|
description = "subagent task runner + MCP daemon for hive-subagent";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
before = [ "hive-agent.service" ];
|
before = [ "hive-agent.service" ];
|
||||||
# The live certificate pair exists once ./bao.nix's unit has run.
|
|
||||||
after = lib.optional subagentReadsStore "hive-agent-bao-identity.service";
|
|
||||||
# A subagent task runs its own nested `claude` invocation (via
|
# A subagent task runs its own nested `claude` invocation (via
|
||||||
# `hive-claude`), which needs to resolve `claude` itself off PATH —
|
# `hive-claude`), which needs to resolve `claude` itself off PATH —
|
||||||
# same reasoning as `hive-bash-daemon`'s `path` above, even though
|
# same reasoning as `hive-bash-daemon`'s `path` above, even though
|
||||||
|
|
@ -444,8 +442,8 @@ in
|
||||||
# Paths and a name only. `%d` is this unit's own credentials directory.
|
# Paths and a name only. `%d` is this unit's own credentials directory.
|
||||||
HIVE_AGENT_NAME = userName;
|
HIVE_AGENT_NAME = userName;
|
||||||
BAO_ADDR = config.services.hyperhive.agent.bao.addr;
|
BAO_ADDR = config.services.hyperhive.agent.bao.addr;
|
||||||
BAO_CLIENT_CERT = config.services.hyperhive.agent.bao.clientCertFile;
|
BAO_CLIENT_CERT = "%d/hive-agent-bao-cert";
|
||||||
BAO_CLIENT_KEY = config.services.hyperhive.agent.bao.clientKeyFile;
|
BAO_CLIENT_KEY = "%d/hive-agent-bao-key";
|
||||||
BAO_CACERT = "%d/hive-agent-bao-server-ca";
|
BAO_CACERT = "%d/hive-agent-bao-server-ca";
|
||||||
};
|
};
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
|
@ -483,9 +481,13 @@ in
|
||||||
MemoryHigh = toString subagentMemoryHigh;
|
MemoryHigh = toString subagentMemoryHigh;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs subagentReadsStore {
|
// lib.optionalAttrs subagentReadsStore {
|
||||||
# Bare id: inherits the credential the container manager passed in,
|
# Bare ids: inherits the credentials the container manager passed in,
|
||||||
# the same form ./queue-identity.nix uses.
|
# the same form ./queue-identity.nix uses.
|
||||||
LoadCredential = [ "hive-agent-bao-server-ca" ];
|
LoadCredential = [
|
||||||
|
"hive-agent-bao-cert"
|
||||||
|
"hive-agent-bao-key"
|
||||||
|
"hive-agent-bao-server-ca"
|
||||||
|
];
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
lib.optionalAttrs
|
lib.optionalAttrs
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
# before its first queue connection and again on every reconnect attempt
|
# before its first queue connection and again on every reconnect attempt
|
||||||
# (`hive-agent`'s `swarm_queue`), so a re-minted secret is picked up without a
|
# (`hive-agent`'s `swarm_queue`), so a re-minted secret is picked up without a
|
||||||
# restart. What this module hands the harness is the store's address and the
|
# restart. What this module hands the harness is the store's address and the
|
||||||
# paths of the live certificate pair ./bao.nix keeps.
|
# paths of its certificate.
|
||||||
#
|
#
|
||||||
# ⛔ The store certificate is for reaching the store and nothing else. It is
|
# ⛔ The store certificate is for reaching the store and nothing else. It is
|
||||||
# never presented to the queue: what goes to the queue is the secret read
|
# never presented to the queue: what goes to the queue is the secret read
|
||||||
|
|
@ -37,9 +37,11 @@ let
|
||||||
# role from it.
|
# role from it.
|
||||||
agentName = config.services.hyperhive.agent.user.name;
|
agentName = config.services.hyperhive.agent.user.name;
|
||||||
|
|
||||||
# The server CA id ./bao.nix loads; the id is
|
# The same three ids ./bao.nix loads, because there is one store identity
|
||||||
# `hive_c0re::lifecycle::agent_identity`'s and a rename is a rename there
|
# per agent; the ids are `hive_c0re::lifecycle::agent_identity`'s and a
|
||||||
# too.
|
# rename is a rename there too.
|
||||||
|
certCredential = "hive-agent-bao-cert";
|
||||||
|
keyCredential = "hive-agent-bao-key";
|
||||||
serverCaCredential = "hive-agent-bao-server-ca";
|
serverCaCredential = "hive-agent-bao-server-ca";
|
||||||
|
|
||||||
# The store's address is the whole switch, exactly as in ./bao.nix — and
|
# The store's address is the whole switch, exactly as in ./bao.nix — and
|
||||||
|
|
@ -52,18 +54,20 @@ in
|
||||||
{
|
{
|
||||||
config = lib.mkIf configured {
|
config = lib.mkIf configured {
|
||||||
systemd.services.hive-agent = {
|
systemd.services.hive-agent = {
|
||||||
# A bare id, no path: the terse `LoadCredential=` form that inherits a
|
# Bare ids, no paths: the terse `LoadCredential=` form that inherits a
|
||||||
# credential the service *manager* received, which is what the container
|
# credential the service *manager* received, which is what the container
|
||||||
# manager passed in. ./bao.nix and ./queue.nix state the same shape.
|
# manager passed in. ./bao.nix and ./queue.nix state the same shape.
|
||||||
serviceConfig.LoadCredential = [ serverCaCredential ];
|
serviceConfig.LoadCredential = [
|
||||||
# The live certificate pair exists once ./bao.nix's unit has run.
|
certCredential
|
||||||
after = [ "hive-agent-bao-identity.service" ];
|
keyCredential
|
||||||
|
serverCaCredential
|
||||||
|
];
|
||||||
# Paths and a name only. `%d` is this unit's own credentials directory.
|
# Paths and a name only. `%d` is this unit's own credentials directory.
|
||||||
environment = {
|
environment = {
|
||||||
HIVE_AGENT_NAME = agentName;
|
HIVE_AGENT_NAME = agentName;
|
||||||
BAO_ADDR = cfg.addr;
|
BAO_ADDR = cfg.addr;
|
||||||
BAO_CLIENT_CERT = cfg.clientCertFile;
|
BAO_CLIENT_CERT = "%d/${certCredential}";
|
||||||
BAO_CLIENT_KEY = cfg.clientKeyFile;
|
BAO_CLIENT_KEY = "%d/${keyCredential}";
|
||||||
# Named even when no CA was delivered: a bare `LoadCredential=` is
|
# Named even when no CA was delivered: a bare `LoadCredential=` is
|
||||||
# non-fatal when absent, and the harness treats a missing or empty file
|
# non-fatal when absent, and the harness treats a missing or empty file
|
||||||
# as "use the container's own trust store".
|
# as "use the container's own trust store".
|
||||||
|
|
|
||||||
|
|
@ -75,17 +75,19 @@ let
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# One store identity per agent: the fetch presents the same certificate
|
# One store identity per agent: the fetch presents the same certificate
|
||||||
# the identity check keeps current. Every `BAO_*` value is an address or
|
# the identity check proves. Every `BAO_*` value is an address or a
|
||||||
# a path, never a value.
|
# `%d/` path, never a value.
|
||||||
name = "the fetch presents the agent's own store identity, by path";
|
name = "the fetch presents the agent's own store identity, by path";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
u = fetchUnit agentForgeBao;
|
u = fetchUnit agentForgeBao;
|
||||||
e = u.environment;
|
e = u.environment;
|
||||||
in
|
in
|
||||||
e.BAO_ADDR == baoAddr
|
builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
|
||||||
&& e.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
|
&& builtins.elem "hive-agent-bao-key" u.serviceConfig.LoadCredential
|
||||||
&& e.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key";
|
&& e.BAO_ADDR == baoAddr
|
||||||
|
&& e.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
||||||
|
&& e.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The field is the secret. It goes to a file by redirect and is never
|
# The field is the secret. It goes to a file by redirect and is never
|
||||||
|
|
@ -199,7 +201,8 @@ let
|
||||||
in
|
in
|
||||||
u.serviceConfig.User == name
|
u.serviceConfig.User == name
|
||||||
&& u.serviceConfig.UMask == "0077"
|
&& u.serviceConfig.UMask == "0077"
|
||||||
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert";
|
&& builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
|
||||||
|
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# Files a hive wrote keep working until the operator re-links them.
|
# Files a hive wrote keep working until the operator re-links them.
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,11 @@ let
|
||||||
in
|
in
|
||||||
u.serviceConfig.User == name
|
u.serviceConfig.User == name
|
||||||
&& u.serviceConfig.UMask == "0077"
|
&& u.serviceConfig.UMask == "0077"
|
||||||
|
&& builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
|
||||||
|
&& builtins.elem "hive-agent-bao-key" u.serviceConfig.LoadCredential
|
||||||
&& u.environment.BAO_ADDR == baoAddr
|
&& u.environment.BAO_ADDR == baoAddr
|
||||||
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
|
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
||||||
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key";
|
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# A file a hive wrote keeps working until the operator links a token in
|
# A file a hive wrote keeps working until the operator links a token in
|
||||||
|
|
|
||||||
|
|
@ -116,17 +116,20 @@ let
|
||||||
# The daemon reads this agent's tokens from the store ITSELF, as itself,
|
# The daemon reads this agent's tokens from the store ITSELF, as itself,
|
||||||
# from inside this container — `main` too, since the swarm mints it and
|
# from inside this container — `main` too, since the swarm mints it and
|
||||||
# no hive does. So it needs the same identity ./agent-forge-bao.nix's
|
# no hive does. So it needs the same identity ./agent-forge-bao.nix's
|
||||||
# fetch carries: the live pair, and the server CA from its credentials.
|
# fetch carries, in its own credentials directory.
|
||||||
name = "the matrix daemon carries this agent's own store identity";
|
name = "the matrix daemon carries this agent's own store identity";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
u = daemon agentMatrixBao;
|
u = daemon agentMatrixBao;
|
||||||
in
|
in
|
||||||
u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ]
|
u.serviceConfig.LoadCredential == [
|
||||||
|
"hive-agent-bao-cert"
|
||||||
|
"hive-agent-bao-key"
|
||||||
|
"hive-agent-bao-server-ca"
|
||||||
|
]
|
||||||
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
|
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
|
||||||
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
|
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
||||||
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key"
|
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
|
||||||
&& builtins.elem "hive-agent-bao-identity.service" u.after;
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The agent's name, and nothing derived from it: the daemon builds its
|
# The agent's name, and nothing derived from it: the daemon builds its
|
||||||
|
|
@ -140,24 +143,16 @@ let
|
||||||
&& !(lib.any (lib.hasInfix "swarm/agents") (lib.attrValues e));
|
&& !(lib.any (lib.hasInfix "swarm/agents") (lib.attrValues e));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# 🩸 A secret is a path: every `BAO_*` entry is the store's address, a
|
# 🩸 A secret is a path: every `BAO_*` entry is the store's address or a
|
||||||
# file under this unit's own credentials directory, or the one file of
|
# file under this unit's own credentials directory, never bytes in an
|
||||||
# the live pair ./bao.nix keeps that its name stands for — never bytes
|
# environment `/proc/<pid>/environ` publishes.
|
||||||
# in an environment `/proc/<pid>/environ` publishes. The live pair is
|
|
||||||
# spelled out rather than read from the option, so a default that
|
|
||||||
# turned into a value would fail here too.
|
|
||||||
name = "the matrix daemon is handed store paths and never store values";
|
name = "the matrix daemon is handed store paths and never store values";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
store = lib.filterAttrs (n: _: lib.hasPrefix "BAO_" n) (daemon agentMatrixBao).environment;
|
store = lib.filterAttrs (n: _: lib.hasPrefix "BAO_" n) (daemon agentMatrixBao).environment;
|
||||||
livePair = {
|
|
||||||
BAO_CLIENT_CERT = "/run/hive-agent-bao-identity/current/cert";
|
|
||||||
BAO_CLIENT_KEY = "/run/hive-agent-bao-identity/current/key";
|
|
||||||
};
|
|
||||||
isPath =
|
|
||||||
n: n == "BAO_ADDR" || lib.hasPrefix "%d/" store.${n} || (livePair.${n} or null) == store.${n};
|
|
||||||
in
|
in
|
||||||
store != { } && lib.all isPath (lib.attrNames store);
|
store != { }
|
||||||
|
&& lib.all (n: n == "BAO_ADDR" || lib.hasPrefix "%d/" store.${n}) (lib.attrNames store);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# A token the swarm mints or replaces in the store changes no file, so
|
# A token the swarm mints or replaces in the store changes no file, so
|
||||||
|
|
|
||||||
|
|
@ -87,30 +87,20 @@ let
|
||||||
&& builtins.elem "hive-agent-bao-server-ca" c;
|
&& builtins.elem "hive-agent-bao-server-ca" c;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The live pair sits in the unit's own runtime directory, which outlives
|
# `%d` and not a path under the agent's state dir: the host file is
|
||||||
# each run so the timer's next run logs in with what the last one
|
# `0600` to the hive daemon, so the only copy this unprivileged unit can
|
||||||
# wrote. The address is the option's value rather than a literal that
|
# open is the one systemd puts in its own credentials directory. The
|
||||||
# agrees with it today.
|
# address is the option's value rather than a literal that agrees with
|
||||||
name = "the identity check presents the live pair it keeps in its runtime directory";
|
# it today.
|
||||||
|
name = "the identity check presents its certificate out of the credentials directory";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
u = agentBaoIdentity agentBao;
|
e = (agentBaoIdentity agentBao).environment;
|
||||||
e = u.environment;
|
|
||||||
in
|
in
|
||||||
e.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
|
e.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
||||||
&& e.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key"
|
&& e.BAO_CLIENT_KEY == "%d/hive-agent-bao-key"
|
||||||
&& u.serviceConfig.RuntimeDirectory == "hive-agent-bao-identity"
|
|
||||||
&& u.serviceConfig.RuntimeDirectoryPreserve == "yes"
|
|
||||||
&& e.BAO_ADDR == agentBao.services.hyperhive.agent.bao.addr;
|
&& e.BAO_ADDR == agentBao.services.hyperhive.agent.bao.addr;
|
||||||
}
|
}
|
||||||
{
|
|
||||||
# A leaf the controller re-issues reaches a running container only
|
|
||||||
# through this re-run, and an active unit cannot be started again.
|
|
||||||
name = "the identity check re-runs on a timer";
|
|
||||||
ok =
|
|
||||||
(agentBaoIdentity agentBao).serviceConfig.RemainAfterExit == false
|
|
||||||
&& agentBao.systemd.timers.hive-agent-bao-identity.timerConfig.OnUnitInactiveSec == "10min";
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
# Same 403-not-a-miss reason as the hive-side readers: the path
|
# Same 403-not-a-miss reason as the hive-side readers: the path
|
||||||
# `swarm_secret_client::mtls::identity_path` builds is the one this
|
# `swarm_secret_client::mtls::identity_path` builds is the one this
|
||||||
|
|
@ -143,22 +133,22 @@ let
|
||||||
{
|
{
|
||||||
# Nothing about the identity may be printed, and the read-back is where
|
# Nothing about the identity may be printed, and the read-back is where
|
||||||
# that could slip: `bao kv get` on this path answers with certificate
|
# that could slip: `bao kv get` on this path answers with certificate
|
||||||
# material and a private key. The answer goes to a file in the unit's
|
# material, and the object beside it is a private key. The check needs
|
||||||
# own `0700` directory.
|
# only whether the read succeeded.
|
||||||
#
|
#
|
||||||
# The path goes through `lib.escapeShellArg` here for the same reason the
|
# The path goes through `lib.escapeShellArg` here for the same reason the
|
||||||
# module passes it through one — that helper decides whether an argument
|
# module passes it through one — that helper decides whether an argument
|
||||||
# needs quotes at all, and this one (only `[a-z0-9/-]`) comes back bare.
|
# needs quotes at all, and this one (only `[a-z0-9/-]`) comes back bare.
|
||||||
# Spelling the quotes in by hand asserts a rendering nixpkgs chooses
|
# Spelling the quotes in by hand asserts a rendering nixpkgs chooses
|
||||||
# rather than the redirect this property is about.
|
# rather than the redirect this property is about.
|
||||||
name = "the identity check writes what it reads back to a file";
|
name = "the identity check discards what it reads back";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
m = agentBao;
|
m = agentBao;
|
||||||
name = m.services.hyperhive.agent.user.name;
|
name = m.services.hyperhive.agent.user.name;
|
||||||
arg = lib.escapeShellArg "secret/swarm/agents/${name}/bao-mtls";
|
arg = lib.escapeShellArg "secret/swarm/agents/${name}/bao-mtls";
|
||||||
in
|
in
|
||||||
lib.hasInfix "bao kv get -format=json ${arg} >\"$raw\"" (agentBaoIdentity m).script;
|
lib.hasInfix "bao kv get -field=cert ${arg} >/dev/null" (agentBaoIdentity m).script;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The absence arm, and what makes the four above able to fail. An agent
|
# The absence arm, and what makes the four above able to fail. An agent
|
||||||
|
|
@ -170,16 +160,17 @@ let
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The harness reads its per-agent queue secret from the store itself,
|
# The harness reads its per-agent queue secret from the store itself,
|
||||||
# under the same store identity the check above keeps current, because
|
# under the same store identity the check above proves, because there is
|
||||||
# there is one identity per agent.
|
# one identity per agent.
|
||||||
name = "the harness presents the agent's own store identity";
|
name = "the harness presents the agent's own store identity";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
u = agentHarness agentBao;
|
u = agentHarness agentBao;
|
||||||
in
|
in
|
||||||
u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
|
builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential
|
||||||
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key"
|
&& builtins.elem "hive-agent-bao-key" u.serviceConfig.LoadCredential
|
||||||
&& builtins.elem "hive-agent-bao-identity.service" u.after
|
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
||||||
|
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key"
|
||||||
&& u.environment.BAO_ADDR == agentBao.services.hyperhive.agent.bao.addr;
|
&& u.environment.BAO_ADDR == agentBao.services.hyperhive.agent.bao.addr;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
@ -209,7 +200,7 @@ let
|
||||||
in
|
in
|
||||||
!(u.environment ? BAO_ADDR)
|
!(u.environment ? BAO_ADDR)
|
||||||
&& !(u.environment ? HIVE_AGENT_NAME)
|
&& !(u.environment ? HIVE_AGENT_NAME)
|
||||||
&& !(builtins.elem "hive-agent-bao-server-ca" (u.serviceConfig.LoadCredential or [ ]));
|
&& !(builtins.elem "hive-agent-bao-cert" (u.serviceConfig.LoadCredential or [ ]));
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
|
|
|
||||||
|
|
@ -113,11 +113,15 @@ let
|
||||||
lib.all
|
lib.all
|
||||||
(
|
(
|
||||||
u:
|
u:
|
||||||
u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ]
|
u.serviceConfig.LoadCredential == [
|
||||||
|
"hive-agent-bao-cert"
|
||||||
|
"hive-agent-bao-key"
|
||||||
|
"hive-agent-bao-server-ca"
|
||||||
|
]
|
||||||
&& u.environment.HIVE_AGENT_NAME == "a1"
|
&& u.environment.HIVE_AGENT_NAME == "a1"
|
||||||
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
|
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
|
||||||
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
|
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
||||||
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key"
|
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key"
|
||||||
)
|
)
|
||||||
[
|
[
|
||||||
(subagent claudeBao)
|
(subagent claudeBao)
|
||||||
|
|
@ -131,11 +135,15 @@ let
|
||||||
let
|
let
|
||||||
u = subagent opencodeBao;
|
u = subagent opencodeBao;
|
||||||
in
|
in
|
||||||
u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ]
|
u.serviceConfig.LoadCredential == [
|
||||||
|
"hive-agent-bao-cert"
|
||||||
|
"hive-agent-bao-key"
|
||||||
|
"hive-agent-bao-server-ca"
|
||||||
|
]
|
||||||
&& u.environment.HIVE_AGENT_NAME == "a1"
|
&& u.environment.HIVE_AGENT_NAME == "a1"
|
||||||
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
|
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
|
||||||
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
|
&& u.environment.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert"
|
||||||
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key";
|
&& u.environment.BAO_CLIENT_KEY == "%d/hive-agent-bao-key";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "an opencode agent with no store hands its subagent daemon no store identity";
|
name = "an opencode agent with no store hands its subagent daemon no store identity";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
# `checks.script-test-agent-bao-fetch` — runs the agent units that log in to
|
# `checks.script-test-agent-bao-fetch` — runs the agent unit that logs in to
|
||||||
# the swarm secret store and fetch a secret, ../agent-modules/forge-token.nix
|
# the swarm secret store and fetches a secret, ../agent-modules/forge-token.nix,
|
||||||
# and ../agent-modules/bao.nix, against a stub `bao`. The cases are in
|
# against a stub `bao`. The cases are in ./agent-bao-fetch.sh.
|
||||||
# ./agent-bao-fetch.sh.
|
|
||||||
#
|
#
|
||||||
# What runs is the unit's rendered `ExecStart`, on the unit's own `PATH` with
|
# What runs is the unit's rendered `ExecStart`, on the unit's own `PATH` with
|
||||||
# the stub in front. The one edit is the unit's `/run/<unit>/` prefix, moved
|
# the stub in front. The one edit is the unit's `/run/<unit>/` prefix, moved
|
||||||
|
|
@ -43,16 +42,11 @@ let
|
||||||
|
|
||||||
# Answers `login` and `kv get` from `FAKE_BAO_*` variables and logs every
|
# Answers `login` and `kv get` from `FAKE_BAO_*` variables and logs every
|
||||||
# call. A `kv` call without the token `login` printed fails, so a script that
|
# call. A `kv` call without the token `login` printed fails, so a script that
|
||||||
# drops `BAO_TOKEN` between the two cannot pass. A login presenting the
|
# drops `BAO_TOKEN` between the two cannot pass.
|
||||||
# certificate `FAKE_BAO_REFUSE_CERT` is refused.
|
|
||||||
fakeBao = pkgs.writeShellScriptBin "bao" ''
|
fakeBao = pkgs.writeShellScriptBin "bao" ''
|
||||||
echo "$*" >> "$FAKE_BAO_LOG"
|
echo "$*" >> "$FAKE_BAO_LOG"
|
||||||
case "$1" in
|
case "$1" in
|
||||||
login)
|
login)
|
||||||
if [ -n "''${FAKE_BAO_REFUSE_CERT-}" ] && [ "$(<"$BAO_CLIENT_CERT")" = "$FAKE_BAO_REFUSE_CERT" ]; then
|
|
||||||
echo "fake bao: certificate refused" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
printf '%s' "$FAKE_BAO_LOGIN_ERR" >&2
|
printf '%s' "$FAKE_BAO_LOGIN_ERR" >&2
|
||||||
printf '%s' "$FAKE_BAO_LOGIN_OUT"
|
printf '%s' "$FAKE_BAO_LOGIN_OUT"
|
||||||
exit "$FAKE_BAO_LOGIN_RC"
|
exit "$FAKE_BAO_LOGIN_RC"
|
||||||
|
|
@ -76,7 +70,6 @@ in
|
||||||
pkgs.runCommand "hyperhive-script-test-agent-bao-fetch"
|
pkgs.runCommand "hyperhive-script-test-agent-bao-fetch"
|
||||||
(
|
(
|
||||||
unitEnv "FORGE" "hive-agent-forge-token"
|
unitEnv "FORGE" "hive-agent-forge-token"
|
||||||
// unitEnv "IDENT" "hive-agent-bao-identity"
|
|
||||||
// {
|
// {
|
||||||
FAKE_BAO_BIN = "${fakeBao}/bin";
|
FAKE_BAO_BIN = "${fakeBao}/bin";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,17 +36,8 @@ setup() {
|
||||||
log=$dir/bao.log
|
log=$dir/bao.log
|
||||||
mkdir -m 0700 "$rt"
|
mkdir -m 0700 "$rt"
|
||||||
mkdir "$creds"
|
mkdir "$creds"
|
||||||
# Per-case stubs that shadow the unit's own tools; empty unless a case adds one.
|
|
||||||
mkdir "$dir/bin"
|
|
||||||
printf cert >"$creds/hive-agent-bao-cert"
|
printf cert >"$creds/hive-agent-bao-cert"
|
||||||
printf key >"$creds/hive-agent-bao-key"
|
printf key >"$creds/hive-agent-bao-key"
|
||||||
# The identity pair the unit presents: the delivered one for the fetch, the
|
|
||||||
# live copy it keeps for the identity unit.
|
|
||||||
if [ "$prefix" = IDENT ]; then
|
|
||||||
client_cert=$rt/current/cert client_key=$rt/current/key
|
|
||||||
else
|
|
||||||
client_cert=$creds/hive-agent-bao-cert client_key=$creds/hive-agent-bao-key
|
|
||||||
fi
|
|
||||||
: >"$log"
|
: >"$log"
|
||||||
if ! sed "s|/run/$unit/|$rt/|g" "${!script_var}" >"$dir/script"; then
|
if ! sed "s|/run/$unit/|$rt/|g" "${!script_var}" >"$dir/script"; then
|
||||||
echo "cannot read the rendered script for $unit" >&2
|
echo "cannot read the rendered script for $unit" >&2
|
||||||
|
|
@ -62,7 +53,6 @@ setup() {
|
||||||
kv_rc=0
|
kv_rc=0
|
||||||
kv_out=the-secret
|
kv_out=the-secret
|
||||||
kv_err=
|
kv_err=
|
||||||
refuse_cert=
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go() {
|
go() {
|
||||||
|
|
@ -71,10 +61,8 @@ go() {
|
||||||
umask 0377
|
umask 0377
|
||||||
cd "$dir" || exit 99
|
cd "$dir" || exit 99
|
||||||
exec env -i \
|
exec env -i \
|
||||||
PATH="$dir/bin:$FAKE_BAO_BIN:$unit_path" \
|
PATH="$FAKE_BAO_BIN:$unit_path" \
|
||||||
BAO_ADDR="$bao_addr" \
|
BAO_ADDR="$bao_addr" \
|
||||||
BAO_CLIENT_CERT="$client_cert" \
|
|
||||||
BAO_CLIENT_KEY="$client_key" \
|
|
||||||
CREDENTIALS_DIRECTORY="$creds" \
|
CREDENTIALS_DIRECTORY="$creds" \
|
||||||
FAKE_BAO_LOG="$log" \
|
FAKE_BAO_LOG="$log" \
|
||||||
FAKE_BAO_LOGIN_RC="$login_rc" \
|
FAKE_BAO_LOGIN_RC="$login_rc" \
|
||||||
|
|
@ -83,7 +71,6 @@ go() {
|
||||||
FAKE_BAO_KV_RC="$kv_rc" \
|
FAKE_BAO_KV_RC="$kv_rc" \
|
||||||
FAKE_BAO_KV_OUT="$kv_out" \
|
FAKE_BAO_KV_OUT="$kv_out" \
|
||||||
FAKE_BAO_KV_ERR="$kv_err" \
|
FAKE_BAO_KV_ERR="$kv_err" \
|
||||||
FAKE_BAO_REFUSE_CERT="$refuse_cert" \
|
|
||||||
"$dir/script"
|
"$dir/script"
|
||||||
) >"$dir/out" 2>"$dir/err"
|
) >"$dir/out" 2>"$dir/err"
|
||||||
rc=$?
|
rc=$?
|
||||||
|
|
@ -247,141 +234,6 @@ expect_out "fetched this agent's forge token"
|
||||||
expect_file "$rt/token" the-secret
|
expect_file "$rt/token" the-secret
|
||||||
expect_no_file "$rt/token.new"
|
expect_no_file "$rt/token.new"
|
||||||
|
|
||||||
ident_path=secret/swarm/agents/a1/bao-mtls
|
|
||||||
ident_kv_call="kv get -format=json $ident_path"
|
|
||||||
leaf() { printf '{"data":{"data":{"cert":"%s","key":"%s"}}}' "$1" "$2"; }
|
|
||||||
|
|
||||||
expect_pair() {
|
|
||||||
expect_file "$rt/current/cert" "$1"
|
|
||||||
expect_file "$rt/current/key" "$2"
|
|
||||||
}
|
|
||||||
|
|
||||||
expect_pairs() {
|
|
||||||
local d n=0
|
|
||||||
for d in "$rt"/pair.*; do
|
|
||||||
if [ -d "$d" ]; then n=$((n + 1)); fi
|
|
||||||
done
|
|
||||||
if [ "$n" != "$1" ]; then fail "$n pair directories left, expected $1"; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# live_pair <name> <cert> <key> — a pair an earlier run made live.
|
|
||||||
live_pair() {
|
|
||||||
mkdir -m 0700 "$rt/pair.$1"
|
|
||||||
printf '%s' "$2" >"$rt/pair.$1/cert"
|
|
||||||
printf '%s' "$3" >"$rt/pair.$1/key"
|
|
||||||
chmod 0400 "$rt/pair.$1/cert" "$rt/pair.$1/key"
|
|
||||||
ln -sfn "pair.$1" "$rt/current"
|
|
||||||
}
|
|
||||||
|
|
||||||
# stop_at <n> — the n-th `ln` or `mv` the script runs fails, which leaves
|
|
||||||
# things as a run killed just before that step would.
|
|
||||||
stop_at() {
|
|
||||||
local tool real
|
|
||||||
mkdir "$dir/steps"
|
|
||||||
for tool in ln mv; do
|
|
||||||
real=$(PATH=$unit_path command -v "$tool")
|
|
||||||
cat >"$dir/bin/$tool" <<STUB
|
|
||||||
#!$BASH
|
|
||||||
mktemp -p "$dir/steps" >/dev/null
|
|
||||||
steps=("$dir/steps"/*)
|
|
||||||
if [ "\${#steps[@]}" -ge "$1" ]; then exit 1; fi
|
|
||||||
exec "$real" "\$@"
|
|
||||||
STUB
|
|
||||||
chmod +x "$dir/bin/$tool"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
setup IDENT "no store identity delivered"
|
|
||||||
: >"$creds/hive-agent-bao-cert"
|
|
||||||
go
|
|
||||||
expect_rc 1
|
|
||||||
expect_err "has no store identity"
|
|
||||||
expect_calls
|
|
||||||
if [ -e "$rt/current" ]; then fail "a live pair was made from nothing"; fi
|
|
||||||
|
|
||||||
setup IDENT "a first run makes the delivered pair live"
|
|
||||||
kv_out=$(leaf cert key)
|
|
||||||
go
|
|
||||||
expect_rc 0
|
|
||||||
expect_out "is unchanged"
|
|
||||||
expect_calls "$login_call" "$ident_kv_call"
|
|
||||||
if [ ! -L "$rt/current" ]; then fail "$rt/current is not a symlink"; fi
|
|
||||||
expect_pair cert key
|
|
||||||
if [ "$(stat -L -c %a "$rt/current/key")" != 400 ]; then fail "key is mode $(stat -L -c %a "$rt/current/key"), expected 400"; fi
|
|
||||||
expect_pairs 1
|
|
||||||
expect_no_file "$rt/bao-mtls.json"
|
|
||||||
|
|
||||||
setup IDENT "the live pair outlives the delivered one"
|
|
||||||
live_pair old cert2 key2
|
|
||||||
kv_out=$(leaf cert2 key2)
|
|
||||||
go
|
|
||||||
expect_rc 0
|
|
||||||
expect_out "is unchanged"
|
|
||||||
expect_pair cert2 key2
|
|
||||||
expect_pairs 1
|
|
||||||
|
|
||||||
setup IDENT "a re-issued leaf goes live once a login with it succeeds"
|
|
||||||
kv_out=$(leaf cert2 key2)
|
|
||||||
go
|
|
||||||
expect_rc 0
|
|
||||||
expect_out "replaced this agent's store certificate"
|
|
||||||
expect_calls "$login_call" "$ident_kv_call" "$login_call"
|
|
||||||
expect_pair cert2 key2
|
|
||||||
# The replaced pair stays for a reader that resolved the link before the swap.
|
|
||||||
expect_pairs 2
|
|
||||||
go
|
|
||||||
expect_rc 0
|
|
||||||
expect_out "is unchanged"
|
|
||||||
expect_pairs 1
|
|
||||||
|
|
||||||
setup IDENT "a re-issued leaf the store refuses does not go live"
|
|
||||||
kv_out=$(leaf cert2 key2)
|
|
||||||
refuse_cert=cert2
|
|
||||||
go
|
|
||||||
expect_rc 1
|
|
||||||
expect_err "refused the certificate at $ident_path"
|
|
||||||
expect_pair cert key
|
|
||||||
expect_pairs 1
|
|
||||||
|
|
||||||
setup IDENT "an object without a key keeps the live pair"
|
|
||||||
kv_out='{"data":{"data":{"cert":"cert2"}}}'
|
|
||||||
go
|
|
||||||
expect_rc 1
|
|
||||||
expect_err "holds no certificate and key"
|
|
||||||
expect_pair cert key
|
|
||||||
expect_pairs 1
|
|
||||||
|
|
||||||
setup IDENT "what a stopped run left behind is removed"
|
|
||||||
live_pair old cert key
|
|
||||||
mkdir -m 0700 "$rt/pair.stale"
|
|
||||||
printf cert2 >"$rt/pair.stale/cert"
|
|
||||||
ln -s pair.stale "$rt/current.new"
|
|
||||||
kv_out=$(leaf cert key)
|
|
||||||
go
|
|
||||||
expect_rc 0
|
|
||||||
expect_pair cert key
|
|
||||||
expect_pairs 1
|
|
||||||
if [ -L "$rt/current.new" ]; then fail "current.new left behind"; fi
|
|
||||||
|
|
||||||
# The swap is atomic: wherever a run stops, the link names one whole pair,
|
|
||||||
# and the next run finishes the job.
|
|
||||||
for n in 1 2 3; do
|
|
||||||
setup IDENT "a swap stopped at its step $n leaves one whole pair live"
|
|
||||||
live_pair old cert key
|
|
||||||
kv_out=$(leaf cert2 key2)
|
|
||||||
stop_at "$n"
|
|
||||||
go
|
|
||||||
got="$(<"$rt/current/cert") $(<"$rt/current/key")"
|
|
||||||
case "$got" in
|
|
||||||
"cert key" | "cert2 key2") ;;
|
|
||||||
*) fail "the live pair is '$got', one file of each" ;;
|
|
||||||
esac
|
|
||||||
rm -f "$dir/bin/ln" "$dir/bin/mv"
|
|
||||||
go
|
|
||||||
expect_rc 0
|
|
||||||
expect_pair cert2 key2
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$failures" -ne 0 ]; then
|
if [ "$failures" -ne 0 ]; then
|
||||||
echo "script-test-agent-bao-fetch: $failures failed assertions over $count runs" >&2
|
echo "script-test-agent-bao-fetch: $failures failed assertions over $count runs" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@
|
||||||
//! the controller's own record, [`queue::AgentCredential::minted_at`]; a secret
|
//! the controller's own record, [`queue::AgentCredential::minted_at`]; a secret
|
||||||
//! without one is stamped, value unchanged ([`SecretStep::Backfill`]).
|
//! without one is stamped, value unchanged ([`SecretStep::Backfill`]).
|
||||||
//!
|
//!
|
||||||
//! A running agent re-reads its certificate every ten minutes under the one it
|
//! Either replacement reaches the agent at its next start, when the container
|
||||||
//! holds (`nix/agent-modules/bao.nix`); the old one stays valid until it
|
//! is handed the certificate and fetches the secret; nothing pulls either into
|
||||||
//! expires. A new secret reaches the agent at its next start, when the
|
//! a running container. The old certificate stays valid until it expires. The
|
||||||
//! container fetches it. The old secret stops working at once: the queue checks
|
//! old secret stops working at once: the queue checks it only at `CONNECT`, so
|
||||||
//! it only at `CONNECT`, so an open connection is unaffected, but a reconnect
|
//! an open connection is unaffected, but a reconnect before that restart
|
||||||
//! before that restart presents the old secret and is denied.
|
//! presents the old secret and is denied.
|
||||||
//!
|
//!
|
||||||
//! [`spawn`]'s pass, once the queue is up and every [`RECONCILE_INTERVAL`],
|
//! [`spawn`]'s pass, once the queue is up and every [`RECONCILE_INTERVAL`],
|
||||||
//! inserts a job per agent with anything due: `MintAgentIdentity` for the
|
//! inserts a job per agent with anything due: `MintAgentIdentity` for the
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,9 @@
|
||||||
//! container.
|
//! container.
|
||||||
//!
|
//!
|
||||||
//! 🔑 The recursion this shape looks like it has — *a credential fetched from
|
//! 🔑 The recursion this shape looks like it has — *a credential fetched from
|
||||||
//! the store that is what opens the store* — is broken by who reads it first.
|
//! the store that is what opens the store* — is broken by who reads it. The
|
||||||
//! The first reader is the hive, which already holds its own leaf, so nothing
|
//! reader is the hive, never the agent, and a hive already holds its own leaf.
|
||||||
//! has to already be an agent in order to obtain an agent's identity. After
|
//! Nothing has to already be an agent in order to obtain an agent's identity.
|
||||||
//! that the agent re-reads its own path under the leaf it holds, which picks
|
|
||||||
//! up a re-issued leaf only while the held one is still valid.
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue