Watch
0
0
Fork
You've already forked hyperhive
0

swarm-nats-auth: refuse an auth request the server did not seal

Every nats-server this module starts renders `auth_callout.xkey` or does
not start, and the responder serves only that server, in the same
container and generation. A request without `Nats-Server-Xkey` therefore
comes from no supported server, so the plaintext framing is gone: such a
request is logged at warn and gets no reply, and the server that sent it
times out the connect as a denial. A signed denial would have had to go
out unsealed, which is the path this removes.

Also scopes two claims to what is true: the xkey's public half is read by
the swarm-nats-auth policy only, and the metadata-delete gap in the
replace-key procedure is about the two xkey paths, not all of
`secret/metadata/`.
This commit is contained in:
atlas 2026-10-05 22:46:52 +02:00 • committed by mara
commit 923554cd62
5 changed files with 78 additions and 79 deletions

View file

@ -256,7 +256,7 @@ async fn serve(args: &Args) -> anyhow::Result<()> {
);
while let Some(msg) = requests.next().await {
let Some((framing, req)) = open_request(&callout_xkey, &msg) else {
let Some((server_key, req)) = open_request(&callout_xkey, &msg) else {
continue;
};
// No token is a denial, not an error: an anonymous connect is a
@ -292,7 +292,6 @@ async fn serve(args: &Args) -> anyhow::Result<()> {
user_nkey = %req.user_nkey,
server_id = %req.server_id.id,
granted = permissions.is_some(),
sealed = framing.is_sealed(),
caller = caller.as_deref().unwrap_or("-"),
"auth request"
);
@ -315,7 +314,7 @@ async fn serve(args: &Args) -> anyhow::Result<()> {
),
None => respond::deny(&issuer, &req.server_id.id, &req.user_nkey),
};
let reply = match framing.seal(&callout_xkey, token.into_bytes()) {
let reply = match xkey::seal(&callout_xkey, &server_key, token.as_bytes()) {
Ok(reply) => reply,
Err(e) => {
tracing::warn!(error = %e, "could not seal the auth response; dropping");
@ -329,8 +328,9 @@ async fn serve(args: &Args) -> anyhow::Result<()> {
anyhow::bail!("subscription to {AUTH_SUBJECT} ended")
}
/// The request in `msg`, opened when the server sealed it, and the framing
/// its reply takes. `None`, logged, for anything else.
/// The request in `msg`, opened, and the server key its reply is sealed to.
/// `None`, logged, for anything else, a request with no
/// [`xkey::SERVER_XKEY_HEADER`] included.
///
/// Logged and dropped, never propagated: the loop calling this is the
/// swarm's login path, and exiting on one malformed payload would let any
@ -338,13 +338,13 @@ async fn serve(args: &Args) -> anyhow::Result<()> {
fn open_request(
callout_xkey: &nkeys::XKey,
msg: &async_nats::Message,
) -> Option<(xkey::Framing, request::AuthRequest)> {
let server_xkey = msg
) -> Option<(nkeys::XKey, request::AuthRequest)> {
let header = msg
.headers
.as_ref()
.and_then(|h| h.get(xkey::SERVER_XKEY_HEADER))
.map(async_nats::HeaderValue::as_str);
let (framing, payload) = match xkey::Framing::open(callout_xkey, server_xkey, &msg.payload) {
let (server_key, payload) = match xkey::open(callout_xkey, header, &msg.payload) {
Ok(opened) => opened,
Err(e) => {
tracing::warn!(error = %e, "unopenable auth request, ignoring");
@ -352,7 +352,7 @@ fn open_request(
}
};
match request::decode(&payload) {
Ok(req) => Some((framing, req)),
Ok(req) => Some((server_key, req)),
Err(e) => {
tracing::warn!(error = ?e, "undecodable auth request, ignoring");
None
@ -469,6 +469,32 @@ mod tests {
assert_eq!(args.agent_token_publish_subjects.len(), 5);
}
/// A request with no `Nats-Server-Xkey` is dropped before anything reads
/// it, so it gets no reply. The same request sealed, with the header,
/// opens.
#[test]
fn a_request_without_the_server_xkey_header_is_dropped() {
let own = nkeys::XKey::new();
let message = |headers, payload: Vec<u8>| async_nats::Message {
subject: AUTH_SUBJECT.into(),
reply: Some("_INBOX.reply".into()),
payload: payload.into(),
headers,
status: None,
description: None,
length: 0,
};
let request = xkey::tests::request_jwt();
assert!(open_request(&own, &message(None, request.clone())).is_none());
let server = nkeys::XKey::new();
let own_public = nkeys::XKey::from_public_key(&own.public_key()).expect("public");
let sealed = server.seal(&request, &own_public).expect("sealed");
let mut headers = async_nats::HeaderMap::new();
headers.insert(xkey::SERVER_XKEY_HEADER, server.public_key());
assert!(open_request(&own, &message(Some(headers), sealed)).is_some());
}
/// The control for the case above: an ordinary value parses through the
/// same flag, so a failure there is about the leading `-` and not about
/// the argument being unknown.