UI security
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
Section titled “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:
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 inlinestyle="…"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 -sI "https://platform.atlanticagenticlabs.com/" | grep -i -E "content-security-policy|strict-transport|x-frame|x-content-type|referrer-policy|permissions-policy"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))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
Section titled “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
Section titled “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 for the second-factor checks.
Branding uploads
Section titled “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 andurl()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
Section titled “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
Section titled “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.