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
OPTIONSrequest returns405or404and the real request never fires. - Server-side
curlto the same API endpoint works perfectly (no browser, no CORS). - Grafana Live / streaming panels embedded on another domain fail with
Origin not allowedin thelogger=livelogs. - The websocket handshake returns HTTP
403before 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
/apicross-origin from JavaScript. - Put Grafana behind a reverse proxy and, if browser access is truly required, centralize CORS handling (including the
OPTIONSpreflight204) there. - Keep
Access-Control-Allow-Originto an explicit allowlist, not*, especially when usingAllow-Credentials: true. - Set
root_urlto 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:
- A browser SPA calling the Grafana
/apidirectly — Grafana adds no CORS headers by design; proxy the call server-side so it is same-origin. - No reverse proxy handling the preflight — add an nginx
OPTIONS204responder plusAccess-Control-Allow-Originheaders. - A failing
OPTIONSpreflight for non-simple requests (custom headers/methods) never returning2xx. - Grafana Live websockets from another domain not listed in
[live] allowed_origins. - A
root_urlthat does not match the browser-visible URL, breaking Live’s same-origin check.
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.