diff --git a/nix/host-modules/glue-nats-auth-bao-identity.nix b/nix/host-modules/glue-nats-auth-bao-identity.nix index 1f4c708b..855ad3ac 100644 --- a/nix/host-modules/glue-nats-auth-bao-identity.nix +++ b/nix/host-modules/glue-nats-auth-bao-identity.nix @@ -1,8 +1,8 @@ # Glue: point the queue's auth-callout responder at the bao leaf minted for it. # # ONE PAIRING PER FILE — the swarm-nats-auth principal ← bao, and nothing else. -# Deleting this leaves a responder with no store identity unless the operator -# names one: every agent token is then denied, and OIDC clients are unaffected. +# Deleting this leaves a queue with no responder identity unless the operator +# names one, which ./swarm-nats.nix refuses at eval. # # ⚠️ The minting is NOT here. ./glue-bao-tls.nix holds the CA and signs the # leaf. What belongs here is the pairing: which paths the responder presents. diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index e1f99f5a..8a1007ad 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -297,23 +297,21 @@ let tlsKeyCredential = "tls-key"; tlsKeyCredentialPath = "/run/credentials/nats.service/${tlsKeyCredential}"; - # The responder's own store identity, for reading agent queue credentials. - # Without it the responder denies every agent token; agents then fall back to - # their hive's OIDC client. + # The responder's own store identity, for the callout xkey, agent queue + # credentials and its OIDC client secret. Required: an assertion below refuses + # a queue without it. authStoreActive = deployCfg.nats.authBaoClientCertFile != null && deployCfg.nats.authBaoClientKeyFile != null; # The role ./swarm-bao.nix writes on the store's `cert` mount, by the same name. authCertRole = "swarm-nats-auth"; # Store identity files the copy unit delivers, as credential id → host source. - authStoreFiles = lib.optionalAttrs authStoreActive ( - { - "bao-client.pem" = deployCfg.nats.authBaoClientCertFile; - "bao-client-key.pem" = deployCfg.nats.authBaoClientKeyFile; - } - // lib.optionalAttrs (baoDeploy.serverCaFile != null) { - "bao-ca.pem" = baoDeploy.serverCaFile; - } - ); + authStoreFiles = { + "bao-client.pem" = deployCfg.nats.authBaoClientCertFile; + "bao-client-key.pem" = deployCfg.nats.authBaoClientKeyFile; + } + // lib.optionalAttrs (baoDeploy.serverCaFile != null) { + "bao-ca.pem" = baoDeploy.serverCaFile; + }; authCredential = id: "/run/credentials/swarm-nats-auth.service/${id}"; atomicWriteSecret = import ./lib/atomic-write-secret.nix { }; @@ -516,17 +514,19 @@ in example = "/var/lib/swarm-bao-pki/nats-auth.pem"; description = '' Client certificate the auth-callout responder presents to the swarm's - secret store to read agent queue credentials, and that - `swarm-bao-nats-auth-oidc` presents to read the responder's own OIDC - client secret. Its subject must be + secret store to read the callout xkey and agent queue credentials, and + that `swarm-bao-nats-auth-oidc` and `swarm-bao-nats-callout-xkey` + present to read the responder's own OIDC client secret and the xkey's + public half. Its subject must be {option}`services.hyperhive.deploy.bao.natsAuthCommonName`. - No default. ./glue-nats-auth-bao-identity.nix points it at the leaf + Required when `deploy.nats.enable` is set; unset fails at eval. + ./glue-nats-auth-bao-identity.nix points it at the leaf ./glue-bao-tls.nix mints, where this host mints one; on any other - queue host it is a file the operator places. Unset, or set to a file - that does not exist, `swarm-bao-nats-auth-oidc` fails; a responder - with no OIDC client secret on disk cannot start, and the queue then - refuses every client. + queue host it is a file the operator places. Set to a file that does + not exist, `swarm-nats-auth-secrets`, `swarm-bao-nats-auth-oidc` and + `swarm-bao-nats-callout-xkey` fail, and the queue's server does not + start without the xkey. A path, never a value. ''; @@ -620,6 +620,24 @@ in to have this host mint all four. ''; } + { + # The callout xkey and the responder's OIDC client secret are read + # from the store under this identity; without either the server or + # the responder does not start. + assertion = authStoreActive; + message = '' + services.hyperhive.deploy.nats.authBaoClientCertFile and + authBaoClientKeyFile must both be set. + + The queue's callout xkey and its responder's OIDC client secret + are read from the swarm's secret store with this certificate, and + neither the server nor the responder starts without them. They + default to the leaf the store mints when it runs on this host; + elsewhere, issue a leaf whose CN is + services.hyperhive.deploy.bao.natsAuthCommonName and name it in + both options. + ''; + } ]; # Declared here rather than in the collector's module: an entry then @@ -929,18 +947,16 @@ in RestartSec = "5s"; SyslogIdentifier = "swarm-nats-auth"; }; - # The store the responder reads agent credentials from. Absent - # without an identity, which the responder reads as "no store". - environment = lib.optionalAttrs authStoreActive ( - { - BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}"; - BAO_CLIENT_CERT = authCredential "bao-client.pem"; - BAO_CLIENT_KEY = authCredential "bao-client-key.pem"; - } - // lib.optionalAttrs (authStoreFiles ? "bao-ca.pem") { - BAO_CACERT = authCredential "bao-ca.pem"; - } - ); + # The store the responder reads the callout xkey and agent + # credentials from. + environment = { + BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}"; + BAO_CLIENT_CERT = authCredential "bao-client.pem"; + BAO_CLIENT_KEY = authCredential "bao-client-key.pem"; + } + // lib.optionalAttrs (authStoreFiles ? "bao-ca.pem") { + BAO_CACERT = authCredential "bao-ca.pem"; + }; }; # The server binary, so an operator with a shell in here can @@ -973,7 +989,7 @@ in lib.optional deployCfg.nats.autoGenerateCallout "swarm-nats-callout-keys.service" # Where the store's PKI is minted on this host, the responder's leaf is # one of its files. Ordering only: elsewhere the unit does not exist. - ++ lib.optional authStoreActive "swarm-bao-pki.service"; + ++ [ "swarm-bao-pki.service" ]; requires = lib.optional deployCfg.nats.autoGenerateCallout "swarm-nats-callout-keys.service"; serviceConfig = { Type = "oneshot"; @@ -989,17 +1005,9 @@ in install -m 0400 ${lib.escapeShellArg issuerSeedFile} \ ${lib.escapeShellArg (hostPath "issuer.seed")} '' - # The store identity is optional to the responder's agent-token path, so - # an absent source is delivered as an empty file rather than failing this - # unit: a missing `LoadCredential` source stops the responder starting. + lib.concatStrings ( lib.mapAttrsToList (id: source: '' - if [ -s ${lib.escapeShellArg source} ]; then - install -m 0400 ${lib.escapeShellArg source} ${lib.escapeShellArg (hostPath id)} - else - echo "${source} is absent; the queue responder denies agent tokens until it exists" >&2 - install -m 0400 /dev/null ${lib.escapeShellArg (hostPath id)} - fi + install -m 0400 ${lib.escapeShellArg source} ${lib.escapeShellArg (hostPath id)} '') authStoreFiles ); }; @@ -1013,8 +1021,7 @@ in # # ⚠️ Without the secret the responder does not start (its `LoadCredential` # source is missing), and `auth_callout` with no responder refuses every - # client. A host with no responder leaf therefore fails here, naming the - # options, rather than rendering no unit. + # client. systemd.services.swarm-bao-nats-auth-oidc = { description = "fetch the swarm queue responder's OIDC client secret from the swarm secret store"; # The store's units exist only where the store runs; `Requires=` on an @@ -1050,70 +1057,61 @@ in }; environment = { BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}"; - } - // lib.optionalAttrs authStoreActive { BAO_CLIENT_CERT = deployCfg.nats.authBaoClientCertFile; BAO_CLIENT_KEY = deployCfg.nats.authBaoClientKeyFile; } // lib.optionalAttrs (baoDeploy.serverCaFile != null) { BAO_CACERT = baoDeploy.serverCaFile; }; - script = - if !authStoreActive then - '' - echo "the swarm queue responder has no store identity, so it cannot read its OIDC client secret: set services.hyperhive.deploy.nats.authBaoClientCertFile and authBaoClientKeyFile" >&2 - exit 1 - '' - else - '' - set -euo pipefail + script = '' + set -euo pipefail - ${atomicWriteSecret} - ${refreshConsumer} + ${atomicWriteSecret} + ${refreshConsumer} - err="$(mktemp)" - trap 'rm -f "$err"' EXIT + err="$(mktemp)" + trap 'rm -f "$err"' EXIT - # `-token-only` answers on stdout and skips the token helper, a - # `sh` this unit's `path` does not carry. - if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then - echo "could not log in to swarm-bao with the queue responder's certificate; leaving its OIDC client secret as it is." >&2 - if [ -s "$err" ]; then - cat "$err" >&2 - else - echo "bao failed without writing a diagnostic." >&2 - fi - exit 1 - fi - export BAO_TOKEN + # `-token-only` answers on stdout and skips the token helper, a + # `sh` this unit's `path` does not carry. + if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then + echo "could not log in to swarm-bao with the queue responder's certificate; leaving its OIDC client secret as it is." >&2 + if [ -s "$err" ]; then + cat "$err" >&2 + else + echo "bao failed without writing a diagnostic." >&2 + fi + exit 1 + fi + export BAO_TOKEN - if ! secret="$(bao kv get -field=value ${lib.escapeShellArg storeSecretPath} 2>"$err")"; then - echo "swarm-bao did not return ${storeSecretPath}; the queue responder has no OIDC client secret yet." >&2 - if [ -s "$err" ]; then - cat "$err" >&2 - else - echo "bao failed without writing a diagnostic." >&2 - fi - exit 1 - fi + if ! secret="$(bao kv get -field=value ${lib.escapeShellArg storeSecretPath} 2>"$err")"; then + echo "swarm-bao did not return ${storeSecretPath}; the queue responder has no OIDC client secret yet." >&2 + if [ -s "$err" ]; then + cat "$err" >&2 + else + echo "bao failed without writing a diagnostic." >&2 + fi + exit 1 + fi - if [ -z "$secret" ]; then - echo "swarm-bao returned an empty ${storeSecretPath}; the queue responder has no OIDC client secret yet." >&2 - exit 1 - fi + if [ -z "$secret" ]; then + echo "swarm-bao returned an empty ${storeSecretPath}; the queue responder has no OIDC client secret yet." >&2 + exit 1 + fi - # Root-owned 0400: `LoadCredential` reads it as root before the - # responder's `DynamicUser` exists. `printf` inside - # `atomic_write_secret` is a builtin, so the plaintext never - # becomes an argument in /proc. - install -d -m 0700 ${lib.escapeShellArg secretDir} - if secret_differs ${lib.escapeShellArg (hostPath "oidc-client.secret")} "$secret"; then - atomic_write_secret 0400 "" ${lib.escapeShellArg (hostPath "oidc-client.secret")} "$secret" - fi + # Root-owned 0400: `LoadCredential` reads it as root before the + # responder's `DynamicUser` exists. `printf` inside + # `atomic_write_secret` is a builtin, so the plaintext never + # becomes an argument in /proc. + install -d -m 0700 ${lib.escapeShellArg secretDir} + if secret_differs ${lib.escapeShellArg (hostPath "oidc-client.secret")} "$secret"; then + atomic_write_secret 0400 "" ${lib.escapeShellArg (hostPath "oidc-client.secret")} "$secret" + fi - # `LoadCredential` copies the file once, at the responder's start. - refresh_consumer ${lib.escapeShellArg machine} swarm-nats-auth.service ${lib.escapeShellArg (hostPath "oidc-client.secret")} - ''; + # `LoadCredential` copies the file once, at the responder's start. + refresh_consumer ${lib.escapeShellArg machine} swarm-nats-auth.service ${lib.escapeShellArg (hostPath "oidc-client.secret")} + ''; }; # The callout xkey's public half, from the store into the file @@ -1157,57 +1155,48 @@ in }; environment = { BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}"; - } - // lib.optionalAttrs authStoreActive { BAO_CLIENT_CERT = deployCfg.nats.authBaoClientCertFile; BAO_CLIENT_KEY = deployCfg.nats.authBaoClientKeyFile; } // lib.optionalAttrs (baoDeploy.serverCaFile != null) { BAO_CACERT = baoDeploy.serverCaFile; }; - script = - if !authStoreActive then - '' - echo "the swarm queue responder has no store identity, so the callout xkey can be neither created nor fetched and the queue does not start: set services.hyperhive.deploy.nats.authBaoClientCertFile and authBaoClientKeyFile" >&2 - exit 1 - '' - else - '' - set -euo pipefail + script = '' + set -euo pipefail - ${atomicWriteSecret} - ${refreshConsumer} + ${atomicWriteSecret} + ${refreshConsumer} - swarm-nats-auth ensure-xkey --store-cert-role ${lib.escapeShellArg authCertRole} + swarm-nats-auth ensure-xkey --store-cert-role ${lib.escapeShellArg authCertRole} - err="$(mktemp)" - trap 'rm -f "$err"' EXIT + err="$(mktemp)" + trap 'rm -f "$err"' EXIT - if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then - echo "could not log in to swarm-bao with the queue responder's certificate; leaving the callout xkey file as it is." >&2 - cat "$err" >&2 - exit 1 - fi - export BAO_TOKEN + if ! BAO_TOKEN="$(bao login -method=cert -token-only 2>"$err")"; then + echo "could not log in to swarm-bao with the queue responder's certificate; leaving the callout xkey file as it is." >&2 + cat "$err" >&2 + exit 1 + fi + export BAO_TOKEN - if ! xkey="$(bao kv get -field=value ${lib.escapeShellArg xkeyPublicPath} 2>"$err")"; then - echo "swarm-bao did not return ${xkeyPublicPath}." >&2 - cat "$err" >&2 - exit 1 - fi - if [ -z "$xkey" ]; then - echo "swarm-bao returned an empty ${xkeyPublicPath}." >&2 - exit 1 - fi + if ! xkey="$(bao kv get -field=value ${lib.escapeShellArg xkeyPublicPath} 2>"$err")"; then + echo "swarm-bao did not return ${xkeyPublicPath}." >&2 + cat "$err" >&2 + exit 1 + fi + if [ -z "$xkey" ]; then + echo "swarm-bao returned an empty ${xkeyPublicPath}." >&2 + exit 1 + fi - install -d -m 0755 ${lib.escapeShellArg hostRuntimeDir} - if secret_differs ${lib.escapeShellArg "${hostRuntimeDir}/${xkeyPublicFile}"} "$xkey"; then - atomic_write_secret 0444 "" ${lib.escapeShellArg "${hostRuntimeDir}/${xkeyPublicFile}"} "$xkey" - fi + install -d -m 0755 ${lib.escapeShellArg hostRuntimeDir} + if secret_differs ${lib.escapeShellArg "${hostRuntimeDir}/${xkeyPublicFile}"} "$xkey"; then + atomic_write_secret 0444 "" ${lib.escapeShellArg "${hostRuntimeDir}/${xkeyPublicFile}"} "$xkey" + fi - # `renderConfig` reads the file as the server starts, and only then. - refresh_consumer ${lib.escapeShellArg machine} nats.service ${lib.escapeShellArg "${hostRuntimeDir}/${xkeyPublicFile}"} - ''; + # `renderConfig` reads the file as the server starts, and only then. + refresh_consumer ${lib.escapeShellArg machine} nats.service ${lib.escapeShellArg "${hostRuntimeDir}/${xkeyPublicFile}"} + ''; }; # ⚠️ Minted on the HOST, not in the container, because the responder is diff --git a/nix/module-eval/nats-authelia.nix b/nix/module-eval/nats-authelia.nix index 99ade3fa..e417da1e 100644 --- a/nix/module-eval/nats-authelia.nix +++ b/nix/module-eval/nats-authelia.nix @@ -82,6 +82,12 @@ let responderOf = h: h.containers.swarm-nats.config.systemd.services.swarm-nats-auth; + refusedWithoutIdentity = + m: + lib.any ( + a: !a.assertion && lib.hasInfix "deploy.nats.authBaoClientCertFile and" a.message + ) m.assertions; + # A hive running NOTHING of the swarm's own services — no IdP here, no # `swarm.authelia.url` set by hand. The whole point of the fixture is what it # does *not* say: no IdP address is given and none is derived from @@ -91,6 +97,17 @@ let autheliaColocated = hive { deploy.authelia.enable = true; }; cases = [ + { + # The xkey and the responder's OIDC client secret are read only from + # the store, so a queue with no responder leaf cannot start. Refused at + # eval; the controls are a leaf named by hand and the store host's + # minted one. + name = "a queue with no responder store identity is refused at eval, one with a leaf is not"; + ok = + refusedWithoutIdentity natsOldPath + && !(refusedWithoutIdentity natsWithStoreIdentity) + && !(refusedWithoutIdentity natsOnStoreHost); + } { # Reads the RENDERED settings, not the option: `calloutBlocks {…} // { # … }` is a shallow merge, and a future edit that dropped or shadowed @@ -182,17 +199,6 @@ let && !(lib.hasInfix "exit 0" s) && u.serviceConfig.Restart == "on-failure"; } - { - name = "a queue host with no responder leaf renders an xkey fetch that fails naming the options"; - ok = - let - u = natsOldPath.systemd.services.swarm-bao-nats-callout-xkey; - in - !(u.environment ? BAO_CLIENT_CERT) - && lib.hasInfix "deploy.nats.authBaoClientCertFile" u.script - && lib.hasInfix "exit 1" u.script - && !(lib.hasInfix "ensure-xkey" u.script); - } { # Its creates are what the responder's role was granted; on the store's # host the role's unit has to have run first. @@ -281,16 +287,16 @@ let && lib.hasPrefix "https://" u.environment.BAO_ADDR; } { - # An absent leaf must not stop the responder's agent-token path: it is - # delivered empty, beside the two seeds. - name = "the copy unit delivers the store identity, and an absent one as an empty file"; + # Copied as the seeds are, so an absent leaf fails the unit rather than + # reaching the responder as a file it cannot log in with. + name = "the copy unit delivers the store identity beside the two seeds, and nothing in its place when absent"; ok = let s = natsWithStoreIdentity.systemd.services.swarm-nats-auth-secrets.script; in lib.hasInfix "install -m 0400 /etc/pki/nats-auth.pem " s && lib.hasInfix "install -m 0400 /etc/pki/nats-auth-key.pem " s - && lib.hasInfix "install -m 0400 /dev/null " s + && !(lib.hasInfix "/dev/null" s) && lib.hasInfix "callout-user.seed" s && lib.hasInfix "issuer.seed" s; } @@ -318,31 +324,6 @@ let && !(lib.hasInfix "oidc-client" copy) && !(lib.hasInfix "authelia" copy); } - { - # No responder leaf is a responder with no OIDC secret, which refuses - # every client. The unit still renders, and fails naming the options. - name = "a queue host with no responder leaf renders a secret fetch that fails naming the options"; - ok = - let - fetch = natsOldPath.systemd.services.swarm-bao-nats-auth-oidc; - in - !(fetch.environment ? BAO_CLIENT_CERT) - && lib.hasInfix "deploy.nats.authBaoClientCertFile" fetch.script - && lib.hasInfix "exit 1" fetch.script - && !(lib.hasInfix "bao login" fetch.script); - } - { - # The control: no identity, no store wiring, and the responder's - # credentials are exactly the three it cannot run without. - name = "a responder with no store identity is not pointed at a store"; - ok = - let - u = responderOf natsOldPath; - in - !(u.environment ? BAO_ADDR) - && !(u.environment ? BAO_CLIENT_CERT) - && lib.length u.serviceConfig.LoadCredential == 3; - } { # On the store's host the fetch logs in under the responder's leaf, so it # waits for the unit writing that role. diff --git a/swarm-nats-auth/src/agent_token.rs b/swarm-nats-auth/src/agent_token.rs index 29d14f36..c8254033 100644 --- a/swarm-nats-auth/src/agent_token.rs +++ b/swarm-nats-auth/src/agent_token.rs @@ -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 { - 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( +pub async fn authorize( policy: &Policy, - source: Option<&S>, + source: &impl CredentialSource, token: &AgentToken<'_>, ) -> Option { - 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 { + async fn grant(source: &Fake, token: &str) -> Option { 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. diff --git a/swarm-nats-auth/src/main.rs b/swarm-nats-auth/src/main.rs index d75da9a2..7f2a477e 100644 --- a/swarm-nats-auth/src/main.rs +++ b/swarm-nats-auth/src/main.rs @@ -155,8 +155,9 @@ struct Args { agent_token_publish_subjects: Vec, /// 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");