Skip to content

API security

The LightHouse API lives at your tenant’s own host, https://<tenant id>.atlanticagenticlabs.com/api/v1/. The portal and the MCP server use the same service, the same checks and the same roles. This page covers each check in the order a request meets it.

What it is. The service works out which tenant a request is for from the hostname alone. It never reads a tenant from a query parameter, a body field or a header such as X-Forwarded-Host, so forging one changes nothing.

Where. The LightHouse service, before any route runs. A hostname that is no tenant’s answers 404 unknown_client.

Verify. Send a request to a hostname that is not yours: it is refused at the edge or answers 404. Add a client parameter to any request on your own host: it is ignored, or refused as 400 bad_query where the route takes no such parameter.

People: sign in through your identity provider

Section titled “People: sign in through your identity provider”

What it is. People sign in through your own identity provider, never with a password held by Atlantic. Supported: Microsoft Entra ID, Google Workspace, GitHub (with rules by organisation and team), GitLab (by group), Okta, OneLogin, Ping, Auth0, Keycloak and any SAML or OIDC provider. A one-time email code is offered for named external viewers only.

Signing in proves who someone is. Your tenant’s access rules then decide whether they get in, and with which role. The rules match on provider, group, GitHub team, email domain or a named email. An email domain alone is not enough for a GitHub or GitLab identity, because its email is often not the company’s.

Where. Twice. Cloudflare Access at the edge checks the identity against your tenant’s own Access application. The LightHouse service verifies the signed token again and applies your access rules. A person who passes the edge but not your rules gets 403 not_authorised.

What it is. Every person must prove a second factor. It cannot be turned off.

Where.

  • At the edge, the Access policy requires a second factor from your identity provider wherever the provider reports one. Otherwise it requires Cloudflare Access’s own second factor (a security key or an authenticator).
  • In the service, a person is accepted only if the identity provider reported a second factor, or if the service has confirmed that every allow policy on your tenant’s Access application requires one. That check runs at start and every five minutes. If a policy lacks the requirement, or the check has not completed for more than 15 minutes, the person is refused with 403 mfa_required, even if the edge let them through. A misconfigured policy fails closed.

Sessions last at most 12 hours.

Verify. Sign in and call GET /api/v1/me from the portal’s session. viewer.mfa.source says how your second factor was established: provider, cloudflare or access_policy.

What it is. Scripts and services authenticate with two credentials, one for each wall:

  1. Your tenant’s Cloudflare Access service token (CF-Access-Client-Id and CF-Access-Client-Secret), checked at the edge.
  2. A LightHouse API key (Authorization: Bearer aalcp-...), checked by the service.

The service token alone reads nothing. The key alone never reaches the service.

Property of an API key Rule
Roles viewer or content_viewer only. Never handler or client_admin, so a leaked key cannot change who else gets in
Scope One tenant. A key is looked up within the hostname’s tenant only; at any other tenant’s host it does not exist and answers 401
Expiry Mandatory, at most 365 days
Storage Shown once when issued. LightHouse keeps only its SHA-256, so a copy of the database grants nothing
Revocation Immediate. A revoked key is a row, not a deletion, so the audit log keeps who revoked it and when

Where. Keys are issued by a client_admin in Settings → API keys, or with POST /api/v1/keys. See Authentication.

Verify. Call any route with your key at another tenant’s host, or with a revoked key: 401. List your keys and the service token’s expiry with GET /api/v1/keys (a client_admin, as a person).

Terminal window
# A key at its own tenant's host: 200. The same key anywhere else: 401.
curl -i "https://$AAL_TENANT.atlanticagenticlabs.com/api/v1/health" \
-H "Authorization: Bearer $AAL_KEY" \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"

What it is. Four roles. Each includes everything the one above it may do.

Role Adds
viewer Operational telemetry: runs, usage, cost, health, evaluations, savings
content_viewer Your content, read in place from your account
handler Commands on your agents: pause, resume, decide an approval, within your command settings and your deployment’s own policy
client_admin Access rules, API keys, the access review, the audit log and the command settings

Where. The service checks the role on every API route and every MCP tool. A missing role is 403 role_required, naming the role needed.

Guard rails that your own administrators cannot change, each refused as 422 guard_rail:

  • At least one unexpired client_admin grant always remains.
  • A one-time email code matches only a named email.
  • Public email domains (gmail.com, outlook.com and the like) cannot be used as a domain rule.
  • A time-limited grant runs at most a year.
  • No rule may name Atlantic. Atlantic staff get at most content_viewer, and only if your rules say so.
  • MFA cannot be turned off.

Verify. GET /api/v1/access/review?format=csv (a client_admin) lists who holds which role by which grant, their last sign-in, every key, and every Atlantic staff read. See Who can see what for Atlantic’s own access.

