Appearance
Security model
Authentication
People: OpenID Connect, Authorization Code + PKCE, tokens held in memory by the SPA. The API validates the issuer, audience
keel-apiand a 60 s clock skew. Keel maps (issuer, subject) to a user and the user's memberships to tenants; a person in several tenants picks one withX-Keel-Tenant.Services: workspace API keys
keel_live_{prefix}_{secret}— stored as SHA-256, compared in constant time, shown once, scoped, optionally expiring, revoked everywhere within 60 s.MCP: API keys with
mcp:use, or OIDC bearer tokens with RFC 9728 protected-resource metadata.Your company's single sign-on: an Owner connects the organisation's own OIDC or SAML identity provider in Settings › SSO & SCIM. Keel reads the provider's discovery document or metadata itself (public
httpsaddresses only, no redirects) and registers it with Keel's sign-in service; the OIDC client secret is write-only. After a successful Test sign-in, the Owner can Require SSO for the verified email domains, with one break-glass Owner who keeps their usual sign-in. Everyone else on those domains must come through the company's provider.SCIM 2.0 provisioning at
/scim/v2/{organisation}with a token shown once (stored as a hash; the previous token keeps working for 24 hours after a new one is generated). Groups mapped to roles (Viewer to Admin, never Owner) give their members that role in every workspace; deactivating a person removes their access at once. A provisioned person is linked to their Keel account at sign-in only when the organisation verified their email domain and the sign-in proves the address — another organisation's provider can never claim your people.
Known gap
Data-residency routing is not built: a deployment runs in one region and trusts one sign-in service.
Tenant isolation
Tenant id from the token → EF Core global query filters → PostgreSQL row-level security on every tenant table (ENABLE + FORCE, set per connection, empty setting matches nothing) → a runtime role without BYPASSRLS. Cache keys are prefixed by tenant. Every aggregate has an integration test proving cross-tenant reads fail; cross-tenant access answers 404 and is audited only in the caller's tenant.
Secrets
Customer credentials, webhook URLs and signing secrets, and Git tokens go only through the secret store:
- local — envelope encryption in
keel.secret: a random AES-256-GCM data key per secret, wrapped with a per-tenant key derived by HKDF-SHA256 from the master key, tenant and secret id bound as associated data, under RLS. - keyvault — Azure Key Vault with a managed identity.
Secrets are write-only through the API and redacted from logs, traces and audit records.
Customer data
- Connectors are read-only with statement timeouts; profiling is bounded; sensitive columns are profiled without values.
- Failing-row samples keep key columns only. The query log and webhooks carry metadata only.
- Serving results pass through Keel and may be cached in Redis for the endpoint's TTL (default 5 minutes, up to 5,000 rows, per effective policy). Assistant result rows are never stored.
- LLM prompts carry metadata and statistics; values only with tenant opt-in and never for sensitive columns.
Support access
Anchor staff never see a tenant's data by default. Platform operators are listed by IdP subject in the deployment's configuration (optionally requiring a hardware-key sign-in) and hold no tenant role. An operator can open a support session in a tenant only while that tenant's Owner has granted time-boxed consent (Settings › Security › Support access, 1 hour to 7 days, with a reason):
- a session lasts at most an hour and never beyond the consent; revoking consent ends it at once;
- it is read-only: a Viewer's view of the catalog and the audit log — no serving queries, and every change is refused both at the HTTP edge and in the application pipeline;
- the app shows a banner throughout;
- consent, session start and end, and every request the session made are in the tenant's own audit log with the operator as the actor.
See Operations.
Offboarding and deletion
Offboarding deletes every row of the tenant in every schema (tables discovered from the database catalog), its secrets and cache entries, and verifies that nothing is left. The tenant's audit chain is deleted with it; a tombstone with row counts stays in the platform operator log. Backups age out on their own schedule. See Operations.
Product analytics
Keel records how the product is used — screens viewed, scans started and finished, proposals accepted or rejected (with a confidence band), commits, merges, certifications, exports, assistant questions asked, answered or declined, the onboarding funnel and error codes shown — to decide what to build. It never records your data:
- Events carry a hashed tenant id and user id (HMAC-SHA256 with a server key), the role, plan, region and app version. Never table or column names, values, SQL, prompts, questions, answers, emails or free text.
- Every event is checked against a closed catalog on the server: unknown events are dropped, and a property is kept only if it is declared for the event and its value is one of a fixed set of words, a bounded number, a boolean or a route id made of known route words. An automated test seeds a schema with distinctive names, exercises the product and fails if any of them reaches an event.
- The browser sends its events to Keel (
POST /api/v1/telemetry/events), never to the analytics provider. Support sessions send none. - The provider is Anchor's choice; the default sends nothing. EU tenants' events go to the provider's EU endpoint.
- An Owner can opt the tenant out (Settings › Privacy › Product analytics); from then on the tenant emits nothing, on every instance within a minute.
The Migrate write plane
Keel is read-only by default. The one exception is the licensed Keel Migrate module's write plane (ADR-0021). Mapping, code generation and reconciliation use read-only connections like everything else. Only a Migrate run writes to customer systems, and only under these conditions:
- The run executes on the private agent inside the customer network.
- It uses a separate write credential that the agent holds. It must differ from every read credential, and the agent's scope check must accept it: a credential that can write outside the target schemas and
keel_stageis refused. - It executes an approved plan version. People with a per-migration
migrate:executegrant start runs. Switching serving and rolling back need a Steward or Owner, and optionally a second one (four-eyes).
Rows move source → target inside the network. Keel receives counts, timings, lag, checksums and at most 50 business keys, and never row values. The agent's egress is pinned to the control-plane origin it enrolled with. Every run step and cutover action is audited. Setting up change capture on a source (change tracking, CDC, a replication slot and publication) is the only change Keel makes to a source, and a DBA can do it instead. See Run and cut over.
Network
SSRF guards on source hosts (Direct mode), cloud provider host allow-lists, notification webhooks and Git hosts: https where applicable, private / loopback / link-local / metadata addresses refused on save and re-checked on connect, no redirects, bounded timeouts.
Audit
Every change to governed objects is written — atomically with the change, through the outbox — to a per-tenant, hash-chained, append-only log (updates and deletes are rejected by a trigger). A nightly job (02:00 UTC) and Verify now recompute the chain and detect modified, missing, reordered, inserted and rewritten rows; tampering notifies Owners. Export as CSV or JSON (the export is itself audited).
Follow-ups
Incremental verification and anchoring the chain in external WORM storage (against a database superuser who rewrites both chain and checkpoints) are planned.