drop the shared hive agent login
Every agent now connects to the queue and reads logs with its own secret from the store (#5181), so the per-hive `hive-<h>-agent` OIDC client has no reader left. Remove it end to end: - hive-c0re no longer forwards `hive-queue-agent-{secret,client-id}` into containers, and no longer forwards HIVE_AGENT_OIDC_TOKEN_ENDPOINT; the agent module that loaded them (nix/agent-modules/queue.nix) is gone, its options refused with a removal message. - The host reader (glue-queue-agent-credential.nix, swarm-bao-queue-agent), its store role and policy unit (swarm-bao-queue-agent-policy), its PKI leaf and the deploy.bao.queueAgent* / queue.agentCredentialDir options are gone; the options are refused with a removal message. - authelia mints no `hive-<h>-agent` client, and swarm-secret-publish no longer writes swarm/hives/<h>/queue/agent. - swarm-secret-client drops queue::agent_client_path / queue::Credential. - HIVE_AGENT_NATS_URL is now gated on the agents' queue address alone. The log store's authelia check stays for operators. swarm-nats-auth still refuses a `-agent` client id; that refusal and `agentClientSuffix` are left for a follow-up. Refs #4459 Refs #4765
This commit is contained in:
parent
d19bd83aa5
commit
868c6cf96c
41 changed files with 286 additions and 1410 deletions
|
|
@ -8,10 +8,9 @@
|
|||
//! asked to authenticate in order to obtain the thing it authenticates with:
|
||||
//! by the time it runs, its identity is already inside it.
|
||||
//!
|
||||
//! **A credential and not a bind mount**, the same answer and the same reason
|
||||
//! as the queue secret beside it in [`super::host_config`]: the files are
|
||||
//! `0600` to `hive-core` and the units that use them run as the unprivileged
|
||||
//! agent user, so a bind would deliver a private key that user cannot open.
|
||||
//! **A credential and not a bind mount**: the files are `0600` to `hive-core`
|
||||
//! and the units that use them run as the unprivileged agent user, so a bind
|
||||
//! would deliver a private key that user cannot open.
|
||||
//! The container manager reads a `--load-credential` source as root and
|
||||
//! re-exposes it under the consuming unit's own `User=`.
|
||||
//!
|
||||
|
|
@ -60,8 +59,7 @@ const SERVER_CA_CREDENTIAL: &str = "hive-agent-bao-server-ca";
|
|||
///
|
||||
/// Already-staged files survive a failed collection. A store that is briefly
|
||||
/// unreachable then delivers the identity from the last successful read rather
|
||||
/// than none at all — the same call as the queue reader's, which leaves its
|
||||
/// files alone when the store says nothing.
|
||||
/// than none at all.
|
||||
pub async fn stage(agent: &str) -> Vec<CredentialMount> {
|
||||
let dir = crate::paths::agent_identity_dir(agent);
|
||||
match collect(agent, &dir).await {
|
||||
|
|
@ -246,7 +244,7 @@ mod tests {
|
|||
|
||||
/// Half a delivery is worse than none: the container's check would fail
|
||||
/// on the missing key and report a login problem instead of a delivery
|
||||
/// one. Same both-or-neither rule the queue credential states.
|
||||
/// one.
|
||||
#[test]
|
||||
fn a_certificate_without_its_key_forwards_nothing() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use hive_priv_sock::{BindMount, CredentialMount};
|
||||
use hive_priv_sock::BindMount;
|
||||
use hive_types::Permission;
|
||||
|
||||
use crate::coordinator::{AgentPaths, HiveEnv};
|
||||
|
|
@ -417,70 +417,6 @@ pub fn hand_permissions(agent: &str, runtime_dir: &Path) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Env var naming the host directory `swarm-bao-queue-agent.service` lands
|
||||
/// this hive's agent queue credential in. Set by the hive-c0re NixOS module
|
||||
/// from `deploy.hive-controller.queue.agentCredentialDir`; absent means this
|
||||
/// daemon runs outside its unit, which is the same "no queue credential"
|
||||
/// answer as an empty directory.
|
||||
const QUEUE_CREDENTIAL_DIR_ENV: &str = "HIVE_C0RE_AGENT_QUEUE_CREDENTIAL_DIR";
|
||||
|
||||
/// systemd credential ids the two files arrive under inside the container.
|
||||
/// `nix/agent-modules/queue.nix` spells the same two names in the harness
|
||||
/// unit's `LoadCredential=`; neither side can discover the other's, so a
|
||||
/// rename here is a rename there.
|
||||
const QUEUE_SECRET_CREDENTIAL: &str = "hive-queue-agent-secret";
|
||||
const QUEUE_CLIENT_ID_CREDENTIAL: &str = "hive-queue-agent-client-id";
|
||||
|
||||
/// Forward this hive's agent queue credential into `agent_name`'s container
|
||||
/// as two systemd credentials, or nothing when the publisher has not landed
|
||||
/// it yet.
|
||||
///
|
||||
/// **A credential and not a bind mount, because of the mode.** The secret is
|
||||
/// `root:0600` on the host and the harness runs as the unprivileged agent
|
||||
/// user, so a bind would deliver a file that user cannot open. nspawn's
|
||||
/// `--load-credential` is read by the container manager as root and
|
||||
/// re-exposed under the consuming unit's own `User=`, which is the whole
|
||||
/// difference. hive-c0re never reads the bytes either way — it runs as
|
||||
/// `hive-core` and only ever needs to know the file is *there*, which a
|
||||
/// `0755` directory permits.
|
||||
///
|
||||
/// **Absent files are legal.** Authelia mints the secret on its first boot
|
||||
/// and a publisher on that host puts it in the store, so "nothing at that
|
||||
/// path" is the ordinary early state of a swarm rather than a fault. Saying
|
||||
/// so at `info!` is what keeps it from being invisible: without a line here,
|
||||
/// an agent that never connects looks identical to one that was never
|
||||
/// configured.
|
||||
fn queue_agent_credentials(agent_name: &str, dir: Option<&Path>) -> Vec<CredentialMount> {
|
||||
let Some(dir) = dir else {
|
||||
tracing::info!(
|
||||
%agent_name,
|
||||
"no {QUEUE_CREDENTIAL_DIR_ENV} in this daemon's environment — agent gets no swarm queue credential"
|
||||
);
|
||||
return Vec::new();
|
||||
};
|
||||
let secret = dir.join("secret");
|
||||
let client_id = dir.join("client_id");
|
||||
// Both or neither: a client holding one of the two comes up fine and
|
||||
// never connects.
|
||||
if !secret.is_file() || !client_id.is_file() {
|
||||
tracing::info!(
|
||||
%agent_name, dir = %dir.display(),
|
||||
"swarm queue credential not published yet — agent gets no swarm queue credential"
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
vec![
|
||||
CredentialMount {
|
||||
name: QUEUE_SECRET_CREDENTIAL.to_owned(),
|
||||
host_path: secret.to_string_lossy().into_owned(),
|
||||
},
|
||||
CredentialMount {
|
||||
name: QUEUE_CLIENT_ID_CREDENTIAL.to_owned(),
|
||||
host_path: client_id.to_string_lossy().into_owned(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Idempotently rewrite the lines in `/etc/nixos-containers/<container>.conf`
|
||||
/// that hive-c0re owns: `PRIVATE_NETWORK` (always 1), `HOST_ADDRESS` (the
|
||||
/// bridge gateway IP) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind). What
|
||||
|
|
@ -537,18 +473,11 @@ async fn set_nspawn_flags(
|
|||
// is needed here — the bind alone is enough.
|
||||
let claude_mount = container_claude_mount(agent_name);
|
||||
|
||||
// The agent's own swarm-queue credential — the one thing forwarded in
|
||||
// here, and forwarded *as a credential* rather than a bind for the
|
||||
// reason `queue_agent_credentials` records.
|
||||
let queue_credential_dir = std::env::var_os(QUEUE_CREDENTIAL_DIR_ENV).map(PathBuf::from);
|
||||
let mut load_creds = queue_agent_credentials(agent_name, queue_credential_dir.as_deref());
|
||||
|
||||
// The agent's own identity *at* the swarm secret store, collected under
|
||||
// this hive's certificate and forwarded the same way and for the same
|
||||
// reason. Staged here rather than at spawn because this is the one place
|
||||
// that writes the container's credential list — see
|
||||
// this hive's certificate. Staged here rather than at spawn because this
|
||||
// is the one place that writes the container's credential list — see
|
||||
// `super::agent_identity` for the whole hop.
|
||||
load_creds.extend(super::agent_identity::stage(agent_name).await);
|
||||
let load_creds = super::agent_identity::stage(agent_name).await;
|
||||
|
||||
let mut binds: Vec<BindMount> = vec![
|
||||
BindMount {
|
||||
|
|
@ -678,10 +607,8 @@ async fn set_nspawn_flags(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
BindMount, HostMemory, MEMORY_CAP_FILE, QUEUE_CLIENT_ID_CREDENTIAL,
|
||||
QUEUE_SECRET_CREDENTIAL, bind_child_agent_dirs, bind_own_agent_dirs,
|
||||
holds_manage_root_agent, limits_to_apply, memory_cap_value, queue_agent_credentials,
|
||||
stage_limits,
|
||||
BindMount, HostMemory, MEMORY_CAP_FILE, bind_child_agent_dirs, bind_own_agent_dirs,
|
||||
holds_manage_root_agent, limits_to_apply, memory_cap_value, stage_limits,
|
||||
};
|
||||
use crate::coordinator::HiveEnv;
|
||||
|
||||
|
|
@ -791,51 +718,6 @@ mod tests {
|
|||
assert!(binds.is_empty());
|
||||
}
|
||||
|
||||
/// The ordinary state of a swarm before the publisher on the authelia
|
||||
/// host has run: the directory is named and empty. Forwarding a
|
||||
/// credential whose source does not exist would make every agent
|
||||
/// container refuse to start, so this has to be silence-with-a-log
|
||||
/// rather than a partial list.
|
||||
#[test]
|
||||
fn an_unpublished_queue_credential_forwards_nothing() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
assert!(queue_agent_credentials("iris", None).is_empty());
|
||||
assert!(queue_agent_credentials("iris", Some(dir.path())).is_empty());
|
||||
std::fs::write(dir.path().join("secret"), "s").expect("write secret");
|
||||
assert!(
|
||||
queue_agent_credentials("iris", Some(dir.path())).is_empty(),
|
||||
"a secret with no client id is not a usable credential"
|
||||
);
|
||||
}
|
||||
|
||||
/// Both files present: two credentials, named the same two ids the
|
||||
/// harness unit imports, pointing at the two files the reader unit
|
||||
/// wrote.
|
||||
#[test]
|
||||
fn a_published_queue_credential_forwards_both_files() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
std::fs::write(dir.path().join("secret"), "s").expect("write secret");
|
||||
std::fs::write(dir.path().join("client_id"), "hive-h1-agent").expect("write client_id");
|
||||
let creds = queue_agent_credentials("iris", Some(dir.path()));
|
||||
let named: Vec<(String, String)> = creds
|
||||
.into_iter()
|
||||
.map(|c| (c.name, c.host_path))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
named,
|
||||
[
|
||||
(
|
||||
QUEUE_SECRET_CREDENTIAL.to_owned(),
|
||||
dir.path().join("secret").to_string_lossy().into_owned(),
|
||||
),
|
||||
(
|
||||
QUEUE_CLIENT_ID_CREDENTIAL.to_owned(),
|
||||
dir.path().join("client_id").to_string_lossy().into_owned(),
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_swarm_permissions_deny_without_failing() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
|
|
|
|||
Loading…
Reference in a new issue