From 2fa92f8635853f6fc496dbe564995da3b1fe502465559e74f652f180c41bc587 Mon Sep 17 00:00:00 2001 From: Tyler King Date: Sun, 5 Apr 2026 10:53:08 -0400 Subject: [PATCH] docs: comprehensive documentation + developer experience polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New files: CONTRIBUTING.md — dev setup, code style, PR process CLAUDE.md — workspace context for Claude Code Makefile — build, test, lint, fmt, docker, helm-lint, dev, ci .editorconfig — consistent formatting rustfmt.toml — Rust formatting config docs/kubernetes.md — Helm install, values, architecture docs/bascule-shell.md — client shell install, config, TPM charts/bascule/README.md — Helm quick start Updated: README.md — accurate feature matrix, clear shipped vs planned config/bascule.example.toml — full reference (72 lines, all fields) All 15 README links verified valid. Helm lint clean. Build passes. 0 substrate deps. Co-Authored-By: Claude Opus 4.6 (1M context) --- .editorconfig | 21 ++++++ CLAUDE.md | 39 ++++++++++ CONTRIBUTING.md | 50 +++++++++++++ Makefile | 40 ++++++++++ README.md | 141 +++++++++++++++++++++--------------- charts/bascule/README.md | 37 ++++++++++ config/bascule.example.toml | 99 +++++++++++++++++++++---- docs/bascule-shell.md | 82 +++++++++++++++++++++ docs/kubernetes.md | 49 +++++++++++++ rustfmt.toml | 3 + 10 files changed, 487 insertions(+), 74 deletions(-) create mode 100644 .editorconfig create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 charts/bascule/README.md create mode 100644 docs/bascule-shell.md create mode 100644 docs/kubernetes.md create mode 100644 rustfmt.toml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..524e32a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.toml] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f9e0283 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,39 @@ +# CLAUDE.md — Context for Claude Code + +## What is this? + +Bascule is an identity-aware SSH proxy. It authenticates operators via SSH keys or AI agent tokens, then connects them to a shell, remote host, or ephemeral container. + +## Workspace + +- `crates/bascule-core/` — Library: SSH server, auth, session backends, hooks +- `crates/bascule-server/` — Binary: CLI wrapper, config loading, telemetry setup +- `crates/bascule-auth-agent-id/` — Optional: Entra Agent ID auth provider +- `crates/bascule-shell/` — Binary: Identity-aware login shell with TPM attestation +- `charts/bascule/` — Helm chart for K8s deployment +- `images/` — Curated container images for operator environments + +## Key traits + +- `AuthProvider` (auth.rs) — implement to add auth methods +- `SessionHandler` (hooks.rs) — implement to add session policy + +## Commands + +```bash +cargo build --all # Build everything +cargo test --all # Run tests +cargo clippy --all-targets # Lint +make ci # Full CI check +make dev # Run locally in dev mode +``` + +## Feature flags (bascule-server) + +- `agent-id` — Entra Agent ID auth + +## Rules + +- Zero substrate/chronicle/gsap dependencies +- No unwrap() in production code +- cargo fmt + cargo clippy must pass diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9b31786 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing to Bascule + +## Development Setup + +```bash +git clone https://github.com/your-org/bascule.git +cd bascule +cargo build --all +cargo test --all +``` + +## Architecture + +Bascule is a Rust workspace: + +| Crate | Purpose | +|-------|---------| +| `bascule-core` | Library — SSH server, auth, PTY, proxy, container, hooks | +| `bascule-server` | Binary — CLI, config, telemetry | +| `bascule-auth-agent-id` | Optional — Entra Agent ID auth | +| `bascule-shell` | Binary — Identity-aware login shell | + +## Testing + +```bash +cargo test --all +``` + +## Code Style + +- `cargo fmt` before committing +- `cargo clippy` must pass +- No `unwrap()` in production code +- All public items need doc comments + +## Pull Request Process + +1. Create a feature branch +2. Ensure `make ci` passes +3. Update docs if adding features +4. Submit PR against `main` + +## Commit Messages + +Format: `type: description` +Types: feat, fix, docs, chore, refactor, test + +## License + +By contributing, you agree your contributions are licensed under Apache 2.0. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..49ab13f --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +.PHONY: build build-full test lint fmt fmt-check clean docker docker-images helm-lint dev ci + +build: + cargo build --release --all + +build-full: + cargo build --release -p bascule-server --features agent-id + +test: + cargo test --all + +lint: + cargo clippy --all-targets -- -D warnings + +fmt: + cargo fmt --all + +fmt-check: + cargo fmt --all --check + +clean: + cargo clean + +docker: + docker build -t bascule:latest . + +docker-images: + docker build -t bascule-shell:minimal images/minimal/ + docker build -t bascule-shell:k8s-ops images/k8s-ops/ + docker build -t bascule-shell:net-ops images/net-ops/ + docker build -t bascule-shell:dev images/dev/ + +helm-lint: + helm lint charts/bascule/ + +dev: + RUST_LOG=debug cargo run -p bascule-server -- --config config/bascule.example.toml + +ci: fmt-check lint build test helm-lint + @echo "All checks passed" diff --git a/README.md b/README.md index 7370d3b..65c9c09 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Identity-aware SSH proxy for modern infrastructure. -**Bascule** is a lightweight SSH proxy that authenticates users via SSH keys or AI agent tokens, then connects them to a local shell, remote host, or ephemeral container. No agents. No control plane. One binary. +Bascule authenticates operators via SSH keys or AI agent tokens, then connects them to a local shell, remote host, or ephemeral container. No agents to install. No control plane. One binary. ## Quick Start @@ -13,78 +13,109 @@ cargo build --release -p bascule-server ssh -p 2222 localhost ``` -See [docs/quickstart.md](docs/quickstart.md) for Docker and container mode. - -## Session Modes - -| Mode | Config | Use case | -|------|--------|----------| -| **Local PTY** | (default) | Spawn a local shell | -| **Remote proxy** | `[proxy]` | Forward to a remote SSH host | -| **Container** | `[container]` | Ephemeral container per session | - -### Container mode - -Each SSH session spawns a fresh container. The image defines the toolset — if it's not in the image, the operator can't use it. - -```toml -[container] -image = "bascule-shell:k8s-ops" -ephemeral = true -hardened = true -memory_limit = "512m" -``` +See [docs/quickstart.md](docs/quickstart.md) for Docker, Helm, and container mode. ## Features -- **Three backends** — local PTY, remote SSH proxy, ephemeral containers -- **Identity-aware sessions** — every connection authenticated and attributed -- **SSH key authentication** — standard authorized_keys file -- **AI agent authentication** — Microsoft Entra Agent ID support (optional feature) -- **Session limiting** — configurable max concurrent sessions -- **Right-sized images** — curated container images (minimal, k8s-ops, net-ops, dev) -- **SessionHandler trait** — extend with custom policy, audit, or recording -- **Structured logging** — JSON format for production observability -- **Small footprint** — single binary, ~7MB, <64MB memory +### Session Backends + +| Mode | Config | Description | +|------|--------|-------------| +| Local PTY | (default) | Spawn a local shell process | +| Remote Proxy | `[proxy]` | Forward to a remote SSH host | +| Container | `[container]` | Ephemeral container per session (Docker/Podman/nerdctl) | +| Kubernetes | `[k8s]` | Shared jumphost with shell sidecar *(config ready, runtime coming)* | + +### Authentication + +- **SSH Keys** — standard OpenSSH authorized_keys files +- **Accept All** — development only, accepts any key +- **Entra Agent ID** — Microsoft AI agent identity (`--features agent-id`) +- **SPIFFE/SPIRE** — workload identity *(config ready, runtime coming)* + +### Security + +- Session limiting (semaphore-based `max_sessions`) +- Container hardening (`--cap-drop ALL`, `--security-opt no-new-privileges`) +- Container config validation (injection prevention) +- Read-only rootfs option +- NetworkPolicy for Kubernetes deployments + +### Observability + +- Structured JSON logging (`BASCULE_LOG_FORMAT=json`) +- Tracing spans on auth, session lifecycle, exec requests + +## Client: bascule-shell + +Identity-aware login shell with TPM attestation: + +```bash +./target/release/bascule-shell --info +``` + +``` +╔═══════════════════════════════════════════════════════╗ +║ Bascule Shell v0.1.0 ║ +║ Principal: tking ║ +║ Method: ssh-key ║ +║ TPM: available (6 PCRs verified) ║ +║ Platform: sha256:e9b95f002f54222d... ║ +╚═══════════════════════════════════════════════════════╝ +``` + +See [docs/bascule-shell.md](docs/bascule-shell.md). ## Comparison | | Bascule | Teleport | Boundary | |---|---|---|---| -| Agents | None | Required | Required | -| Control plane | None | Required | Required | -| License | Apache 2.0 | AGPL | MPL | -| Container sessions | Native | No | No | -| AI Agent Identity | Native | No | No | -| Auth | SSH keys, Entra Agent ID | OIDC, SAML, GitHub | OIDC, LDAP | +| License | Apache 2.0 | AGPL / Commercial | MPL / Commercial | +| Agents required | No | Yes | Yes | +| Control plane | No | Required | Required | +| Container sessions | Yes | No | No | +| AI Agent Identity | Yes (Entra Agent ID) | No | No | | Binary size | ~7MB | ~150MB | ~100MB | -See [docs/comparison.md](docs/comparison.md) for the full comparison. +See [docs/comparison.md](docs/comparison.md). + +## Deployment + +- **Standalone**: `cargo build --release -p bascule-server` +- **Docker**: `docker build -t bascule .` +- **Kubernetes**: `helm install bascule charts/bascule/` — see [docs/kubernetes.md](docs/kubernetes.md) ## Extending Bascule -Implement the `SessionHandler` trait to add custom behavior: +Implement `SessionHandler` to add custom policy: ```rust use bascule_core::hooks::{SessionHandler, SessionInfo}; -struct MyAuditHandler; +struct AuditHandler; #[async_trait] -impl SessionHandler for MyAuditHandler { - async fn on_session_start(&self, session: &SessionInfo) -> anyhow::Result<()> { - log::info!("{} connected from {}", session.principal, session.source_ip); - Ok(()) - } - - async fn on_exec(&self, session: &SessionInfo, command: &str) -> anyhow::Result<()> { - log::info!("{} executed: {}", session.principal, command); +impl SessionHandler for AuditHandler { + async fn on_session_start(&self, s: &SessionInfo) -> anyhow::Result<()> { + println!("{} connected from {}", s.principal, s.source_ip); Ok(()) } } ``` -Projects like [Guildhouse](https://guildhouse.dev) use the SessionHandler trait to add custom authorization, audit logging, and session governance. +See [docs/architecture.md](docs/architecture.md). + +## Roadmap + +Not yet implemented: + +- OIDC authentication (Keycloak, Entra, Okta) +- K8s API exec backend runtime +- SPIFFE/SPIRE auth runtime +- OpenTelemetry OTLP exporter +- Prometheus metrics endpoint +- Session recording +- Per-session Pod isolation ## Documentation @@ -93,17 +124,11 @@ Projects like [Guildhouse](https://guildhouse.dev) use the SessionHandler trait - [Authentication](docs/authentication.md) - [Architecture](docs/architecture.md) - [Observability](docs/observability.md) +- [Kubernetes](docs/kubernetes.md) +- [bascule-shell](docs/bascule-shell.md) - [Comparison](docs/comparison.md) - [Container Images](images/README.md) - -## Roadmap - -- [ ] OIDC authentication (Keycloak, Entra, Okta, Google) -- [ ] Certificate-based authentication -- [ ] OpenTelemetry OTLP trace export -- [ ] Prometheus metrics endpoint -- [ ] Session recording -- [ ] Web UI for session management +- [Contributing](CONTRIBUTING.md) ## License diff --git a/charts/bascule/README.md b/charts/bascule/README.md new file mode 100644 index 0000000..f5b53b4 --- /dev/null +++ b/charts/bascule/README.md @@ -0,0 +1,37 @@ +# Bascule Helm Chart + +Deploy Bascule SSH proxy on Kubernetes. + +## Install + +```bash +helm install bascule charts/bascule/ +``` + +## Connect + +```bash +ssh -p 2222 $(kubectl get svc bascule -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +## Common Configurations + +```bash +# NodePort +helm install bascule charts/bascule/ --set service.type=NodePort + +# SSH keys from Secret +kubectl create secret generic bascule-keys --from-file=authorized_keys=$HOME/.ssh/authorized_keys +helm install bascule charts/bascule/ --set auth.authorizedKeysSecret=bascule-keys + +# Custom shell image +helm install bascule charts/bascule/ --set shell.image.tag=net-ops +``` + +## Architecture + +Pod with two containers: +- `bascule` — SSH proxy (port 2222) +- `shell` — operator environment (exec'd into on connect) + +See [values.yaml](values.yaml) for all options. diff --git a/config/bascule.example.toml b/config/bascule.example.toml index c53725d..b89899e 100644 --- a/config/bascule.example.toml +++ b/config/bascule.example.toml @@ -1,23 +1,90 @@ -# Bascule SSH Proxy — Example Configuration +# ╔══════════════════════════════════════════════════════╗ +# ║ Bascule SSH Proxy — Configuration Reference ║ +# ╚══════════════════════════════════════════════════════╝ -# Listen address +# ─── Server ────────────────────────────────────────────── + +# Listen address (default: 0.0.0.0:2222) listen_addr = "0.0.0.0:2222" -# Host key (auto-generated if not present) -# host_key_path = "/etc/bascule/host_key" +# Path to host key. Auto-generated Ed25519 if not present. +# host_key_path = "/var/lib/bascule/host_key" -# Shell command to spawn for each session -# Default: /bin/bash -# shell_command = "/bin/bash" -# shell_command = "/usr/local/bin/custom-shell" +# Maximum concurrent sessions (default: 0 → 10000 internal cap) +# max_sessions = 100 -# Authentication -[auth] -mode = "accept-all" # "accept-all" (dev only), "authorized-keys" -# authorized_keys_path = "/etc/bascule/authorized_keys" - -# Session banner (optional) +# Banner shown after authentication # banner = "Welcome to Bascule." -# Max concurrent sessions (0 = unlimited) -# max_sessions = 100 +# ─── Shell (Local PTY mode — default backend) ──────────── + +# Shell command to spawn (default: /bin/bash) +# shell_command = "/bin/bash" +# shell_args = ["--login"] + +# ─── Authentication ────────────────────────────────────── + +[auth] +# Auth mode: "accept-all" (DEV ONLY), "authorized-keys" +mode = "accept-all" + +# For authorized-keys mode: +# authorized_keys_path = "/etc/bascule/keys" + +# ─── Entra Agent ID (optional, --features agent-id) ───── +# [auth.agent_id] +# tenant_id = "your-entra-tenant-id" +# audiences = ["api://bascule-proxy"] +# multi_tenant = false + +# ─── SPIFFE/SPIRE (config ready, runtime planned) ─────── +# [auth.spiffe] +# trust_domain = "example.com" +# trust_bundle_path = "/run/spire/bundle/bundle.pem" +# workload_api_socket = "/run/spire/agent/sockets/agent.sock" + +# ─── Remote Proxy Mode ────────────────────────────────── +# Uncomment to forward sessions to a remote SSH host. +# [proxy] +# target_host = "192.168.1.100" +# target_port = 22 +# target_user = "deploy" +# target_key_path = "/path/to/key" +# accept_target_host_key = false + +# ─── Container Mode ───────────────────────────────────── +# Uncomment to spawn ephemeral containers per session. +# [container] +# runtime = "auto" # auto | docker | podman | nerdctl +# image = "bascule-shell:k8s-ops" +# pull_policy = "if-not-present" # always | if-not-present | never +# ephemeral = true # destroy container on disconnect +# hardened = true # cap-drop ALL, no-new-privileges +# read_only_rootfs = false +# memory_limit = "512m" +# cpu_limit = "1.0" +# shell = "/bin/bash" +# user = "operator" +# network = "bridge" # bridge | none | host +# +# [[container.mounts]] +# source = "/home/user/.kube" +# target = "/home/operator/.kube" +# readonly = true + +# ─── Kubernetes Mode (config ready, runtime planned) ──── +# Auto-detected in-cluster via downward API. +# [k8s] +# enabled = true +# shell_container = "shell" +# shell = "/bin/bash" + +# ─── Telemetry ────────────────────────────────────────── +# [telemetry] +# otlp_endpoint = "http://localhost:4317" +# service_name = "bascule" + +# ─── Metrics (planned) ────────────────────────────────── +# [metrics] +# enabled = true +# port = 9090 diff --git a/docs/bascule-shell.md b/docs/bascule-shell.md new file mode 100644 index 0000000..6808f78 --- /dev/null +++ b/docs/bascule-shell.md @@ -0,0 +1,82 @@ +# bascule-shell + +Identity-aware login shell with TPM attestation. + +## What it does + +`bascule-shell` wraps your preferred shell (bash/zsh/fish) and: + +1. Detects your identity (Entra, Kerberos, SSH key) +2. Reads TPM PCR values and IMA measurements +3. Displays a banner with identity + attestation summary +4. Sets `BASCULE_*` environment variables +5. Execs into the inner shell + +Every SSH connection from inside the shell carries your identity and platform attestation. + +## Install + +```bash +cargo install --path crates/bascule-shell +# or +cargo build --release -p bascule-shell +cp target/release/bascule-shell /usr/local/bin/ +``` + +## Usage + +```bash +# Start the shell +bascule-shell + +# Show identity + attestation (dry run) +bascule-shell --info + +# JSON output +bascule-shell --info --json + +# Run a single command +bascule-shell --exec "env | grep BASCULE_" +``` + +## Configuration + +`~/.config/bascule/shell.toml`: + +```toml +inner_shell = "/bin/bash" +show_banner = true +pcr_indices = [0, 1, 2, 7, 10, 14] + +# Auto-configure SSH for Bascule servers +# [[servers]] +# alias = "jumphost" +# hostname = "bascule.example.com" +# port = 2222 +``` + +## Identity Detection + +Priority order: + +| Method | Source | When detected | +|--------|--------|---------------| +| Entra (WSL2) | `cmd.exe` interop | WSL2 with Entra-joined Windows | +| Azure CLI | `az account show` | `az` installed and logged in | +| Kerberos | `klist -s` | Valid TGT present | +| Cached OIDC | `~/.config/bascule/token.json` | Token file exists and not expired | +| System user | `$USER` | Always (fallback) | + +## Environment Variables + +After startup, the inner shell has: + +| Variable | Example | +|----------|---------| +| `BASCULE_PRINCIPAL` | `tking` | +| `BASCULE_AUTH_METHOD` | `ssh-key` | +| `BASCULE_ATTESTATION_HASH` | `sha256:e9b95f...` | +| `BASCULE_TPM_AVAILABLE` | `true` | +| `BASCULE_PCR_COUNT` | `6` | +| `BASCULE_IMA_COUNT` | `1247` | +| `BASCULE_PLATFORM_SUMMARY` | `tpm:6pcr,ima:1247` | diff --git a/docs/kubernetes.md b/docs/kubernetes.md new file mode 100644 index 0000000..6b4e3ad --- /dev/null +++ b/docs/kubernetes.md @@ -0,0 +1,49 @@ +# Kubernetes Deployment + +## Helm Install + +```bash +helm install bascule charts/bascule/ +``` + +### Common Options + +```bash +# NodePort access +helm install bascule charts/bascule/ --set service.type=NodePort + +# Authorized keys from a Secret +kubectl create secret generic bascule-keys --from-file=authorized_keys=$HOME/.ssh/authorized_keys +helm install bascule charts/bascule/ --set auth.authorizedKeysSecret=bascule-keys + +# Custom shell image +helm install bascule charts/bascule/ --set shell.image.tag=net-ops +``` + +## Architecture + +The chart deploys a Pod with two containers: + +- **bascule** — the SSH proxy (port 2222) +- **shell** — the operator environment (configured image, sleeps until exec'd) + +Operators SSH to Bascule. Bascule exec's into the shell container for each session. Multiple operators share the Pod with separate exec sessions. + +## Security Defaults + +- **NetworkPolicy**: egress restricted to DNS + K8s API +- **RBAC**: minimal Role (pods/exec in own namespace only) +- **SecurityContext**: no privilege escalation, cap-drop ALL on shell container +- **Host key**: persisted via volume (stable across restarts) + +## Values Reference + +See [values.yaml](../charts/bascule/values.yaml) for all options. + +| Key | Default | Description | +|-----|---------|-------------| +| `shell.image.tag` | `k8s-ops` | Shell image variant | +| `auth.mode` | `authorized-keys` | Auth mode | +| `service.type` | `LoadBalancer` | Service type | +| `maxSessions` | `100` | Max concurrent SSH sessions | +| `networkPolicy.enabled` | `true` | Enable network restrictions | diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..93a09de --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +edition = "2021" +max_width = 100 +use_small_heuristics = "Default"