Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Grafana By James Joyner IV · · 9 min read

Grafana Error Guide: 'Refused to display in a frame' — enabling iframe embedding

Fix Grafana iframe embed blocked by X-Frame-Options/frame-ancestors — set allow_embedding, cookie_samesite none, anonymous auth, and check the reverse proxy.

  • #grafana
  • #troubleshooting
  • #errors
  • #embedding
  • #iframe

Overview

When you drop a Grafana dashboard or panel into an <iframe> on another website, the browser often refuses to render it and shows a blank frame. By default, Grafana sends an X-Frame-Options: deny header on every response to protect against clickjacking. The browser honors that header and blocks the embed.

Refused to display 'https://grafana.example.com/' in a frame because it set 'X-Frame-Options' to 'deny'.

On newer Grafana versions that emit a Content-Security-Policy instead of (or alongside) X-Frame-Options, the console message reads:

Refused to frame 'https://grafana.example.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self'".

Even after the frame loads, a second problem is common: the iframe shows a Grafana login page or a blank panel because the session cookie is not sent in a third-party (cross-site) context.

Symptoms

  • The iframe area is blank or shows a browser “Refused to display” message.
  • Browser console logs X-Frame-Options to deny or a frame-ancestors CSP violation.
  • The frame loads but shows the Grafana login screen instead of the dashboard.
  • curl -I against Grafana shows X-Frame-Options: deny.
  • Embed works when Grafana and the host page share an origin, but breaks cross-site (over HTTPS with third-party cookies).

Common Root Causes

1. allow_embedding is disabled (default)

With embedding off, Grafana always adds X-Frame-Options: deny. Turn embedding on to remove that header.

# grafana.ini
[security]
allow_embedding = true
# Before the fix, every response carries:
X-Frame-Options: deny

Environment-variable equivalent for containers:

env:
  - name: GF_SECURITY_ALLOW_EMBEDDING
    value: "true"

Once framing is allowed, a cross-site iframe needs the Grafana session cookie to be SameSite=None; Secure, otherwise the browser withholds it and Grafana shows the login page.

[security]
cookie_samesite = none
cookie_secure = true          ; required whenever SameSite=None, needs HTTPS
# Symptom in the frame: redirected to /login because grafana_session cookie was dropped

3. Anonymous access not enabled for public embeds

If the embedding page has no logged-in Grafana user, enable anonymous viewing so the dashboard renders without a login.

[auth.anonymous]
enabled = true
org_name = Main Org.
org_role = Viewer
logger=context msg="Unauthorized request to embedded dashboard" -> shows login until anonymous is enabled

Container form: GF_AUTH_ANONYMOUS_ENABLED=true, GF_SECURITY_COOKIE_SAMESITE=none.

4. A CSP frame-ancestors directive still blocks the parent

If you set a custom Content-Security-Policy, frame-ancestors 'self' will still block outside origins even with allow_embedding = true.

[security]
content_security_policy = true
content_security_policy_template = """frame-ancestors 'self' https://portal.example.com;"""
Refused to frame ... "frame-ancestors 'self'"

5. The reverse proxy adds its own X-Frame-Options

Even after Grafana stops sending the header, an nginx add_header X-Frame-Options DENY; in front of Grafana re-adds it. You must fix both layers.

# Remove or scope this — it overrides Grafana's own behavior
# add_header X-Frame-Options "DENY";      <-- delete this line
location / {
    proxy_pass http://grafana:3000;
    proxy_set_header Host $host;
}
# curl -I still shows X-Frame-Options: DENY even though Grafana config is correct

Diagnostic Workflow

Step 1 — Read the exact browser error. Open DevTools → Console on the embedding page and note whether it is X-Frame-Options or a frame-ancestors CSP violation.

Step 2 — Inspect the response headers to see which layer sets them.

curl -I https://grafana.example.com/ | grep -iE 'x-frame-options|content-security-policy'

Step 3 — Compare Grafana direct vs through the proxy. If Grafana on :3000 is clean but the proxied URL still has the header, the proxy is the culprit.

curl -I http://grafana:3000/ | grep -i x-frame        # direct
curl -I https://grafana.example.com/ | grep -i x-frame  # via proxy

Step 4 — Apply the Grafana settings and restart.

[security]
allow_embedding = true
cookie_samesite = none
cookie_secure = true

[auth.anonymous]
enabled = true
org_role = Viewer
systemctl restart grafana-server
# or: kubectl rollout restart deploy/grafana -n monitoring

Step 5 — Rebuild the embed URL. Use the dashboard’s Share → Embed iframe snippet, and if Grafana runs on a sub-path set root_url / serve_from_sub_path so asset URLs resolve.

[server]
root_url = https://grafana.example.com/grafana/
serve_from_sub_path = true

Example Root Cause Analysis

An operations portal embedded a Grafana panel via <iframe>. The frame stayed blank and the console read Refused to display ... 'X-Frame-Options' to 'deny'. The team set [security] allow_embedding = true and restarted — but curl -I still showed X-Frame-Options: DENY. Comparing direct (:3000, clean) against the proxied URL (header present) pinpointed an add_header X-Frame-Options "DENY"; in the fronting nginx config. After removing that line the frame loaded, but now showed the login page. Because the portal was on a different site over HTTPS, the grafana_session cookie was being dropped; setting cookie_samesite = none and cookie_secure = true (plus [auth.anonymous] enabled = true, org_role = Viewer for public viewers) made the dashboard render cleanly. See the companion Grafana troubleshooting guides for related CORS issues, which affect API fetch calls rather than iframe embeds.

Prevention Best Practices

  • Enable allow_embedding only for instances that genuinely need embedding; it disables clickjacking protection.
  • Scope embeds with a CSP frame-ancestors allowlist of trusted parent origins rather than leaving framing fully open.
  • Always pair cookie_samesite = none with cookie_secure = true and serve Grafana over HTTPS.
  • Audit both Grafana and every reverse proxy for duplicate X-Frame-Options / CSP headers.
  • Prefer least-privilege anonymous Viewer (or a dedicated read-only org) for public dashboards, and consider the public/shared dashboard feature.

Quick Command Reference

# Which layer is sending the framing header?
curl -I https://grafana.example.com/ | grep -iE 'x-frame-options|content-security-policy'
curl -I http://grafana:3000/ | grep -i x-frame          # Grafana direct

# Apply env-var equivalents in a container
# GF_SECURITY_ALLOW_EMBEDDING=true
# GF_SECURITY_COOKIE_SAMESITE=none
# GF_SECURITY_COOKIE_SECURE=true
# GF_AUTH_ANONYMOUS_ENABLED=true

# Restart to apply grafana.ini changes
systemctl restart grafana-server
kubectl rollout restart deploy/grafana -n monitoring

Conclusion

The top root causes of Grafana iframe embeds being blocked are:

  1. allow_embedding is off by default, so Grafana sends X-Frame-Options: deny — set [security] allow_embedding = true.
  2. The session cookie is dropped in a cross-site frame — set cookie_samesite = none and cookie_secure = true over HTTPS.
  3. No anonymous access, so the frame shows a login page — enable [auth.anonymous] with org_role = Viewer.
  4. A custom CSP frame-ancestors directive still excludes the parent origin — add the parent to the allowlist.
  5. A reverse proxy re-adds X-Frame-Options / CSP — remove the duplicate header at the proxy layer.
Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.