Security Model
SAM’s security model separates platform secrets (managed by operators) from user credentials (encrypted per-user in the database).
Credential Types
Section titled “Credential Types”Platform Secrets
Section titled “Platform Secrets”These are Cloudflare Worker secrets set during deployment:
| Secret | Purpose |
|---|---|
ENCRYPTION_KEY | AES-256-GCM key for encrypting user credentials |
JWT_PRIVATE_KEY | RSA-2048 key for signing workspace and callback tokens |
JWT_PUBLIC_KEY | RSA-2048 key for token verification (exposed via JWKS) |
CF_API_TOKEN | Cloudflare DNS and API access |
GITHUB_CLIENT_ID/SECRET | OAuth 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 Credentials
Section titled “User Credentials”User-provided secrets stored encrypted in D1:
| Credential | Purpose | Encryption |
|---|---|---|
| Hetzner API token | VM provisioning | AES-256-GCM, per-credential IV |
| Agent API keys | Claude/OpenAI API access | AES-256-GCM, per-credential IV |
| Agent OAuth tokens | Claude Pro/Max subscriptions | AES-256-GCM, per-credential IV |
User credentials are never stored as environment variables or Worker secrets.
Authentication Flow
Section titled “Authentication Flow”SAM uses BetterAuth with GitHub OAuth for user authentication:
- User clicks “Sign in with GitHub”
- API redirects to GitHub OAuth
- GitHub returns authorization code
- API exchanges code for access token
- API fetches user profile and primary email
- BetterAuth creates/updates user record and session
- Session cookie set in browser
Token Types
Section titled “Token Types”| Token | Lifetime | Purpose | Validated By |
|---|---|---|---|
| Session cookie | Hours | Browser authentication | API Worker (BetterAuth) |
| Workspace JWT | Minutes | Terminal WebSocket auth | VM Agent (via JWKS) |
| Bootstrap token | 5 minutes | One-time VM credential injection | API Worker |
| Callback token | Minutes | VM Agent → API callbacks | API Worker |
Credential Encryption
Section titled “Credential Encryption”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 Authentication
Section titled “Terminal Authentication”Terminal WebSocket connections use short-lived JWTs:
- Browser requests a terminal token:
POST /api/terminal/token - API signs a JWT with the workspace ID and user ID
- Browser connects:
wss://ws-{id}.domain/workspaces/{id}/shell?token=... - Worker proxies the WebSocket to the VM Agent
- VM Agent validates the JWT against the API’s JWKS endpoint (
/.well-known/jwks.json)
Bootstrap Security
Section titled “Bootstrap Security”When a new VM starts, it needs credentials (callback URL, node ID) but no secrets are embedded in cloud-init:
- API creates a one-time bootstrap token (cryptographically random, 5-minute expiry)
- Cloud-init script includes only the token and API URL
- VM Agent redeems the token:
POST /api/bootstrap/{token} - API returns the full configuration (callback URL, node ID, etc.)
- Token is invalidated after use
Security Best Practices
Section titled “Security Best Practices”- 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