Observability: Structured JSON logging via BASCULE_LOG_FORMAT=json Tracing spans on auth (method, principal, peer) Tracing spans on session lifecycle (id, principal, backend, source_ip) Tracing spans on exec requests (session_id, command) Config: [telemetry] and [metrics] sections (OTel export planned) Documentation (8 files, 489 lines): docs/quickstart.md — three-path getting started docs/configuration.md — full config reference with examples docs/authentication.md — all auth modes with setup guides docs/architecture.md — backends, traits, extension model, security docs/observability.md — logging, tracing, metrics docs/comparison.md — vs Teleport, Boundary, StrongDM images/README.md — curated image catalog README.md — features, comparison, quickstart, extension example 1557 lines Rust, 489 lines docs, 0 substrate deps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3.1 KiB
Architecture
Overview
Bascule is a single-binary SSH proxy with three pluggable layers:
Client SSH → Bascule Server
│
├── AuthProvider (SSH keys / OIDC / Agent ID)
│
├── SessionHandler hooks
│ on_session_start → build_session_env → on_exec → on_session_end
│
└── SessionBackend
├── LocalPty (portable-pty → /bin/bash)
├── RemoteProxy (upstream SSH → target host)
└── Container (docker/podman → ephemeral image)
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:
#[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:
#[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<String, String>;
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.
Crate Structure
| Crate | Purpose |
|---|---|
bascule-core |
Library — server, handler, auth, PTY, proxy, container, hooks |
bascule-server |
Binary — CLI, config loading, tracing setup |
bascule-auth-agent-id |
Optional — Entra Agent ID authentication |