# Architecture ## Overview Bascule is a single-binary SSH proxy with a built-in management API: ``` ┌─────────────────────────────────────┐ Operator workstation │ bascule (single binary) │ ┌───────────────┐ │ │ │ bascule-shell │ SSH │ Port 2222: SSH Proxy (russh) │ │ identity + │─────▶│ ├── AuthProvider │ │ TPM attest │ │ ├── SessionHandler hooks │ └───────────────┘ │ └── SessionBackend │ │ ├── Local PTY │ Browser / curl │ ├── Remote Proxy │ ┌───────────────┐ HTTP │ └── Container │ │ Dashboard │─────▶│ │ │ /api/* │ │ Port 9090: Management API (axum) │ └───────────────┘ │ ├── /api/sessions │ │ ├── /api/stats │ │ ├── /api/health │ │ └── /dashboard (WASM, planned) │ │ │ │ Arc shared │ └─────────────────────────────────────┘ ``` ## Session Backends ### Local PTY (default) Spawns a process on this machine via `portable-pty`. The process gets a real PTY with terminal emulation, resize support, and environment variables. ### Remote Proxy Opens a second SSH connection to a target host via `russh` client. Bridges I/O between the client and upstream channels. The client sees a transparent connection to the target host, but Bascule mediates authentication and applies SessionHandler hooks. ### Container Spawns an ephemeral container per session using docker, podman, or nerdctl. The container is destroyed on disconnect. The container image defines what tools are available — if it's not in the image, the operator can't use it. ## Extension Model ### AuthProvider trait Implement to add custom authentication: ```rust #[async_trait] pub trait AuthProvider: Send + Sync + 'static { async fn check_public_key(&self, user: &str, key: &PublicKey) -> bool; async fn check_password(&self, user: &str, password: &str) -> bool; fn principal_for_user(&self, user: &str) -> String; } ``` ### SessionHandler trait Implement to add policy, audit, or custom behavior: ```rust #[async_trait] pub trait SessionHandler: Send + Sync + 'static { async fn on_session_start(&self, session: &SessionInfo) -> anyhow::Result<()>; async fn build_session_env(&self, session: &SessionInfo) -> HashMap; async fn on_exec(&self, session: &SessionInfo, command: &str) -> anyhow::Result<()>; async fn on_session_end(&self, session: &SessionInfo) -> anyhow::Result<()>; fn display_name(&self, session: &SessionInfo) -> String; } ``` All methods have default implementations (accept/passthrough). Override only what you need. ## Security Model ### Container hardening (default) When `hardened = true`: - `--security-opt no-new-privileges` - `--cap-drop ALL` - `--cap-add SETUID --cap-add SETGID` (minimal for shell) ### Ephemeral sessions When `ephemeral = true` (default), the container is `--rm` and destroyed on disconnect. Nothing persists between sessions. ### Network isolation Set `network = "none"` to completely isolate the container from the network. The operator can run local tools but can't reach external services. ## Management API The management API runs alongside the SSH proxy in the same process. Both share an `Arc` — when a session starts via SSH, the store updates; when the dashboard polls `/api/sessions`, it reads the same store. Zero serialization, zero IPC. Enable via `[dashboard]` config section or `--features dashboard` (default on). ## Crate Structure | Crate | Purpose | |-------|---------| | `bascule-core` | Library — server, handler, auth, PTY, proxy, container, hooks, store | | `bascule-server` | Binary — CLI, config, tracing, management API | | `bascule-auth-agent-id` | Optional — Entra Agent ID authentication | | `bascule-shell` | Binary — Identity-aware login shell with TPM | | `bascule-dashboard` | Library — Dioxus UI components | | `bascule-dashboard-web` | Binary — WASM web dashboard target |