feat(m5): bascule-shell prefers SPIFFE SVID URI as principal
Adds bascule_shell::identity::detect_spiffe_svid which reads a SPIFFE SVID URI from /var/run/spire/svid-uri (override via SPIFFE_SVID_PATH). When present it wins over Entra/AZ-CLI/ Kerberos/cached-OIDC/system-user, becoming the SAT session_leaf actor field that QM's M5 SpiffeSvidEvaluator validates against the cluster allowlist. Why a file read instead of the SPIFFE Workload API: bascule-shell ships independently from QM and the standard SPIRE k8s sidecar writes the URI as /var/run/spire/svid-uri alongside svid.pem. The file path is hermetic for tests and matches the deploy model. If a future iteration needs continuous SVID URI rotation, switch to a notify watcher or pull spiffe::workload_api. Trust domain is parsed and surfaced as Identity.domain so the banner / dashboard can show "spiffe://gh.dev" affiliation. bascule_shell::main::set_env: auth_method == "spiffe" maps to BASCULE_ROLES = "operator" by default. SPIRE-attested workloads are explicitly cluster-issued so they get operator role until per-workload provisioning lands. The existing precedence (caller-set BASCULE_ROLES wins) is unchanged. Bascule mTLS *channel* construction (Bascule -> QM gRPC renegotiation) is intentionally NOT wired in this commit. Per ADR D9 hot path is local; the renegotiation client is deferred to M6 alongside the Rekor signing client because they share the rustls dep tree. Tested (Docker rust:1.88-bookworm): cargo build -p bascule-shell -p bascule-core clean cargo test -p bascule-core --lib sat 7/7 (M1 regression) Stacked on feat/m3-defcon-env. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Claude Code <claude@guildhouse.dev>
This commit is contained in:
parent
df5a2a6f88
commit
2520525ec6
2 changed files with 39 additions and 0 deletions
|
|
@ -10,6 +10,12 @@ pub struct Identity {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn detect() -> 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_entra_wsl2() { return id; }
|
||||||
if let Some(id) = detect_az_cli() { return id; }
|
if let Some(id) = detect_az_cli() { return id; }
|
||||||
if let Some(id) = detect_kerberos() { return id; }
|
if let Some(id) = detect_kerberos() { return id; }
|
||||||
|
|
@ -17,6 +23,35 @@ pub fn detect() -> Identity {
|
||||||
detect_system_user()
|
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<Identity> {
|
||||||
|
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<Identity> {
|
fn detect_entra_wsl2() -> Option<Identity> {
|
||||||
if !std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() {
|
if !std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,10 @@ fn set_env(
|
||||||
// has something to match against.
|
// has something to match against.
|
||||||
if std::env::var_os("BASCULE_ROLES").is_none() {
|
if std::env::var_os("BASCULE_ROLES").is_none() {
|
||||||
let default_role = match id.auth_method.as_str() {
|
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",
|
"oidc-entra" | "oidc-cached" => "operator",
|
||||||
"kerberos" => "operator",
|
"kerberos" => "operator",
|
||||||
"ssh-key" => "apprentice",
|
"ssh-key" => "apprentice",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue