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

Grafana Error Guide: 'origin not allowed' — fixing CORS on the Grafana API and Live

Fix Grafana CORS 'origin not allowed' errors — Grafana adds no CORS headers by design; proxy the API, add headers in nginx, or set Live allowed_origins.

  • #grafana
  • #troubleshooting
  • #errors
  • #cors
  • #api

Overview

When a browser-side application (a JavaScript SPA) calls the Grafana HTTP API directly with fetch() from a different origin, the request is blocked by the browser’s Same-Origin Policy. The key fact that trips people up: Grafana does not implement general CORS headers on its API by design. The API is meant to be consumed server-side, not from a user’s browser, so it never emits an Access-Control-Allow-Origin header — which the browser reads as the origin being disallowed.

Access to fetch at 'http://grafana:3000/api/dashboards/home' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

The Grafana Live websocket path has its own origin check with its own message:

Origin not allowed
level=warn logger=live msg="Origin check failed" origin=https://app.example.com

Symptoms

  • Browser console shows blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
  • A preflight OPTIONS request returns 405 or 404 and the real request never fires.
  • Server-side curl to the same API endpoint works perfectly (no browser, no CORS).
  • Grafana Live / streaming panels embedded on another domain fail with Origin not allowed in the logger=live logs.
  • The websocket handshake returns HTTP 403 before upgrading.

Common Root Causes

1. A browser SPA calling /api directly (the by-design case)

There is no Grafana setting that turns on CORS for the REST API. Calling /api/* from browser JavaScript on another origin will always be blocked.

// This will ALWAYS hit CORS from a browser on a different origin
fetch('http://grafana:3000/api/search', {
  headers: { Authorization: 'Bearer glsa_xxx' }
})
No 'Access-Control-Allow-Origin' header is present on the requested resource.

The correct fix is to proxy the call through your own backend so the browser only ever talks to its own origin:

// Browser → your backend (same origin) → Grafana (server-to-server, no CORS)
fetch('/my-backend/grafana/search')   // your server injects the Grafana token

2. No reverse proxy adding CORS headers / handling preflight

If you must call Grafana from the browser, terminate it behind nginx and let nginx answer the preflight and inject the headers.

location / {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin "https://app.example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
        add_header Access-Control-Allow-Credentials "true" always;
        add_header Content-Length 0;
        return 204;                       # answer preflight, no body
    }
    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    add_header Access-Control-Allow-Credentials "true" always;
    proxy_pass http://grafana:3000;
    proxy_set_header Host $host;
}
< HTTP/1.1 204 No Content
< Access-Control-Allow-Origin: https://app.example.com

The browser sends a preflight OPTIONS request first for any non-simple request (custom headers like Authorization, or methods like PUT/DELETE). If that preflight does not return 2xx with the right Allow-* headers, the actual request is never sent.

3. Grafana Live websocket origin not whitelisted

Grafana Live checks the Origin header of the websocket against [live] allowed_origins. The default only allows the same host as root_url, so streaming from another domain fails.

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

[live]
# Regex list of extra origins allowed to open Live websockets
allowed_origins = https://app.example.com https://*.example.com
logger=live msg="Origin check failed" origin=https://app.example.com allowed=https://grafana.example.com

If root_url does not match the address the browser actually uses, the same-origin check for Live also fails — set root_url to the externally visible URL.

Diagnostic Workflow

Step 1 — Confirm it is CORS, not auth or network. Repeat the request server-side; if curl works but the browser fails, it is CORS.

# Server-side — no browser, so no CORS. Should return 200/JSON.
curl -s -H "Authorization: Bearer glsa_xxx" http://grafana:3000/api/search | head

Step 2 — Simulate the browser preflight. Send an OPTIONS with an Origin header and inspect the response headers.

curl -i -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: authorization" \
  http://grafana:3000/api/search
# Look for: Access-Control-Allow-Origin. Grafana alone returns none.

Step 3 — Check what the proxy actually returns. After configuring nginx, verify the header is present on both the preflight and the real request.

curl -I -H "Origin: https://app.example.com" https://grafana.example.com/api/search | grep -i access-control

Step 4 — For Live, inspect the websocket handshake and logs.

journalctl -u grafana-server -f | grep -i 'logger=live'
kubectl logs deploy/grafana -n monitoring -f | grep -i 'origin'

Step 5 — Reload Grafana after [live]/root_url changes.

systemctl restart grafana-server

Example Root Cause Analysis

A dashboard portal at https://app.example.com fetched panel data straight from http://grafana:3000/api/ds/query. Locally it worked (same origin during dev); in production the browser reported No 'Access-Control-Allow-Origin' header is present. A server-side curl returned valid JSON, proving the token and network were fine — the problem was purely the browser blocking a cross-origin fetch.

Because Grafana intentionally ships no CORS headers, the team stopped calling /api from the browser. They routed the request through their own backend (same origin as the SPA), which held the Grafana service-account token and forwarded server-to-server. The CORS error vanished with no Grafana config change. For the one genuinely browser-driven Live streaming panel, they additionally set [live] allowed_origins = https://app.example.com and corrected root_url. See other Grafana error guides for embedding-related issues, which are a separate class of problem from datasource CORS.

Prevention Best Practices

  • Treat the Grafana API as server-side only; never ship a Grafana token to the browser or call /api cross-origin from JavaScript.
  • Put Grafana behind a reverse proxy and, if browser access is truly required, centralize CORS handling (including the OPTIONS preflight 204) there.
  • Keep Access-Control-Allow-Origin to an explicit allowlist, not *, especially when using Allow-Credentials: true.
  • Set root_url to the exact externally visible URL and list extra websocket origins in [live] allowed_origins.
  • Do not confuse browser/API CORS with datasource CORS — they are unrelated.

Quick Command Reference

# Prove it's CORS: server-side works, browser doesn't
curl -s -H "Authorization: Bearer glsa_xxx" http://grafana:3000/api/search | head

# Simulate the preflight and inspect CORS headers
curl -i -X OPTIONS -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: authorization" \
  http://grafana:3000/api/search

# Verify proxy is injecting the header
curl -I -H "Origin: https://app.example.com" https://grafana.example.com/api/search \
  | grep -i access-control

# Watch Grafana Live origin checks
journalctl -u grafana-server -f | grep -i 'logger=live'

Conclusion

The top root causes of Grafana origin not allowed / CORS errors are:

  1. A browser SPA calling the Grafana /api directly — Grafana adds no CORS headers by design; proxy the call server-side so it is same-origin.
  2. No reverse proxy handling the preflight — add an nginx OPTIONS 204 responder plus Access-Control-Allow-Origin headers.
  3. A failing OPTIONS preflight for non-simple requests (custom headers/methods) never returning 2xx.
  4. Grafana Live websockets from another domain not listed in [live] allowed_origins.
  5. A root_url that does not match the browser-visible URL, breaking Live’s same-origin check.
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.