docs: comprehensive documentation + developer experience polish

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) <noreply@anthropic.com>
This commit is contained in:
Tyler King 2026-04-05 10:53:08 -04:00
parent 9dc5cb9eee
commit 2fa92f8635
10 changed files with 487 additions and 74 deletions

21
.editorconfig Normal file
View file

@ -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

39
CLAUDE.md Normal file
View file

@ -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

50
CONTRIBUTING.md Normal file
View file

@ -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.

40
Makefile Normal file
View file

@ -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"

141
README.md
View file

@ -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

37
charts/bascule/README.md Normal file
View file

@ -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.

View file

@ -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

82
docs/bascule-shell.md Normal file
View file

@ -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` |

49
docs/kubernetes.md Normal file
View file

@ -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 |

3
rustfmt.toml Normal file
View file

@ -0,0 +1,3 @@
edition = "2021"
max_width = 100
use_small_heuristics = "Default"