PATTERN Cited by 1 source
Embedded dashboard with zero-trust iframe¶
A pattern for embedding analytics dashboards into other internal
tools that defends in depth via three layers:
CSP frame-ancestors to restrict embedding sites,
Zero-Trust gateway authentication (Cloudflare Access /
similar) gating the iframe contents themselves, and
view-time permission re-check against the underlying data
tables.
Cloudflare Skipper is the canonical wiki instance, from the 2026-05-28 launch post.
The single-tag embed contract¶
<div data-skipper-dashboard="dash-123"></div>
<script src="https://skipper.cloudflare.com/embed.js" async></script>
Two tags. The script auto-resizes the iframe to fit content. Embedding a Skipper dashboard into any internal tool is one HTML snippet.
The three-layer defence¶
Layer 1 — CSP frame-ancestors¶
The Skipper service ships an HTTP response header on the iframe content:
This blocks the iframe from being embedded outside the corporate domain at the browser level — even if an attacker managed to copy the embed snippet to a malicious site, the browser refuses to render the iframe. The browser is the enforcement point; Skipper doesn't need to verify embedding sites server-side.
Layer 2 — Zero-Trust gateway¶
"Cloudflare Access still gates the iframe contents, so an unauthenticated viewer hits the Access login page in the iframe rather than seeing the data."
The iframe content is served behind Cloudflare Access. If the viewer isn't authenticated to the Cloudflare corporate IDP, they see the Access login page inside the iframe rather than the dashboard. The Access cookie / token is per-domain, so this works whether the iframe is embedded in a Worker app, an internal wiki, or a custom internal tool — as long as the viewer is authenticated, the iframe loads; otherwise, they get the login flow.
Layer 3 — View-time permission re-check¶
"Non-owner viewers are checked against the underlying tables: if they don't have access, they get pointed at the right group to request."
Even if the viewer is authenticated to Cloudflare's Access, they might not have access to the specific data tables the dashboard queries. The dashboard re-checks permissions at view time against the underlying tables (not against a saved-result materialisation). If they don't have access:
- The dashboard fails open to the error-as-permission-request flow.
- Skipper suggests the right RBAC group.
- The viewer can request access without leaving the dashboard.
The structural argument: defence in depth¶
Each layer covers a different threat model:
| Threat | Defended by |
|---|---|
| Malicious site copies the embed snippet | CSP frame-ancestors |
| Unauthenticated user navigates directly to the iframe URL | Cloudflare Access |
| Authenticated user without table-level access tries to view | View-time permission re-check |
| Viewer's group membership changes after the dashboard was shared | View-time permission re-check (the save-time-vs-view-time distinction is canonicalised at concepts/security-model-as-data-model) |
Each layer alone has gaps; together they cover every realistic attack vector for embedded analytics.
Why view-time, not save-time, permission checks¶
The naive design materialises the query result on save and serves the materialised result to all viewers. That's broken under group membership change — see concepts/security-model-as-data-model for the full argument.
The Town Lake / Skipper choice is to store the query, not the result, and re-execute against the calling viewer's permissions. Costs query execution per view; eliminates entire classes of permission-leak bugs.
Composes with data-attribute configuration¶
The data-skipper-dashboard="dash-123" attribute is the only
configuration the embedding site needs. The rest is handled
server-side:
- Dashboard ID maps to query, layout, and visualisation configuration.
- Iframe URL is derived by the bootstrap script.
- Auto-resize logic is in the bootstrap script.
- Auth flow is handled inside the iframe.
The embedding site is completely decoupled from the dashboard's identity / data / auth — it's "just an iframe" at the embedding-site code level.
Generalises beyond Skipper¶
The pattern applies to any internal-only embedded analytics service:
- Replace Cloudflare Access with whatever Zero Trust gateway the org uses (Okta + reverse proxy, Google IAP, etc.).
- Replace
*.cloudflare.comin CSP with the corporate domain. - The view-time permission check requires the dashboard service to store the query, not the result — that's the architectural choice.
Anti-patterns this replaces¶
- Public embed URLs with token-in-URL auth — token leakage becomes total compromise; no group-membership-change handling.
- Save-time permission materialisation — group-membership changes don't propagate to existing shared dashboards; permission leaks accumulate.
- No CSP
frame-ancestors— embed snippet copyable to malicious sites; browsers happily render the iframe.
Seen in¶
- sources/2026-05-28-cloudflare-how-we-built-cloudflares-data-platform-and-an-ai-agent-on-top-of-it — canonical wiki instance. The single-tag embed + CSP + Cloudflare Access + view-time permission check stack.
Related¶
- systems/cloudflare-skipper — canonical wiki dashboard service.
- systems/cloudflare-access — the Zero Trust gateway layer.
- systems/cloudflare-town-lake — the underlying data platform whose permissions are re-checked at view time.
- concepts/security-model-as-data-model — the broader Cloudflare framing where view-time permission checks come from.
- patterns/error-message-as-self-serve-permission-request — the fallback flow when the view-time check fails.