# UI security

> The content security policy, headers, sessions and tests behind LightHouse's interface.

The LightHouse portal is the interface your people sign in to at `https://<tenant id>.atlanticagenticlabs.com`. It shows your telemetry, and your content where your roles allow. Every control on this page can be checked with your browser’s developer tools.

## Content security policy

**What it is.** Every page carries a content security policy with a fresh nonce, made for that request alone (18 random bytes). Only scripts from the portal’s own origin, or inline scripts carrying that request’s nonce, can run. There is no `'unsafe-inline'` for scripts anywhere. An attacker who managed to inject HTML into a page still could not run a script in it.

The policy, as sent:

```text
default-src 'self';
script-src 'self' 'nonce-<per request>' https://js.stripe.com https://*.js.stripe.com;
style-src 'self' 'nonce-<per request>';
style-src-elem 'self' 'nonce-<per request>';
style-src-attr 'unsafe-inline';
img-src 'self'; font-src 'self';
connect-src 'self' https://api.stripe.com;
frame-src 'self' https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com;
object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
```

Two things in it are worth knowing:

- **Stripe.js is the only third party**, for the in-app payment pages, with exactly the origins Stripe’s own security guide lists. A test in the portal’s suite fails if anything else adds an origin.
- **`style-src-attr 'unsafe-inline'`** allows inline `style="…"` attributes, which the charts, maps and progress bars render on the server. A nonce cannot cover an attribute. No tenant-supplied value ever reaches a style attribute: a tenant’s brand is served as a same-origin stylesheet built from validated values.

The sign-in page at `platform.atlanticagenticlabs.com` has its own, stricter policy (`default-src 'none'`, `'strict-dynamic'`, `form-action 'none'`, `Referrer-Policy: no-referrer`) and serves nothing but the sign-in page and its files.

**Where.** The portal’s request handler makes the nonce, sets the policy on the response, and passes the nonce to the framework’s own scripts.

**Verify.** Load any page twice and compare the `Content-Security-Policy` header: the nonce differs each time.

**curl**

```bash
curl -sI "https://platform.atlanticagenticlabs.com/" | grep -i -E "content-security-policy|strict-transport|x-frame|x-content-type|referrer-policy|permissions-policy"
```

**Python**

```python
import httpx

r = httpx.get("https://platform.atlanticagenticlabs.com/")
for name in ("content-security-policy", "strict-transport-security", "x-frame-options",
             "x-content-type-options", "referrer-policy", "permissions-policy"):
    print(name, "=", r.headers.get(name))
```

**TypeScript**

```ts
const res = await fetch("https://platform.atlanticagenticlabs.com/");
for (const name of ["content-security-policy", "strict-transport-security", "x-frame-options",
  "x-content-type-options", "referrer-policy", "permissions-policy"]) {
  console.log(name, "=", res.headers.get(name));
}
```

The sign-in host needs no credentials, so anyone can run this. Your tenant’s own pages need a signed-in session; open your browser’s network panel on any of them to see the portal’s policy.

## Security headers

**What it is.** Every portal response carries:

| Header                      | Value                                      | What it prevents                                                        |
| --------------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains`      | Any visit over plain HTTP after the first                               |
| `X-Content-Type-Options`    | `nosniff`                                  | A browser treating an upload as script                                  |
| `X-Frame-Options`           | `DENY`                                     | The portal being framed by another site (with `frame-ancestors 'none'`) |
| `Referrer-Policy`           | `strict-origin-when-cross-origin`          | Paths and queries leaking to other sites                                |
| `Permissions-Policy`        | `camera=(), microphone=(), geolocation=()` | The page asking for devices it never needs                              |

The API adds `nosniff`, `Referrer-Policy: no-referrer` and the same HSTS to every response, including streamed ones, that has not set its own.

**Verify.** The command above, or `curl -sI` against any API route with your key.

## Sessions

**What it is.** A person’s session is a Cloudflare Access session at your tenant’s host, opened through your identity provider with a second factor. It lasts at most 12 hours. Each request carries the signed Access token, which the LightHouse service verifies on every request, not only at sign-in.

**Where.** Cloudflare Access issues and expires the session. The service verifies the token’s signature and audience against your tenant’s own Access application, and refuses a token made for any other.

**Verify.** Sign in, wait past the session length, and reload: you are sent to sign in again. See [API security](/security/api/#people-sign-in-through-your-identity-provider) for the second-factor checks.

## Branding uploads

**What it is.** Your administrators can brand the portal: a name, logos, a favicon, two colours, a font and a corner radius. It is applied as **values, never CSS**. A tenant cannot send a stylesheet, because CSS could restyle or hide parts of a page (a spoofed approval, a hidden warning) or read the page out through selectors.

- **SVG** logos are rebuilt from an allow-list of elements and attributes. Scripts, foreign objects, event handlers, external or `data:` references and `url()` to anything but a fragment are refused.
- **PNG and WebP** (at most 512 KB) are decoded in full and re-encoded from their pixels alone, so metadata, colour profiles and anything appended after the image never reach your portal’s origin. An image whose header disagrees with its contents is refused.
- Every image is served from the portal’s own origin with its exact content type and `nosniff`; an SVG also carries a sandboxing policy of its own.
- Colours that fall below WCAG AA contrast (4.5:1) on either theme are adjusted to the nearest shade that passes, and both values are returned so you can see what changed.
- Fonts come from a fixed list of self-hosted or system fonts. The portal makes no third-party font request.

**Where.** The LightHouse service, on upload. Every branding change is in your audit log.

**Verify.** Upload an SVG containing `<script>`: it is refused, naming the element.

## No secret is ever shown twice

**What it is.** A LightHouse API key is shown once, when it is issued, and never again: only its SHA-256 is kept. LightHouse shows your Access service token’s expiry and renewal date, never the token. A connector credential you enter passes through LightHouse once, on its way to your own account’s secret store over your deployment’s command channel; LightHouse keeps only where it went, a keyed fingerprint, its scopes and its expiry.

**Where.** The LightHouse service, which stores hashes and fingerprints only.

**Verify.** Issue a key in **Settings → API keys**, close the dialog, and look for it again: only its label, role, ID and expiry remain.

## Tests on every change

**What it is.** The portal’s end-to-end suite runs in CI on every change to the portal. On every page, for each of the four roles, it checks:

- no console error or warning, no failed request and no error response;
- **no content security policy violation**: a refused script, style or connection fails the test, whatever the test was looking at;
- **accessibility**: no WCAG 2.2 A or AA violation found by axe, in light and in dark;
- no horizontal scroll at 390, 768, 1280 and 1440 pixels wide;
- pages a role may not open are refused politely, not with an error.

Unit tests feed hostile input (`<script>` in filters, saved preferences and assistant actions; scripts and event handlers in SVG) to the code that parses it.

**Where.** Atlantic’s CI, before a change can be released.

**Verify.** Run your own scanner against the sign-in page, which needs no credentials. Under NDA we will share the latest results of these suites. See [Verify it yourself](/security/verify/).

Source: https://docs.atlanticagenticlabs.com/security/ui/
