Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

Cloud SSH is one trusted single-node service with one SQLite database and one fixed deployment tenant. Configure it with CLI flags or the equivalent environment variables shown by cloud-ssh serve --help.

Before deployment, read the prominent Security Model: local room PTYs are not isolated from one another and all run as the service account.

Bootstrap

Create the tenant, default project, deployment owner, prod room, prod alias, and optionally an SSH key:

cargo run -p cloud-ssh-server --locked -- bootstrap \
  --ssh-public-key-file ~/.ssh/id_ed25519.pub

Bootstrap is transactional and idempotent for matching resources. The schema prevents removing or disabling the last active deployment owner.

Run the HTTP service:

cargo run -p cloud-ssh-server --locked -- serve

The default listeners and tenant are:

HTTP:      127.0.0.1:8080
Tenant:    default
SQLite:    sqlite://cloud-ssh.db?mode=rwc
Web build: web/dist

A persistent deployment should set:

DATABASE_URL='sqlite:///var/lib/cloud-ssh/cloud-ssh.db?mode=rwc'
CLOUD_SSH_TENANT_ID=default
CLOUD_SSH_PUBLIC_BASE_URL=https://cloud.example.com
CLOUD_SSH_SESSION_HMAC_KEY="$(openssl rand -hex 32)"

SQLite connections enable foreign keys and a busy timeout. File databases use WAL and synchronous=NORMAL.

Security-Critical Settings

CLOUD_SSH_SESSION_HMAC_KEY must contain at least 32 bytes of secret entropy. It protects opaque session, CSRF, and OAuth state digests. Store it in a secret manager and keep it stable across restarts. Changing it invalidates existing browser sessions and login attempts.

Browser cookies are Secure, the session cookie is HttpOnly, and both use SameSite=Lax. Session reads and idle-activity refresh use the CSRF-protected POST /api/auth/v1/session endpoint; a fresh session cookie is issued at login, not on every session read. Only local plain-HTTP development may set:

CLOUD_SSH_ALLOW_INSECURE_COOKIES=true

Do not enable that setting in production. Terminate TLS at a trusted reverse proxy, preserve the configured public origin, and set CLOUD_SSH_TRUST_PROXY_HEADERS=true only if untrusted clients cannot inject or bypass the proxy’s client-address headers. The proxy must impose connection limits and bounded header and request-body read timeouts before proxying HTTP or a WebSocket upgrade. Do not use a blanket post-upgrade timeout: an established interactive WebSocket is intentionally long-lived.

HTTP Routes

