Watch
0
0
Fork
You've already forked hyperhive
0

swarm-controller reads its forge token from the store, not a host file

The controller's forge token never entered bao: the forge minted it in its
container, hive-forge-swarm-controller-token copied it to a host file, and the
controller LoadCredential'ed that file. forgeTokenFile defaulted to that path
only where the forge ran, so a controller on any other host had no forge
access unless an operator copied the file by hand.

Now the forge's host publishes it and the controller reads it:

- swarm-bao-forge-publisher (replaces hive-forge-swarm-controller-token)
  waits for the in-container token, logs in with its own forge-publisher leaf
  and writes it to swarm/controller/swarm-controller/forge/token. It runs on
  every forge container start, the old unit's trigger.
- New principal swarm-forge-publisher: leaf minted by glue-bao-tls.nix,
  policy create/update on that one path, written by
  swarm-bao-forge-publisher-policy, CN reserved as a hive name.
- The controller policy gains read on that path. swarm-controller reads it
  at start under its own certificate with the OIDC secret's retry, and runs
  without forge access if it still cannot.
- forgeTokenFile, its LoadCredential and the forgeEnv gate are gone;
  SWARM_CONTROLLER_FORGE_URL is always set. hostSwarmControllerTokenFile is
  removed, and a tmpfiles rule deletes the old host copy on the forge host.

The in-container file stays: the mint unit's only-once guard reads it, and
it is the publisher's source.

Refs #4998
This commit is contained in:
atlas 2026-10-08 13:23:17 +02:00
commit 121c4678aa
18 changed files with 580 additions and 277 deletions

View file

@ -1,7 +1,7 @@
//! The forge agreement: where agents' and hives' forge tokens live in the store,
//! and what the objects at those paths hold.
//!
//! Three kinds. The agent's own token on the swarm's forge
//! Four kinds. The agent's own token on the swarm's forge
//! ([`agent_token_path`]): `swarm-controller` mints it with the forge's admin
//! API, and `nix/agent-modules/forge-token.nix` reads it back under the agent's
//! own certificate. And the agent's accounts on external forges
@ -12,6 +12,10 @@
//! hive's own forge account, and hive-c0re reads it under the hive's
//! certificate. Neither end is senior, so both halves are stated once, here,
//! beside [`crate::matrix`].
//!
//! `swarm-controller`'s own forge token ([`controller_token_path`]) is the
//! fourth: the forge mints it, `swarm-bao-forge-publisher` on the forge's host
//! writes it here, and the controller reads it as it starts.
use serde::{Deserialize, Serialize};
@ -62,6 +66,33 @@ pub fn hive_token_path(hive: &str) -> Result<String, Error> {
Ok(format!("{prefix}/forge/token"))
}
/// The path holding `swarm-controller`'s own forge token: the admin account it
/// provisions every agent's and hive's forge objects with.
///
/// Under [`Kind::Controller`], which no hive's policy reads. The nix half is
/// `swarm-bao.nix`'s `controllerForgeTokenLeaf`, which the publisher's write
/// grant and the controller's read grant both name.
///
/// # Errors
/// Never in practice: the name segment is a constant. The `Result` is
/// [`principal_prefix`]'s.
pub fn controller_token_path() -> Result<String, Error> {
let prefix = principal_prefix(Kind::Controller, crate::matrix::CONTROLLER)?;
Ok(format!("{prefix}/forge/token"))
}
/// What [`controller_token_path`] holds. No token name beside the value, unlike
/// [`Credential`]: the publisher copies a file that holds the token alone.
///
/// No `Debug` derive: `value` is the forge admin credential.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControllerToken {
/// The token itself. `swarm-bao-forge-publisher` writes it with
/// `bao kv put … value=@<file>`, so this name is load-bearing for a writer
/// this crate does not control.
pub value: String,
}
/// What an agent's forge-token path, and a hive's [`hive_token_path`], holds.
///
/// No `Debug` derive: `value` is a live forge credential, and a derived
@ -148,6 +179,37 @@ mod tests {
);
}
#[test]
fn the_controller_token_is_under_the_controller_prefix() {
// Spelled out: `swarm-bao.nix` grants exactly this string.
assert_eq!(
controller_token_path().expect("a constant segment is legal"),
"swarm/controller/swarm-controller/forge/token"
);
}
#[test]
fn no_hive_policy_reaches_the_controller_token() {
let policy = crate::policy::render("alpha").expect("legal");
let covers = |path: &str| {
policy.lines().any(|line| {
line.strip_prefix("path \"")
.and_then(|rest| rest.split_once("\" {"))
.and_then(|(p, _)| p.strip_suffix('*'))
.is_some_and(|prefix| path.starts_with(prefix))
})
};
assert!(!covers(&format!(
"secret/data/{}",
controller_token_path().expect("legal")
)));
// The control: the same matcher finds the hive's own token.
assert!(covers(&format!(
"secret/data/{}",
hive_token_path("alpha").expect("legal")
)));
}
#[test]
fn no_forge_key_is_also_a_directory() {
// KV-v2 cannot hold a key whose name is also a prefix of another key.