bascule-workspace/bascule-agent/src/main.rs
Tyler J King 47a5484614 feat(bascule-agent): replace soft-mode attestation with ConfigMap posture reader
Replace hardcoded posture return in AttestationHandler (Shellstream
namespace 0x0005) with PostureReader that reads the posture-current
ConfigMap written by the substrate-operator's posture evaluator.

Data pipeline is now end-to-end:
  Keylime verifier -> posture evaluator -> ConfigMap -> bascule-agent

Behavior:
- posture_source='config': reads posture-current ConfigMap, maps
  level to PostureLevel, caches with configurable TTL (default 30s)
- posture_source='static' or dev_mode: returns configured static
  level and wire value (replaces hardcoded string for clarity)
- Graceful fallback: missing ConfigMap -> PostureLevel::Lockdown
  (fail-closed) + warning log

New dependencies: kube, k8s-openapi, governance-types (via path).
Does NOT add keylime-client — reads ConfigMap JSON directly.

Signed-off-by: Tyler King <tking@guildhouse.dev>
Signed-off-by: Tyler J King <tking727@gmail.com>
2026-04-15 10:17:00 -04:00

169 lines
5.4 KiB
Rust

//! bascule-agent — governed application sidecar.
//!
//! A single Rust process serving two interfaces over one governance engine:
//!
//! 1. **Governance IPC** — Unix socket at `/var/run/substrate/agent.sock`
//! Django middleware, Celery workers, and co-located apps talk Shellstream here.
//!
//! 2. **Interactive Shell** — SSH server on port 2222 (optional)
//! Bascule Gateway, `sb shell`, and Dashboard WebSocket connect here.
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Context;
use clap::Parser;
use tracing::{error, info};
mod command_filter;
mod config;
mod governance_server;
mod namespace;
mod posture_reader;
mod session_store;
mod shellstream;
mod ssh_server;
use governance_server::GovernanceServer;
use namespace::NamespaceRouter;
use session_store::SessionStore;
use shellstream::Namespace;
use ssh_server::AgentSSHServer;
#[derive(Parser)]
#[command(name = "bascule-agent", about = "Governed application sidecar")]
struct Cli {
/// Path to shell.toml configuration file.
#[arg(long, default_value = "/etc/substrate/shell.toml")]
config: PathBuf,
/// Run with default config (dev mode, no config file needed).
#[arg(long)]
dev: bool,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.json()
.init();
let cli = Cli::parse();
let config = if cli.dev {
info!("Starting in dev mode with default configuration");
config::AgentConfig::default_config()
} else {
config::AgentConfig::load(&cli.config)
.with_context(|| format!("Failed to load config from {:?}", cli.config))?
};
info!(
socket_path = %config.agent.socket_path,
ssh_enabled = config.agent.ssh.enabled,
namespace_backend = %config.agent.namespaces.backend,
"bascule-agent starting"
);
let dev_mode = config.agent.namespaces.backend == "soft";
let config = Arc::new(config);
// Build namespace router with all soft-mode handlers
let mut router = NamespaceRouter::new();
router.register(
Namespace::Crypto,
Arc::new(namespace::crypto::CryptoHandler::new()),
);
router.register(
Namespace::Identity,
Arc::new(namespace::identity::IdentityHandler::new(dev_mode)),
);
router.register(
Namespace::Secrets,
Arc::new(namespace::secrets::SecretsHandler::new()),
);
router.register(
Namespace::Governance,
Arc::new(namespace::governance::GovernanceHandler::new(dev_mode)),
);
let attestation_handler = if dev_mode
|| config.agent.namespaces.attestation.posture_source == "static"
{
info!(
level = %config.agent.namespaces.attestation.default_posture,
wire = config.agent.namespaces.attestation.static_posture_wire,
"Attestation handler: static mode"
);
namespace::attestation::AttestationHandler::new_static(
config.agent.namespaces.attestation.default_posture.clone(),
config.agent.namespaces.attestation.static_posture_wire,
)
} else {
info!(
configmap = %config.agent.namespaces.attestation.configmap_name,
namespace = %config.agent.namespaces.attestation.configmap_namespace,
cache_ttl = config.agent.namespaces.attestation.cache_ttl_secs,
"Attestation handler: ConfigMap mode"
);
let reader = posture_reader::PostureReader::new(
config.agent.namespaces.attestation.configmap_namespace.clone(),
config.agent.namespaces.attestation.configmap_name.clone(),
config.agent.namespaces.attestation.cache_ttl_secs,
);
namespace::attestation::AttestationHandler::new_configmap(Arc::new(reader))
};
router.register(Namespace::Attestation, Arc::new(attestation_handler));
router.register(
Namespace::Audit,
Arc::new(namespace::audit::AuditHandler::new()),
);
router.register(
Namespace::Network,
Arc::new(namespace::network::NetworkHandler::new()),
);
router.register(
Namespace::Intelligence,
Arc::new(namespace::intelligence::IntelligenceHandler::new()),
);
let router = Arc::new(router);
let sessions = Arc::new(SessionStore::new());
// Start governance IPC server
let server = GovernanceServer::new(config.clone(), router.clone(), sessions);
let ipc_handle = tokio::spawn(async move {
if let Err(e) = server.serve().await {
error!(error = %e, "Governance IPC server failed");
}
});
// Start SSH shell server (if enabled)
let ssh_handle = if config.agent.ssh.enabled {
let ssh_server = AgentSSHServer::new(config.clone(), router.clone())?;
Some(tokio::spawn(async move {
if let Err(e) = ssh_server.serve().await {
error!(error = %e, "SSH shell server failed");
}
}))
} else {
info!("SSH shell server disabled");
None
};
info!("bascule-agent ready");
// Wait for shutdown signal
tokio::signal::ctrl_c().await?;
info!("bascule-agent shutting down");
ipc_handle.abort();
if let Some(h) = ssh_handle {
h.abort();
}
Ok(())
}