PathPurpose
/ and /assets/*production browser SPA
/api/auth/v1/*providers, session, logout, and current-user SSH keys
/api/room/v1/*authorized rooms, WebSocket attach, snapshots, and output catch-up
/api/admin/v1/*owner APIs for providers, allowlists, principals, owners, projects, memberships, rooms, and audit
/health, /healthzcredential-free process liveness
/readySQLite, migration, and session-crypto readiness
/metricsPrometheus metrics with independent bearer authorization

Unknown /api/* paths return JSON and never fall through to the SPA.

A deployment owner can update an existing pinned room with PUT /api/admin/v1/rooms/{room_id}/pinned-canonical-size and JSON {"canonical_rows": 40, "canonical_cols": 132}. The request requires the normal owner session and CSRF token. Controller-policy rooms are rejected. The response includes a monotonic canonical_size_revision; its audit record includes the previous and requested dimensions and revision.

For an inactive room, 200 OK means the desired size was durably stored and the next generation will start from it. For a live room, 200 OK is returned only after that generation’s PTY confirms the resize and the actor publishes the confirmed size. Per-room serialization covers both metadata writes and runtime acknowledgements, so concurrent owner requests cannot leave the PTY behind a newer durable row. If the live PTY rejects the resize, the endpoint returns 503; the desired size remains durable for a future generation and a failed runtime-sync audit event is recorded.

SSH Adapter

Generate and persist an operator-managed host key:

install -d -m 0700 /var/lib/cloud-ssh
ssh-keygen -t ed25519 -f /var/lib/cloud-ssh/ssh_host_ed25519 -N ''

Enable SSH:

CLOUD_SSH_SSH_ADDR=0.0.0.0:2222
CLOUD_SSH_SSH_HOST_KEY=/var/lib/cloud-ssh/ssh_host_ed25519

The host key must survive upgrades and container replacement. Never generate a new key on every service start.

list, default, and an empty transport username display a minimal authorized room list. Room attachment selectors are:

<alias>
<project>.<alias>
room:<room-id-or-slug>

Organization-qualified and machine-qualified selectors are unsupported.

Local Room Runtime

Trust boundary: LocalPtyRuntime is intended only for one trusted team. It starts every room process as the Cloud SSH service account, so rooms share that account’s filesystem and operating-system permissions. It is not a sandbox and is not an authorization boundary between mutually untrusted users or workloads. Do not enable public signup or run untrusted workloads with this runtime.

Select the trusted operator-controlled shell and idle lifetime:

CLOUD_SSH_ROOM_SHELL=/bin/bash
CLOUD_SSH_ROOM_IDLE_TTL_SECONDS=1800

The shell path is deployment configuration, not user input. Do not expose arbitrary executable paths or environment selection to users.

Bound process growth:

CLOUD_SSH_MAX_LIVE_ROOMS=128
CLOUD_SSH_MAX_CLIENTS_PER_ROOM=32
CLOUD_SSH_MAX_TOTAL_CLIENTS=512

The runtime also uses bounded actor mailboxes, input queues, journal queues, and per-client output queues. A client that cannot consume its bounded live stream is detached and must catch up from durable history.

Identity Providers

Provider client_secret_ref values refer to process environment secrets, for example env:CLOUD_SSH_GITHUB_CLIENT_SECRET. Raw secrets are not stored in SQLite or returned by APIs.

Configure GitHub OAuth App:

cargo run -p cloud-ssh-server --locked -- identity provider upsert-github \
  --client-id "$GITHUB_CLIENT_ID" \
  --client-secret-ref env:CLOUD_SSH_GITHUB_CLIENT_SECRET

Configure OIDC discovery:

cargo run -p cloud-ssh-server --locked -- identity provider upsert-oidc \
  --discovery-url https://idp.example.com/.well-known/openid-configuration \
  --client-id "$OIDC_CLIENT_ID" \
  --client-secret-ref env:CLOUD_SSH_OIDC_CLIENT_SECRET \
  --scopes "openid profile email"

Provider issuer and endpoint URLs must be absolute, contain no credentials or fragments, and use HTTPS except for localhost/loopback development fixtures. Outbound provider calls use a bounded timeout:

CLOUD_SSH_PROVIDER_HTTP_TIMEOUT_SECONDS=10

Admission modes are bootstrap, allowlist, and disabled. Add an active allowlist before admitting new external subjects:

cargo run -p cloud-ssh-server --locked -- identity allowlist upsert \
  --id rule-example-domain \
  --rule-type verified_email_domain \
  --value example.com \
  --project default \
  --role developer

GitHub supports verified_email, verified_email_domain, github_user_id, github_organization, and bootstrap_owner rules. OIDC supports verified_email, verified_email_domain, oidc_group, oidc_claim, and bootstrap_owner rules. Existing external identities remain bound by stable issuer/subject identity and cannot be moved across tenants.

Rate Limits

High-risk entrypoints use database-backed sliding windows so multiple adapters in the process share a budget:

CLOUD_SSH_RATE_LIMIT_WINDOW_SECONDS=60
CLOUD_SSH_LOGIN_RATE_LIMIT=20
CLOUD_SSH_CALLBACK_RATE_LIMIT=60
CLOUD_SSH_SSH_AUTH_RATE_LIMIT=120
CLOUD_SSH_ROOM_ATTACH_RATE_LIMIT=120
CLOUD_SSH_RATE_LIMIT_MAX_KEYS=100000

Choose limits for expected traffic and monitor rate_limited outcomes. The key ceiling bounds memory and database growth from rotating attacker identifiers.

Metrics And Tracing

Protect /metrics with at least 32 visible ASCII bytes:

CLOUD_SSH_METRICS_BEARER_TOKEN="$(openssl rand -hex 32)"

Only a network already protected by infrastructure controls should opt in to:

CLOUD_SSH_ALLOW_UNAUTHENTICATED_METRICS=true

Metrics use low-cardinality labels and expose room/client ceilings, live usage, auth outcomes, journal queue bytes, batch sizes, output segment bytes, write latency, durable lag, snapshot cadence, and commit failures.

Fmt logs remain enabled. Optional OTLP HTTP/protobuf traces are enabled by:

OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
RUST_LOG=cloud_ssh_server=info,cloud_ssh=info

An unavailable collector does not block startup or room operation.

Retention

Scheduled retention defaults to hourly:

CLOUD_SSH_RETENTION_INTERVAL_SECONDS=3600
CLOUD_SSH_RETAIN_ROOM_EVENTS=10000
CLOUD_SSH_RETAIN_ROOM_SNAPSHOTS=10
CLOUD_SSH_RETAIN_AUDIT_EVENTS=100000

CLOUD_SSH_RETAIN_ROOM_SNAPSHOTS must be at least 1. Retention preserves a valid snapshot-plus-segment catch-up anchor and removes output segments only when they are fully represented by the oldest retained snapshot. Set the interval to 0 only when a separate scheduler runs maintenance prune.

See Operations for manual prune, backup, restore, upgrade, and runtime cleanup procedures.

Docker

Build and smoke-test the exact production image:

make docker-build
make docker-smoke

Run with a persistent data volume and pre-provisioned SSH host key:

docker run --rm \
  --name cloud-ssh \
  -p 8080:8080 \
  -p 2222:2222 \
  -v cloud-ssh-data:/var/lib/cloud-ssh \
  --env-file .env \
  cloud-ssh:dev

The final image contains the Rust binary, CA certificates, and built SPA. It runs as UID 10001, contains no Node runtime, and stores mutable data only on the SQLite volume. The container healthcheck runs cloud-ssh healthcheck.