What it is. Every tenant’s rows sit in one database, separated by PostgreSQL row-level security. It is a second wall behind the query layer, which already scopes every query to the hostname’s tenant.

Where. The database itself:

  • The service connects as a role that owns nothing and cannot bypass row-level security.
  • Every table that holds a tenant’s rows has row-level security enabled and forced.
  • The tenant is set per transaction, from the request’s hostname (or, for telemetry, from the signing deployment).
  • If a code path forgets to set it, the setting is empty and matches nothing: the query reads and writes nothing, rather than everything.
  • A query that names another tenant gets no rows. An insert or update of another tenant’s row fails.

A test in LightHouse’s own suite finds every table with a tenant column from the database catalogue and fails if any lacks forced row-level security and its policy, so a new table cannot be added without it.

Verify. From outside, you cannot reach the database, and no route takes a tenant parameter to try. Under NDA we will walk through the policies and the test. See Verify it yourself.

What it is, today:

Limit Where
Telemetry from your deployment: records a minute and bytes a day, per tenant and per deployment LightHouse intake answers 429 with Retry-After; nothing of the refused request is stored
Telemetry requests per source address Cloudflare, at the edge
Sign-up attempts per address, email and email domain, per minute and per day The public sign-up endpoints
JSON request bodies: 256 KB by default, 16 KB for commands and approval decisions, 4 KB for profile requests The service reads bodies in chunks and stops at the cap, so an oversized body is never buffered whole
Branding images: 512 KB The branding routes

Per-key and per-tenant rate limits on the read API, with headers that report them: Coming

The provider limits your own models are under (requests, input and output tokens a minute) are a separate thing. LightHouse measures them for you; see Rate limits.

Verify. Send a JSON body over 256 KB to any write route: it is refused before it is read in full.

What it is. Two mechanisms make writes safe to retry.

  • Idempotency-Key on commands. POST /api/v1/agents/{agent_id}/commands and POST /api/v1/approvals/decisions accept an Idempotency-Key header of 8 to 128 characters (letters, digits, ., _, : and -). The same person sending the same key again gets the original command back, not a second command. The database enforces it with a unique index, so even a double click that races is caught.
  • Optimistic versions on settings. Access rules, command settings, routing settings and other settings carry a version. A PUT with a stale version is refused with 409 version_conflict, so two administrators cannot silently overwrite each other.

Verify. Send the same command twice with one Idempotency-Key: both answers carry the same command_id.

Terminal window
# Run as a person with the handler role (commands are never made by a key).
curl -X POST "https://$AAL_TENANT.atlanticagenticlabs.com/api/v1/agents/$AGENT/commands" \
-H "Cookie: CF_Authorization=$CF_AUTHORIZATION" \
-H "Idempotency-Key: pause-2026-11-01-001" \
-H "Content-Type: application/json" \
-d '{"type": "agent.pause", "params": {"reason": "investigating a failure"}}'

$AGENT is the agent’s ID, <pack>:<card>. $CF_AUTHORIZATION is the session cookie your browser holds after signing in. See Idempotency and the Handler commands reference.

What it is. An append-only audit log per tenant. It records every change to your access rules (who, when, before and after), every key issued or revoked, every sign-in and refusal, every handler command, every change to limits, billing, branding, connectors and settings, every content read, and every read by Atlantic staff.

Personal preferences (a person’s own time zone, theme or tour progress) are not audited.

Where. The database refuses any update, delete or truncate of the audit log. The service writes the entry in the same transaction as the change wherever the change is LightHouse’s own.

Verify. Setup and admin → Audit in LightHouse, or GET /api/v1/audit as a client_admin. Each entry names the actor, their identity provider type, the summary, the reason and whether it was a break-glass action. See the Audit reference.

What it is. Some actions are for a signed-in person only, whatever role a key or a service holds:

  • Pay: top up, change auto-recharge, add or remove a payment method, open checkout.
  • Raise a limit: add, change or clear a spend limit.
  • Approve: decide an approval, issue any handler command, grant a pre-approval.
  • Administer: change access rules, issue keys, change routing settings, start a benchmark.

Where. In the LightHouse service, each of these routes checks that the caller is a person from your organisation who has proved a second factor. A key is refused with 403 person_required (on administration routes, 403 role_required, since no key can hold client_admin). In your account, your deployment accepts approvals and limit changes only as commands signed by LightHouse, and only those your policy permits. No MCP tool does any of these things.

Verify. Call POST /api/v1/approvals/decisions or POST /api/v1/limits with your API key: 403 person_required. LightHouse’s own test suite asserts this for top-up, auto-recharge, payment methods, the customer portal and spend limits.