Skip to content

Tenant isolation and secret encryption on a shared platform

How to secure a multi-tenant platform: enforce tenant scope in the data layer, seal credentials with AES-256-GCM, rotate keys and limit tools.

Updated · 7 min read

Why is a multi-tenant platform riskier than a normal SaaS app?

A normal SaaS app stores a tenant’s data. A platform also stores the keys to that tenant’s other systems: mailbox tokens, ITSM credentials, monitoring API keys. A cross-tenant bug therefore leaks not only records but the ability to act inside another company.

The assistant itself adds a second risk. Its next step is chosen by a model that reads untrusted text such as emails, tickets and web pages. OWASP’s Top 10 for LLM applications calls the result “excessive agency”: damaging actions triggered by manipulated or simply wrong model output, rooted in too much functionality, too many permissions or too much autonomy.

  • Cross-tenant reads: one missing tenant filter returns another customer’s sessions, tickets or memory.
  • Credential exposure: connector secrets returned by an API, written to logs, or placed where a model can repeat them.
  • Object-level authorisation gaps: an ID taken from a request and used without checking who owns it (OWASP API1:2023).
  • Excessive agency: a prompt-injected agent that can send, delete or call without a human in the loop.
  • Silent failure: no audit record to show which identity triggered which action.

Where should tenant isolation be enforced?

There are three common designs: a database per tenant, a schema per tenant, or shared tables with a tenant column. Shared tables are the usual choice for platforms with many tenants because migrations, analytics and operations stay simple, but they put all the isolation weight on one thing: every query must be scoped.

Scoping by developer discipline alone fails eventually, because it only takes one forgotten filter. The safer rule is to enforce the scope in one choke point that every query passes through, so a forgotten filter becomes an error instead of a leak.

ApproachIsolationOperational costTypical failure
Database per tenantStrongestHigh: many databases to migrate and monitorDrift between tenant databases
Schema per tenantStrongMedium to highConnection routed to the wrong schema
Shared tables, filters by conventionWeakLowOne forgotten tenant filter leaks data
Shared tables, guard in the data-access layerStrong for application queriesLowRaw SQL or an explicitly unguarded client
Shared tables, PostgreSQL row-level securityStrong inside the databaseMedium: policies and session contextTable owners and BYPASSRLS roles skip policies unless forced

How Botify enforces tenant scope in the data layer

Botify uses shared tables with a guard built into its Prisma database client. Prisma’s query extensions let code intercept every operation on every model; Botify uses that hook to refuse any read, update, delete, count or aggregate on tenant-scoped data whose filter does not constrain the tenant, and any create whose payload does not name the tenant. The refusal is a thrown error at the call site, so the mistake surfaces in development and tests instead of in a customer’s data.

The check looks for the tenant anywhere in the filter tree, not only at the top level. Real queries scope the tenant inside AND or OR branches, compound unique keys or relation filters, and a guard that only accepted one shape would push developers to switch it off. The unguarded client exists only as an explicit option for migrations, seeding and platform jobs. Connections and credentials are stored per tenant, so they fall under the same guard.

An application-layer guard only sees queries that go through the client. Raw SQL, reporting tools and direct database access need their own controls, which is where database-level mechanisms such as PostgreSQL row-level security and strict database roles complement it.

How should an AI platform encrypt connector credentials?

Use authenticated encryption. AES in Galois/Counter Mode (GCM), specified in NIST SP 800-38D, encrypts the secret and produces an authentication tag, so tampering with the ciphertext is detected on decryption instead of producing garbage that some code might trust. GCM needs a unique nonce for every encryption under the same key; generating a fresh random 96-bit nonce per seal is the standard approach.

GCM also accepts additional authenticated data (AAD): context that is not encrypted but is bound into the tag. Botify seals every connector credential with AES-256-GCM and sets the AAD to the owning tenant, the credential’s ID and its type. If someone copies an encrypted credential into another tenant’s row, or relabels it as a different type, the tag no longer verifies and decryption fails. Before decrypting, the vault also checks that the stored AAD names the row it was read from.

  • Algorithm recorded in the envelope, so it can change later without guessing.
  • Key version recorded in the envelope, so the right key is chosen on read.
  • Random 96-bit nonce per encryption, stored next to the ciphertext.
  • Authentication tag stored and verified on every decryption.
  • AAD binding the ciphertext to tenant, credential and type.

