Appearance
Security
How ServiceLabs protects callers, the people using the console, and the infrastructure behind it.
Two doors, two credentials
| Door | Who | Credential |
|---|---|---|
/v1 | Services | API key du_live_… as a bearer token |
/console/api | People | An httpOnly session cookie from password (+ TOTP) or Google sign-in |
The two never mix: an API key can't reach the console API, and a console session can't call /v1.
API keys
- A key is
du_live_+ 48 random hex characters (192 bits). Only its SHA-256 hash is stored; the full key is shown once, at creation. Lookup is by hash, so it's constant-time with respect to the secret. - Keys can be scoped to specific utilities (
403 forbiddenotherwise), expire, and be revoked instantly — the next request is401. - Each key has its own rate limit (default 600/min) and every call is attributed to it in jobs and usage.
- A key only ever sees the jobs it created —
GET /v1/jobs/{id}for another key's job is404, not403, so ids can't be probed.
Console sign-in
- Passwords are hashed with argon2id (19 MiB, t=2, p=1). Minimum length 10. Unknown emails take the same time as wrong passwords, so accounts can't be enumerated by timing.
- Two-factor (TOTP): 6-digit codes, ±30 s drift. The TOTP secret is stored AES-256-GCM encrypted under a key derived (HKDF) from
APP_SECRET. Admins can reset another member's two-factor. - Google sign-in (OIDC authorization-code flow): the id token's audience, issuer, expiry, nonce and
email_verifiedare checked. Only accounts onGOOGLE_ALLOWED_DOMAINS— or people holding an invite for that exact email — can get in. The OAuthstateis HMAC-signed and bound to a short-lived cookie. - Sessions are opaque random tokens, stored hashed, in an
httpOnly,Secure,SameSite=Laxcookie — never readable by JavaScript. They slide forSESSION_TTL_DAYS(default 14); changing your password signs out your other sessions; disabling a member signs them out everywhere. - CSRF: every state-changing console request must carry
X-DU-Console: 1. A cross-site form or image can't set a custom header, andSameSite=Laxstops the cookie riding along on cross-site POSTs. - Login throttling: 10 failures per email + IP, and 50 per email across all IPs, per 15 minutes. The second limit doesn't depend on the client IP at all, so spoofing forwarding headers can't buy unlimited guesses.
- Invites are single-use links valid for 7 days, stored hashed. Nothing is emailed; an admin copies the link.
Roles and isolation
| Role | Sees | Can |
|---|---|---|
admin | Everything | Everything, plus invite/disable members, change roles, reset 2FA, rotate the workspace signing secret. |
member | Everything | Run utilities, manage API keys, view all jobs and usage. |
guest (external partner) | Only their own keys, the jobs those keys (or they, in the playground) ran, and that usage | Create and revoke their own keys (at the default rate limit), run utilities. Not members, not the workspace signing secret, not other people's keys or jobs. |
At least one active admin always remains — the last one can't be demoted or disabled.
Callbacks
- Signed per key. Each API key has its own
whsec_…signing secret (Standard Webhooks). A partner holding one key can't produce valid signatures for another key's callbacks. Verify every callback — see Callbacks. - SSRF guard. Callback URLs must be
https://, may not embed credentials, and are DNS-resolved before every attempt: any address in a private, loopback, link-local, CGNAT, multicast or unique-local range (IPv4 and IPv6, including IPv4-mapped) is refused. The workers share a host with the datastores, so this matters. Redirects are not followed.
Input validation
Every input is validated against a strict schema before anything runs — unknown fields are rejected. Notably:
- Zip slip:
zip-filesentry names must be relative paths without.., so an archive can never write outside its folder when extracted. Forunzip-files, entry paths are normalised when stored, so a malicious archive can't escape its job's folder either. - Source URLs must be
http(s)://ors3://, and are fetched by the utility functions in the cloud — not from the platform host.
The path to the functions
- The gateway authenticates to the cloud with its own credentials, limited to invoking the five utility functions, reading their output prefixes, and reading one signing secret. It can't touch anything else.
- The functions still check an HS256 token themselves. The gateway mints a fresh token per call, valid only for that function's time limit plus 5 minutes, signed with that function's key. The functions log their incoming event, so these tokens appear in their logs — acceptable because each expires within minutes and is useless without the gateway's cloud credentials to invoke the function.
- Once all callers have moved over, the functions' old public HTTP endpoints can be removed, leaving the gateway as the only way in.
Data & retention
| Data | Kept |
|---|---|
| Jobs (input, result, timeline, client IP, user agent) | JOB_RETENTION_DAYS (default 90), then deleted |
| Usage rollups (counts + latency histograms) | Indefinitely — no inputs or outputs in them |
| Output files on R2 | Per the bucket's lifecycle rule (e.g. 30 days) |
| Expired sessions, stale invites | Cleaned up daily |
Output links are either public URLs on the files domain or presigned (7 days max) — see Files & storage. Anyone holding a link can download the file, so treat links like the files themselves.
Transport & headers
- TLS terminates at Cloudflare, then again at the host with a Cloudflare origin certificate (Full (strict)). The api listens on loopback only.
- Every response carries
X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENYand a restrictivePermissions-Policy. The console is served with a strict Content-Security-Policy (script-src 'self', no inline scripts).
Recommended hardening
The api trusts CF-Connecting-IP for client IPs (used in logs and login throttling). Restrict the origin's nginx vhost to Cloudflare's IP ranges (allow …; deny all;) so nobody can reach it directly and spoof that header.
Logging
Logs are JSON lines (one object per line). Each API request logs its request id, method, path, status and duration — never bodies, keys, cookies or signing secrets. Send X-Request-Id to correlate your logs with ours; 500 responses include the request id.
Secrets you manage
| Secret | Where | Rotation |
|---|---|---|
APP_SECRET | host .env | Rotating it signs everyone out and invalidates stored TOTP and callback signing secrets. |
| API keys | caller side | Create a new key, switch the caller, revoke the old one. |
| Callback signing secrets | per key (console → API keys → Signing secret) | Rotate, then update the receiver; old signatures stop verifying immediately. |
| Utility signing keys, cloud and R2 credentials | host .env | Update .env and redeploy. |