From 1a54cc387702b4fa99a7f2f59c810bce3c1dda182b960395f3f74cdf21b51cfa Mon Sep 17 00:00:00 2001 From: Tyler J King Date: Wed, 15 Apr 2026 10:37:30 -0400 Subject: [PATCH] feat(bascule-gateway): derive ShellClass at ceremony grant from posture Read the cluster's operational posture level from the posture-current ConfigMap at ceremony grant time. Derive ShellClass via derive_shell_class() and stamp into the granted SessionScope. - Normal posture (5) -> ShellClass::System - Any DEFCON escalation -> ShellClass::Application - Fail-closed: missing ConfigMap -> Lockdown -> Application - posture_level_at_establishment stored for audit/breach comparison Signed-off-by: Tyler King Signed-off-by: Tyler J King --- bascule-gateway/Cargo.toml | 3 ++ bascule-gateway/src/server.rs | 62 +++++++++++++++++++++++++- bascule-gateway/src/session_manager.rs | 2 + 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/bascule-gateway/Cargo.toml b/bascule-gateway/Cargo.toml index 13dc852..852d2e6 100644 --- a/bascule-gateway/Cargo.toml +++ b/bascule-gateway/Cargo.toml @@ -18,6 +18,9 @@ accord-core = { path = "../../guildhouse/services/accord-core" } accord-opa = { path = "../../guildhouse/services/accord-opa" } qm-core = { path = "../../guildhouse/services/qm-core" } +# Cross-workspace path dep — substrate governance types (for PostureLevel). +governance-types = { path = "../../substrate/crates/governance-types" } + # Kubernetes kube = { workspace = true } k8s-openapi = { workspace = true } diff --git a/bascule-gateway/src/server.rs b/bascule-gateway/src/server.rs index be6a2b4..8856f3c 100644 --- a/bascule-gateway/src/server.rs +++ b/bascule-gateway/src/server.rs @@ -98,7 +98,23 @@ impl bascule_proto::bascule_v1::bascule_gateway_server::BasculeGateway for Bascu }; match response { - CeremonyResponse::Granted(grant) => { + CeremonyResponse::Granted(mut grant) => { + // Derive ShellClass from the cluster's current posture level. + // Reads posture-current ConfigMap, maps level to PostureLevel, + // derives ShellClass, and stamps into the granted scope. + let posture_level = read_posture_level().await; + let shell_class = bascule_core::derive_shell_class(posture_level, None); + grant.granted_scope.shell_class = shell_class; + grant.granted_scope.posture_level_at_establishment = + Some(posture_level.to_wire()); + + tracing::info!( + ceremony_id = %grant.ceremony_id, + posture_level = ?posture_level, + shell_class = %shell_class, + "Session shell class derived at ceremony grant" + ); + let session = self .session_manager .create_session(&grant) @@ -403,6 +419,10 @@ fn proto_scope_to_core(proto: &bascule_proto::bascule_v1::SessionScope) -> Sessi pathways: proto.pathways.iter().map(|p| parse_pathway(p)).collect(), mutation_budget: proto.mutation_budget, can_delegate: proto.can_delegate, + // ShellClass is server-derived from posture, not client-requested. + // Set to default here; stamped by the ceremony grant path. + shell_class: bascule_core::ShellClass::default(), + posture_level_at_establishment: None, } } @@ -497,3 +517,43 @@ fn core_global_to_proto(core: &GlobalScope) -> bascule_proto::bascule_v1::Global can_view_topology: core.can_view_topology, } } + +// --- Posture level reader --- + +/// Read the cluster's current operational posture level from the +/// `posture-current` ConfigMap. Falls back to `PostureLevel::Lockdown` +/// (fail-closed) if the ConfigMap is missing or unreadable. +async fn read_posture_level() -> governance_types::PostureLevel { + use governance_types::PostureLevel; + + let client = match kube::Client::try_default().await { + Ok(c) => c, + Err(e) => { + tracing::warn!(error = %e, "kube client init failed for posture read"); + return PostureLevel::Lockdown; + } + }; + + let namespace = std::env::var("ENFORCEMENT_NAMESPACE") + .unwrap_or_else(|_| "guildhouse-infra".into()); + + use k8s_openapi::api::core::v1::ConfigMap; + use kube::api::Api; + + let api: Api = Api::namespaced(client, &namespace); + match api.get("posture-current").await { + Ok(cm) => { + let level_u8: u8 = cm + .data + .as_ref() + .and_then(|d| d.get("level")) + .and_then(|v| v.parse().ok()) + .unwrap_or(1); + PostureLevel::from_wire(level_u8).unwrap_or(PostureLevel::Lockdown) + } + Err(e) => { + tracing::warn!(error = %e, "posture-current ConfigMap read failed"); + PostureLevel::Lockdown + } + } +} diff --git a/bascule-gateway/src/session_manager.rs b/bascule-gateway/src/session_manager.rs index 3df67c4..cf0a101 100644 --- a/bascule-gateway/src/session_manager.rs +++ b/bascule-gateway/src/session_manager.rs @@ -221,6 +221,8 @@ impl SessionManager { pathways: vec![ChangePathway::DryRunOnly], mutation_budget: Some(0), can_delegate: false, + shell_class: bascule_core::ShellClass::default(), + posture_level_at_establishment: None, } } }