diff --git a/crates/bascule-shell/src/identity.rs b/crates/bascule-shell/src/identity.rs index 3faba44..5734b35 100644 --- a/crates/bascule-shell/src/identity.rs +++ b/crates/bascule-shell/src/identity.rs @@ -10,6 +10,12 @@ pub struct Identity { } pub fn detect() -> Identity { + // M5: SPIFFE workload identity wins when present. The SVID URI + // becomes the SAT session_leaf actor field, and QM's + // SpiffeSvidEvaluator validates it against the cluster allowlist. + // Mounted by SPIRE's k8s sidecar at the standard path; override + // via SPIFFE_SVID_PATH for tests / non-standard deploys. + if let Some(id) = detect_spiffe_svid() { return id; } if let Some(id) = detect_entra_wsl2() { return id; } if let Some(id) = detect_az_cli() { return id; } if let Some(id) = detect_kerberos() { return id; } @@ -17,6 +23,35 @@ pub fn detect() -> Identity { detect_system_user() } +/// M5: read a SPIFFE SVID URI from a sidecar-rendered file. The +/// standard SPIRE k8s sidecar writes the URI as a single line in +/// `/var/run/spire/svid-uri` (or wherever SPIFFE_SVID_PATH points). +/// We deliberately do NOT pull the `spiffe` crate here — bascule-shell +/// stays small and the URI string is all we need for the SAT session +/// claim. The mTLS dance is QM's job (server side); bascule-shell is +/// the producer. +fn detect_spiffe_svid() -> Option { + let path = std::env::var("SPIFFE_SVID_PATH") + .unwrap_or_else(|_| "/var/run/spire/svid-uri".to_string()); + let content = std::fs::read_to_string(&path).ok()?; + let uri = content.lines().next()?.trim().to_string(); + if !uri.starts_with("spiffe://") { + return None; + } + // Trust domain is the host component. + let trust_domain = uri + .strip_prefix("spiffe://") + .and_then(|rest| rest.split('/').next()) + .map(|s| s.to_string()); + Some(Identity { + principal: uri, + auth_method: "spiffe".into(), + domain: trust_domain, + source: format!("SPIFFE workload SVID ({})", path), + has_token: true, + }) +} + fn detect_entra_wsl2() -> Option { if !std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() { return None; diff --git a/crates/bascule-shell/src/main.rs b/crates/bascule-shell/src/main.rs index 1f2b6f0..4653096 100644 --- a/crates/bascule-shell/src/main.rs +++ b/crates/bascule-shell/src/main.rs @@ -134,6 +134,10 @@ fn set_env( // has something to match against. if std::env::var_os("BASCULE_ROLES").is_none() { let default_role = match id.auth_method.as_str() { + // M5: SPIFFE workload identities are explicitly cluster-issued + // -> trusted as operator until per-workload role provisioning + // lands in a future milestone. + "spiffe" => "operator", "oidc-entra" | "oidc-cached" => "operator", "kerberos" => "operator", "ssh-key" => "apprentice",