Security and compliance
Tenancy, authentication, residency, keys, retention.
This page is written for the person who has to sign off on Beetl, not for the person who has to use it. It states what the platform enforces today and what it does not. Where a control is missing, it says so.
Tenancy and isolation
A tenant is the hard boundary. Isolation is enforced server-side and derived from the caller's credential. No caller-supplied tenant identifier is trusted anywhere in the current paths.
Three layers carry it:
- Object storage. Every tenant brings its own object store, registered by an admin and recorded per dataset. There is no shared platform bucket. Storage paths are namespaced
{tenant_id}/{tier}/{data_set_id}/v{version}inside that store. - Analytics. Each tenant gets its own Superset database connection, provisioned at startup. Guest tokens for an embedded dashboard are minted only after checking that the dashboard's datasets belong to that tenant's connection.
- SQL access. The Arrow Flight SQL interface takes tenant scope from the credential. A tenant sees one catalog, its own slug as the only schema, and only its own active datasets.
Two residual risks are accepted and documented rather than fixed. Superset runs as a shared instance with a shared metadata database, cache and admin accounts, so a full Superset compromise reaches every tenant's stored credential. Dashboard membership is checked when a guest token is minted and not re-checked afterwards, which leaves a window bounded by the token lifetime.
Authentication
Sign-in is Zitadel OIDC using the PKCE authorization-code flow, with end_session logout. Organisations and users are provisioned against a Zitadel project, and roles are set there rather than in Beetl.
There is no local password anywhere in the product. The previous local password path and its Argon2 hashing were deleted, and the login page is a single button that redirects to Zitadel. Deactivation and reactivation happen in Zitadel and take effect against Beetl immediately.
Browser sessions use a __Host-beetl-session cookie with Secure, HttpOnly and SameSite=Strict, plus an X-CSRF-Token header on every state-changing call.
Roles, honestly
There are two roles, TenantAdmin and TenantUser.
Object-store management in Settings is the only role gate in the entire server. Creating, updating and deleting an object store requires TenantAdmin and returns 403 TENANT_ADMIN_REQUIRED otherwise. Reading the list of registered stores was deliberately downgraded to any authenticated user.
Everything else is ungated by role. Automations, pipelines, connections, datasets, API keys and MCP key scopes carry no role check, so any authenticated user in the tenant can create and delete them. The endpoint that changes a user's role is itself ungated. If your control model depends on separating who can build from who can read, Beetl does not enforce that split today, and you should scope tenant membership accordingly.
The one authorization axis that does exist below the role level is the API key scope, described below.
Data residency and encryption
Hosting is in the EU, on Hetzner. Superset runs as a separate service alongside the platform.
In transit, public traffic terminates TLS at the ingress with HSTS enabled. The Flight SQL port has no public ingress at all and is restricted by network policy to Superset pods.
At rest, tenant data lives in the tenant's own object store, so encryption at rest for the data itself is a property of the store you register. Beetl does not add a second encryption layer over it.
Platform secrets sit behind a SecretsBackend abstraction with two implementations. PostgresSecrets uses AES-256-GCM with a fresh 12-byte nonce per write and the secret path bound as additional authenticated data, so ciphertext cannot be replayed across paths, with a retired-key map for rotation. OkmsSecrets is the OVHcloud KMS client. Production pins the OKMS backend for its EU audit-log requirement. The decision record for this (ADR-0028) is still marked Proposed while the code is live.
Connection credentials are not yet covered
The secrets backend currently has exactly one caller: object-store credentials. Credentials for data-source connections still travel as plain values and remain unencrypted in the event store. The cipher exists and has not been pointed at them yet.
Model inference runs on Google gemini-3.5-flash. Anything the assistant or an Automation reads while answering, query results included, is sent to that model. A formal sub-processor list and DPA are not part of this documentation.
API keys and their limits
API keys are per user and per tenant. They are created in Settings under the User tab, shown once in a one-time reveal dialog, and stored as a hash. Delete is revoke. They carry a scope of read or write, checked at the tool-dispatch choke point rather than in a per-surface wrapper.
State every limit plainly, because each one is a question a reviewer will ask:
| Control | Status |
|---|---|
| Expiry | None. A key is valid until revoked. |
| Rotation | None. Create a new key and delete the old one. |
| Per-key resource allow-list | None. Scope is the only restriction. |
| Rate limiting | None. |
| Usage log | A throttled last_used_at timestamp. Nothing more. |
| Surfaces authenticated | /mcp only. Not Flight SQL, not the REST surface. |
One control does work well. The key lookup joins the active user and the active tenant, so deactivating either kills every key instantly.
Setup instructions are on MCP setup.
Retention and deletion
Ad-hoc query results are retained for 7 days as Arrow IPC artifacts in object storage, with metadata in Postgres. They are creator-owned and tenant-scoped. There is no pin and no save-as-report, so results expire silently on that clock. Automation reports persist longer than the evidence they link to, which is a known consequence.
Transient Flight SQL artifacts are swept every 5 minutes with a 15-minute retention. The durable 7-day artifacts have no physical cleanup sweep and rely on metadata expiry, so objects may outlive their addressability in your bucket.
Deletion cascades are defined rather than incidental. Deleting a connection cascades to its data source but preserves the datasets. Deleting a connector pauses the connections that depend on it. Deleting a source dataset leaves the pipeline running on existing data with a warning. Deleted entities never auto-cancel jobs already in flight.
On audit trail: the CQRS event store records every aggregate event, and each agent session exposes a durable, user-visible timeline. What does not exist is a cross-entity activity feed, so the product cannot currently answer "who deleted this dataset" without going to the event store directly.
The agent trust boundary, for a reviewer
Beetl runs an LLM over customer data. The containment is structural rather than prompt-based.
- Every chat-visible tool result is wrapped in an
untrusted-dataenvelope with nested tags neutralized, and the corresponding instruction is appended in code on every path regardless of which prompt file loaded. Data read from a source cannot present itself to the model as instructions. - Tool availability is computed, not requested. A tool is offered only when its access level sits at or below the surface ceiling, which is write for chat and read for Automation runs, and only when its required context actually exists.
- Scope denial is uniform. Hidden tools and unknown tools return an identical error, so the registry cannot be enumerated by probing.
deploy_pipelineis wired to an end-to-end confirmation flow. A production-scoped tool that does not define confirmation behaviour is rejected by the harness.- Writes to the shared platform wiki are rejected for tenant-scoped callers, which closes a document-shadowing channel into other tenants' prompts.
Two caveats belong in the same list. Chat sessions run at full scope, relying on resource allow-lists and two-phase confirmation rather than a reduced ceiling. And patch_page_state, which lets the assistant edit the page you are looking at, is an unconfirmed write path guarded by schema validation and a visible revert affordance rather than an approval step. It only touches draft state you still have to save.
More on this in Agent trust boundary.
In this section
| Page | What it covers |
|---|---|
| Tenancy and isolation | What separates one tenant from another. |
| Authentication | Zitadel OIDC, PKCE, and session lifecycle. |
| Data residency and encryption | Where data sits, and how secrets are held. |
| API keys | Scopes, and the limits that come with them today. |
| Retention and deletion | What is kept, for how long, and how to remove it. |