> ## Documentation Index
> Fetch the complete documentation index at: https://cactal.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> Cactal's security model as implemented: SHA-256-hashed API keys shown once, capability-based access control, cross-tenant 404 isolation, per-organization audit logging with actor attribution, noindex draft previews, separated control and content planes, and sandboxed publish builds.

This page describes security behavior you can observe and verify through the product and the API. It documents what the platform does — not aspirations.

## API key handling

* A key's secret is generated from 32 random bytes and shown **once**, when the key is created or rotated. Cactal stores only its SHA-256 hash; nothing on Cactal's side can reveal the plaintext again.
* Rotation (`POST /v1/apiKeys/{apiKeyId}/rotate`) replaces the secret immediately — the old secret stops authenticating the moment the call returns.
* Revocation (`POST /v1/apiKeys/{apiKeyId}/revoke`) is permanent. Keys can also carry an `expiresAt` timestamp; expired and revoked keys fail authentication with `401`.
* REST routes on `api.cactal.ai` accept bearer API keys. The MCP route also accepts audience-bound OAuth access tokens. Requests carrying a browser session cookie are rejected with `401`, so bearer credentials and sessions can never be confused on the public surface.

See [Authentication](/docs/api-reference/authentication) for the full key lifecycle.

## Access control

Authorization is capability-based. Roles expand to capability sets, and every operation — over the dashboard, REST, or MCP — checks one required capability before running.

* Organization roles: `owner`, `admin`, `full_editor`, `content_editor`.
* Website-scoped roles: `full_editor`, `content_editor`, granted per website.
* API keys carry the same model: organization-scoped or website-scoped access with a role. The `owner` role is not grantable to keys.

The MCP server applies the same checks and additionally filters `tools/list`, so a user or key never sees tools it cannot call. See [Access control](/docs/concepts/access-control).

## MCP OAuth

* Standard protected-resource and authorization-server metadata let MCP clients discover the authorization flow without Cactal-specific configuration.
* Cactal accepts Client ID Metadata Documents from public HTTPS origins. Client metadata may link to a public homepage on another HTTPS origin, while non-loopback redirect URIs remain bound to the metadata document's origin. The consent screen identifies discovered clients by that fetched origin. Verified publisher branding is reserved for identities Cactal recognizes, including ChatGPT.
* Clients that use dynamic registration, including Claude's current connector flow, remain visibly unverified because their publisher details are self-reported. All public clients use the authorization code flow with PKCE; client-credentials grants are not exposed.
* Access tokens are signed JWTs with the Cactal authorization server as issuer and `https://api.cactal.ai/v1/mcp` as their exact audience. Tokens without the `mcp` scope are rejected.
* Consent is explicit, refresh tokens require `offline_access`, and tool calls retain the signed-in user's grants and audit identity.

## Tenant isolation

A website you cannot access returns `404` `not_found`, the same response as a website that does not exist, so website existence never leaks across organizations or across website scopes.

```json theme={null}
{ "kind": "not_found", "message": "Website not found" }
```

This applies to websites and everything scoped under them: source code, CMS, assets, domains, and analytics. Organization-scoped endpoints (members, invitations, the audit log, and billing) behave differently: a caller without a grant on an organization that exists gets `403` `forbidden`, and `404` `not_found` means the organization id itself is unknown.

## Audit logging

Lifecycle and access-control operations write audit events to the organization's audit log, whichever interface invoked them: website create/rename/delete/restore/transfer, editor grants and invitations, API keys, domains, organization members and invitations, billing, and analytics CSV export downloads. Source file edits, publishes, CMS content, and asset operations are not audit-logged — review content work through [source versions, changed-file lists, and targeted diffs](/docs/concepts/source-code) instead. Each audit event records:

* The event type and a human-readable summary.
* The affected resource type and id.
* Actor attribution: `actorKind` distinguishes a `user` from an `api_key`, with a reference to the specific user or key.

Query the log with `GET /v1/organizations/{organizationId}/audit-log`, filtering by `actorKind`, `eventType`, `resourceType`, `actorPrincipalId`, or `websiteId` (which matches both the website itself and resources belonging to it). [Agent-safe publishing](/docs/agents/agent-safe-publishing) shows how to use this for agent oversight.

## Draft privacy

Draft and version preview hosts (`draft--<label>` and `v<n>--<label>` on `cactal.app`) are served with:

```text theme={null}
X-Robots-Tag: noindex, nofollow
```

Search engines are instructed not to index preview content. Preview URLs are reachable by whoever holds the link, so share them deliberately.

## Separated planes

The control plane and customer content run on different domains:

| Plane         | Domains                                                              | Serves                                       |
| ------------- | -------------------------------------------------------------------- | -------------------------------------------- |
| Control plane | `cactal.ai`, `app.cactal.ai`, `api.cactal.ai`                        | The dashboard, browser sessions, and the API |
| Content plane | `cactal.app` — `<label>.cactal.app`, preview hosts, `cdn.cactal.app` | Published websites, previews, and assets     |

Browser sessions exist only on the control plane. Customer-authored content — pages, scripts, assets — is served from the content plane, never from a control-plane origin. See [Domains](/docs/concepts/domains).

## Publish build isolation

Publishing builds a website's source in a disposable sandbox created for that job and discarded afterwards. A deployment serves traffic only after its build reaches `ready`; a failed build does not replace what is currently serving. See [Publishing](/docs/concepts/publishing).

## Abuse mitigation

The API rate-limits at several layers. Beyond the per-key buckets, two per-IP buckets protect the authentication surface itself:

* A pre-authentication bucket caps total requests per IP before any key is checked.
* An authentication-failure bucket allows a burst of 25 failed attempts per IP, refilling at 0.5 per second — throttling key guessing to a crawl.

Exact bucket sizes and `429` semantics are in [Rate limits](/docs/api-reference/rate-limits).

## Sign-in

Human sign-in uses Google or email with a password or a one-time code (OTP) sent to that address. The public API host exposes no sign-in routes; MCP OAuth sends users to the product app's authorization server, while REST access uses API keys.

## Reporting

Report suspected vulnerabilities or security incidents to [support@cactal.ai](mailto:support@cactal.ai). Include reproduction details; do not include API key secrets in the report.
