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 deploy, DNS, observability, and AI Gateway operations |
GITHUB_CLIENT_ID/SECRET | OAuth authentication |
GITHUB_APP_* | GitHub App for repository access |
GITHUB_WEBHOOK_SECRET | GitHub App webhook HMAC verification; set from GitHub Actions secret GH_WEBHOOK_SECRET |
Security keys and Origin CA credentials are automatically generated and persisted by Pulumi on first deployment. Cloudflare and GitHub secrets are external inputs supplied through GitHub Actions and mapped into Worker secrets by the deploy scripts. 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