Watch
0
0
Fork
You've already forked hyperhive
0

swarm-nats: require the responder's store identity, and drop its no-store mode

The responder reads the callout xkey before it connects, so a responder
with no secret store no longer starts, and its "deny every agent token,
serve OIDC" mode could not run. That mode is gone: `agent_token::Store`
is built from the settings the xkey load already read, and `authorize`
takes a source rather than an `Option` of one.

On the nix side, `deploy.nats.authBaoClientCertFile` and
`authBaoClientKeyFile` are now asserted at eval when the queue is
enabled. The units' no-identity branches, the copy unit's empty-file
fallback and its "denies agent tokens" message are removed; an absent
leaf file fails `swarm-nats-auth-secrets` like an absent seed does.
This commit is contained in:
atlas 2026-10-05 22:13:58 +02:00 • committed by mara
commit 52c931c3a7
5 changed files with 193 additions and 248 deletions

View file

@ -58,19 +58,11 @@ pub struct Store {
}
impl Store {
/// The store named by the `BAO_*` environment, or `None` when that is
/// unset: a responder with no store identity denies every agent token and
/// serves the OIDC path unchanged.
pub fn from_env(cert_role: String) -> Option<Self> {
match Settings::from_env() {
Ok(settings) => Some(Self {
settings,
cert_role,
}),
Err(e) => {
tracing::info!(reason = %e, "no secret store configured; agent tokens are denied");
None
}
/// The store at `settings`, logged in to as `cert_role`.
pub fn new(settings: Settings, cert_role: String) -> Self {
Self {
settings,
cert_role,
}
}
}
@ -149,20 +141,11 @@ async fn verify(source: &impl CredentialSource, token: &AgentToken<'_>) -> bool
}
/// The grant for an agent token, or `None` for a denial.
///
/// `source` is `None` when this responder has no store identity, which denies.
pub async fn authorize<S: CredentialSource>(
pub async fn authorize(
policy: &Policy,
source: Option<&S>,
source: &impl CredentialSource,
token: &AgentToken<'_>,
) -> Option<Permissions> {
let Some(source) = source else {
tracing::warn!(
agent = token.agent,
"agent token presented, but this responder has no secret store; denying"
);
return None;
};
if !verify(source, token).await {
return None;
}
@ -237,7 +220,7 @@ mod tests {
.expect("valid")
}
async fn grant(source: Option<&Fake>, token: &str) -> Option<Permissions> {
async fn grant(source: &Fake, token: &str) -> Option<Permissions> {
let Presented::Agent(token) = classify(token) else {
panic!("{token:?} must classify as an agent token");
};
@ -246,7 +229,7 @@ mod tests {
#[tokio::test]
async fn a_valid_agent_token_is_granted_exactly_its_own_subjects() {
let g = grant(Some(&atlas()), "swarm-agent.atlas.Ab9_-zSECRET")
let g = grant(&atlas(), "swarm-agent.atlas.Ab9_-zSECRET")
.await
.expect("granted");
assert_eq!(
@ -262,12 +245,12 @@ mod tests {
#[tokio::test]
async fn a_wrong_secret_is_denied() {
assert!(
grant(Some(&atlas()), "swarm-agent.atlas.Ab9_-zSECREX")
grant(&atlas(), "swarm-agent.atlas.Ab9_-zSECREX")
.await
.is_none()
);
assert!(
grant(Some(&atlas()), "swarm-agent.atlas.Ab9_-zSECRE")
grant(&atlas(), "swarm-agent.atlas.Ab9_-zSECRE")
.await
.is_none()
);
@ -279,7 +262,7 @@ mod tests {
#[tokio::test]
async fn another_agents_name_with_this_agents_secret_is_denied() {
assert!(
grant(Some(&atlas()), "swarm-agent.argus.Ab9_-zSECRET")
grant(&atlas(), "swarm-agent.argus.Ab9_-zSECRET")
.await
.is_none()
);
@ -288,19 +271,16 @@ mod tests {
#[tokio::test]
async fn an_agent_with_nothing_stored_is_denied() {
assert!(
grant(
Some(&stored("argus", "argus")),
"swarm-agent.atlas.Ab9_-zSECRET"
)
.await
.is_none()
grant(&stored("argus", "argus"), "swarm-agent.atlas.Ab9_-zSECRET")
.await
.is_none()
);
}
#[tokio::test]
async fn a_failed_lookup_is_denied() {
assert!(
grant(Some(&Fake::Fails), "swarm-agent.atlas.Ab9_-zSECRET")
grant(&Fake::Fails, "swarm-agent.atlas.Ab9_-zSECRET")
.await
.is_none()
);
@ -313,7 +293,7 @@ mod tests {
let bound = crate::introspect::INTROSPECTION_TIMEOUT;
let started = std::time::Instant::now();
assert!(
grant(Some(&Fake::Hangs), "swarm-agent.atlas.Ab9_-zSECRET")
grant(&Fake::Hangs, "swarm-agent.atlas.Ab9_-zSECRET")
.await
.is_none()
);
@ -325,24 +305,12 @@ mod tests {
);
}
#[tokio::test]
async fn no_store_is_denied() {
assert!(
grant(None, "swarm-agent.atlas.Ab9_-zSECRET")
.await
.is_none()
);
}
#[tokio::test]
async fn a_stored_object_naming_another_agent_is_denied() {
assert!(
grant(
Some(&stored("argus", "atlas")),
"swarm-agent.argus.Ab9_-zSECRET"
)
.await
.is_none()
grant(&stored("argus", "atlas"), "swarm-agent.argus.Ab9_-zSECRET")
.await
.is_none()
);
}
@ -359,7 +327,7 @@ mod tests {
let Presented::Agent(token) = classify("swarm-agent.atlas.Ab9_-zSECRET") else {
panic!("an agent token");
};
assert!(authorize(&bare, Some(&atlas()), &token).await.is_none());
assert!(authorize(&bare, &atlas(), &token).await.is_none());
}
/// Everything without the prefix reaches introspection as it arrived.

View file

@ -155,8 +155,9 @@ struct Args {
agent_token_publish_subjects: Vec<String>,
/// Role on the secret store's `cert` auth mount this responder logs in
/// as to read agent credentials. The store is found through the `BAO_*`
/// environment; with none, every agent token is denied.
/// as to read the callout xkey and agent credentials. The store is found
/// through the `BAO_*` environment, which the responder does not start
/// without.
#[arg(long, default_value = "swarm-nats-auth")]
store_cert_role: String,
}
@ -219,19 +220,25 @@ async fn serve(args: &Args) -> anyhow::Result<()> {
args.hive_publish_subjects.clone(),
)?
.with_agent_token_subjects(args.agent_token_publish_subjects.clone())?;
let store = agent_token::Store::from_env(args.store_cert_role.clone());
let http = reqwest::Client::new();
let issuer = nkeys::KeyPair::from_seed(&read_secret(&args.issuer_seed_file)?)
.context("parse the account signing seed")?;
let settings = swarm_secret_client::client::Settings::from_env()
.context("reading the secret store's address and this responder's identity")?;
// Before the connect: a responder that cannot open a sealed request must
// not start answering. Read, never created here; see `xkey`.
let callout_xkey = xkey::load(
&swarm_secret_client::SecretStore::from_env(&args.store_cert_role)
.await
.context("logging in to the secret store for the callout xkey")?,
&swarm_secret_client::SecretStore::connect(
&settings,
&args.store_cert_role,
swarm_secret_client::client::DEFAULT_CERT_MOUNT,
)
.await
.context("logging in to the secret store for the callout xkey")?,
)
.await?;
tracing::info!(public_key = %callout_xkey.public_key(), "loaded the callout xkey");
let store = agent_token::Store::new(settings, args.store_cert_role.clone());
let user_seed = read_secret(&args.user_seed_file)?;
let client = async_nats::ConnectOptions::with_nkey(user_seed)
.name("swarm-nats-auth")
@ -266,7 +273,7 @@ async fn serve(args: &Args) -> anyhow::Result<()> {
// the secret proved it; `granted` says which.
Some(agent_token::Presented::Agent(token)) => (
Some(format!("agent:{}", token.agent)),
agent_token::authorize(&policy, store.as_ref(), &token).await,
agent_token::authorize(&policy, &store, &token).await,
),
Some(agent_token::Presented::Malformed(e)) => {
tracing::warn!(error = %e, "malformed agent token; denying");