Skip to content

Security Model

SAM’s security model separates platform secrets (managed by operators) from user credentials (encrypted per-user in the database).

These are Cloudflare Worker secrets set during deployment:

SecretPurpose
ENCRYPTION_KEYAES-256-GCM key for encrypting user credentials
JWT_PRIVATE_KEYRSA-2048 key for signing workspace and callback tokens
JWT_PUBLIC_KEYRSA-2048 key for token verification (exposed via JWKS)
CF_API_TOKENCloudflare DNS and API access
GITHUB_CLIENT_ID/SECRETOAuth authentication
GITHUB_APP_*GitHub App for repository access

Platform secrets are automatically generated and persisted by Pulumi on first deployment. They never appear in source control.

User-provided secrets stored encrypted in D1:

CredentialPurposeEncryption
Hetzner API tokenVM provisioningAES-256-GCM, per-credential IV
Agent API keysClaude/OpenAI API accessAES-256-GCM, per-credential IV
Agent OAuth tokensClaude Pro/Max subscriptionsAES-256-GCM, per-credential IV

User credentials are never stored as environment variables or Worker secrets.

SAM uses BetterAuth with GitHub OAuth for user authentication:

  1. User clicks “Sign in with GitHub”
  2. API redirects to GitHub OAuth
  3. GitHub returns authorization code
  4. API exchanges code for access token
  5. API fetches user profile and primary email
  6. BetterAuth creates/updates user record and session
  7. Session cookie set in browser
TokenLifetimePurposeValidated By
Session cookieHoursBrowser authenticationAPI Worker (BetterAuth)
Workspace JWTMinutesTerminal WebSocket authVM Agent (via JWKS)
Bootstrap token5 minutesOne-time VM credential injectionAPI Worker
Callback tokenMinutesVM Agent → API callbacksAPI Worker

User credentials are encrypted at rest using AES-256-GCM:

Encrypt: plaintext + ENCRYPTION_KEY → { ciphertext, iv } (stored in D1)
Decrypt: { ciphertext, iv } + ENCRYPTION_KEY → plaintext (on-demand)

Each credential gets a random initialization vector (IV), ensuring identical plaintext values produce different ciphertext.

Terminal WebSocket connections use short-lived JWTs:

  1. Browser requests a terminal token: POST /api/terminal/token
  2. API signs a JWT with the workspace ID and user ID
  3. Browser connects: wss://ws-{id}.domain/workspaces/{id}/shell?token=...
  4. Worker proxies the WebSocket to the VM Agent
  5. VM Agent validates the JWT against the API’s JWKS endpoint (/.well-known/jwks.json)

When a new VM starts, it needs credentials (callback URL, node ID) but no secrets are embedded in cloud-init:

  1. API creates a one-time bootstrap token (cryptographically random, 5-minute expiry)
  2. Cloud-init script includes only the token and API URL
  3. VM Agent redeems the token: POST /api/bootstrap/{token}
  4. API returns the full configuration (callback URL, node ID, etc.)
  5. Token is invalidated after use
  • Rotate keys quarterly — regenerate JWT and encryption keys
  • Minimal GitHub App permissions — only Contents (read/write), Metadata (read-only), and Email addresses (read-only)
  • HTTPS everywhere — all traffic encrypted via Cloudflare
  • Session isolation — each workspace JWT is scoped to a specific workspace ID
  • No shared cloud credentials — BYOC model means the platform has no Hetzner access