How do you rotate encryption keys without downtime?

Separate two kinds of rotation. Credential rotation replaces the secret itself, for example a new API token from the provider. Key rotation replaces the root key that seals all secrets. The second is the one teams postpone, because done naively it means decrypting and rewriting every row in one risky migration.

Versioned keys remove that risk. Botify’s keyring holds several versions, one of them active; new secrets are sealed with the active version, and older versions stay available only to read what they sealed. A background worker loop then re-encrypts old envelopes to the active key in small batches per tenant. Each rewrite only succeeds if the row still carries the key version that was read, so a concurrent credential rotation is never overwritten. Once no envelope uses an old version, that key can be removed.

Two details save real incidents. Validate key material strictly at startup: a mistyped base64 key that silently decodes to wrong bytes would make every stored credential unreadable. And record rotations and revocations as audit events, so an auditor can see when a secret changed and who changed it.

Why should secrets never be returned through the API?

If an API can return a secret, sooner or later it will: to a compromised session, a debugging script, a browser extension or a log line. The safest contract is write-only. Clients can create, rotate and revoke credentials, and read their metadata, but never read the value back.

Botify’s credential API returns metadata only: provider, type, scopes, key version, expiry, last use and revocation. The decrypted value exists only inside the connector call that needs it, revoked credentials refuse to open, and each use updates a last-used timestamp. Logs and traces carry the credential’s ID, never its value.

What does least privilege mean for assistant tools?

Least privilege for assistants has two layers. The first is the credential: request the narrowest OAuth scopes and API permissions the use case needs. The second is the tool: each tool the model can call should declare what it can do, and the platform, not the model, decides whether it runs.

Botify labels every tool with a risk level (read, write, destructive or external send) and an approval mode. Read-only tools such as searching Gmail or running a Splunk search run directly. Sending an email, creating a ServiceNow incident or placing a phone call always waits for a human approval. Many writes, such as archiving or labelling mail, are left to tenant policy. Identity and roles are checked before any tool runs, and MCP servers registered by a tenant join the same catalogue under the same policies.

What should an AI platform audit log record?

An audit trail answers one question after the fact: who caused this action, with what authority, and what happened. For an assistant platform that means linking the human request, each tool call the assistant made, any approval or rejection, and the result, all scoped to the tenant.

  • Identity and tenant of the requester, and the roles checked.
  • Every tool call with its inputs, policy decision and outcome.
  • Every approval or rejection, with the approver’s identity.
  • Credential lifecycle events: created, rotated, revoked.
  • No secrets and no raw credentials in any audit entry or log.

Frequently asked questions

Is a tenant_id column enough for multi-tenant isolation?

The column is necessary but not sufficient. Isolation depends on every query using it, so enforce the filter in one layer every query passes through, such as a guarded database client or row-level security, and treat an unscoped query as an error rather than a code-review item.

Why AES-256-GCM instead of AES-CBC for stored credentials?

GCM is authenticated encryption: decryption fails if the ciphertext, nonce or associated data was changed. CBC on its own gives confidentiality only and needs a separate MAC to detect tampering. GCM also supports associated data, which lets you bind a secret to its tenant and record.

What is additional authenticated data (AAD) in encryption?

AAD is context that is not encrypted but is covered by the authentication tag, such as a tenant ID and record ID. Decryption only succeeds with the same AAD, so a ciphertext copied to another tenant or record cannot be opened there.

How often should encryption keys be rotated?

Your security policy and regulators set the interval. What matters architecturally is that rotation is routine: versioned keys, a background re-encryption job, and removal of the old key only after nothing uses it. If rotation needs downtime, it will be postponed.

Can the language model see my API keys?

It should not. Credentials belong in connector code, not in prompts or tool results. In Botify the decrypted secret exists only inside the connector call that uses it, and the API never returns it.

Sources

  1. NIST SP 800-38D: Galois/Counter Mode (GCM) and GMAC
  2. OWASP Top 10 for LLM Applications 2025: LLM06 Excessive Agency
  3. OWASP API Security Top 10 2023: API1 Broken Object Level Authorization
  4. PostgreSQL documentation: Row Security Policies
  5. Prisma documentation: query component of Prisma Client extensions

Where this applies in Botify

Related articles

All articles

Start with one team.

Pick one workflow and one or two systems. We connect Botify and measure the difference.