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
| Path | Purpose |
|---|---|
/ 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, /healthz | credential-free process liveness |
/ready | SQLite, migration, and session-crypto readiness |
/metrics | Prometheus 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:
LocalPtyRuntimeis 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.