Security overview
Tenant isolation, egress control, secret handling and how runners are trusted.
This is a summary for evaluating qaitai. Report vulnerabilities to security@tryqaitai.com.
Tenant isolation
Every table with a workspace id has forced row-level security. Queries run inside a transaction
that sets the workspace; outside it, tenant tables return nothing and reject writes. Cross-workspace
lookups that have to start without a workspace (GitHub webhooks, cron) use a read-only transaction with
a SELECT-only policy. The app connects as a role with no superuser, no BYPASSRLS and no table
ownership; migrations run separately as the owner. CI checks that every tenant table is covered.
Secrets never enter the model's context
Test credentials are referenced by name (@secrets/login). The runner decrypts only the secrets the
prompt references; fill_secret types the value into the page without returning it. Values are
redacted from recorded thoughts, accessibility snapshots and page URLs. The system prompt treats all page
content as untrusted (a prompt-injection defense).
Encryption
BYOK keys, test secrets and stored GitHub App credentials use envelope encryption: AES-256-GCM with a
fresh data key per value, wrapped by a master key (QAITAI_MASTER_KEY when self-hosted, AWS KMS on
qaitai Cloud). The workspace, table and name are bound as authenticated data, so a ciphertext copied
into another workspace fails to decrypt.
What the browser can reach
Cloud runs may only target hosts the workspace has verified (DNS TXT record, well-known file, or a preview deployment from a connected repo); verification is re-checked daily. In the browser, an egress guard blocks top-level navigation off verified hosts, every request to private ranges (RFC 1918, loopback, link-local including cloud metadata, CGNAT, IPv6 ULA and link-local) and non-http(s) schemes. Self-hosted runs, on your own runner, may reach private networks.
Runners
- Remote runners (yours) never listen on a port. Their only credential is a runner token (shown once, stored as a SHA-256 hash, compared in constant time). They have no database access, no master key and no queue token, and only see queued jobs of their own workspace. Each claimed job gets a short-lived token bound to that job, runner and workspace.
- The managed model key never leaves the control plane: runs on your runner use your key.
- Request bodies are size-capped and validated; usage minutes come from the control plane's own clock.
- qaitai Cloud workers run each job on its own short-lived machine, destroyed on exit, with no inbound connections and only a per-job token.
GitHub App
Read-only access to contents, pull requests, deployments, statuses and metadata; write access to checks only. Signing in grants no code access. Webhooks are verified with the App's webhook secret in constant time.
Accounts and audit
Security-relevant actions (runs created, runner tokens issued, domain verification changes, GitHub App setup) are written to an audit log. Self-hosted sign-up is invite-only by default, for every sign-in method.
Email + password accounts: passwords of 10–128 characters, hashed with scrypt; on qaitai Cloud (optional when self-hosting) passwords found in Have I Been Pwned are rejected, sending only a 5-character hash prefix. An email must be confirmed before a password sign-in whenever the instance can send mail (always on qaitai Cloud). Sign-in, sign-up and reset answer the same whether or not an account exists, and are rate-limited per client IP across all web instances. Accounts link to GitHub or Google only when both sides have a verified email.
Known limits
We keep a threat model with open findings and fix them in the open. Current known limits include per-process rate limits (use a shared limiter at your edge for strict global limits) and no network-level egress allowlist on qaitai Cloud worker machines (enforcement is the in-browser guard plus Chromium's sandbox).