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:
parent
c433bdef92
commit
121c4678aa
18 changed files with 580 additions and 277 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue