ObservabilityXXX · Grafana SecurityGrafanaSecurity
Anonymous Access
What you'll learn
- Configure [auth.anonymous] so unauthenticated visitors receive a defined role within a defined organisation, instead of the default login or 401
- Recognise that anonymous Viewer with access to data sources exposes query parameters and labels to anyone who can reach the URL
- Distinguish the legitimate public-dashboard pattern (Grafana Public Dashboards) from the dangerous open-Grafana pattern (anonymous.enabled=true with default Org 1)
- Disable anonymous access in production and document why in the security baseline
- Diagnose the symptom where every request returns a redirect to /login when anonymous.enabled is unset
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A Grafana 11 install is exposed to the internal network so the SRE team can reach it from their laptops. The default grafana.ini ships with [auth.anonymous] enabled = true in many distribution packages. Every employee of the company can open the URL, see every dashboard that the Viewer role allows, query every data source that does not require explicit credentials, and read every label on every metric and log line in the platform. The platform team thought they had “internal Grafana.” They have “public Grafana inside the corporate firewall.”
This lesson is about closing that boundary. Anonymous access in Grafana is a single switch; the operator has to know when it is the right answer, when it is not, and what the alternative looks like.
What it is
Anonymous access in Grafana is the [auth.anonymous] block. When enabled = true, any request that arrives without a session cookie or a Authorization header is automatically logged in as a single shared “anonymous” user. The role of that user is configured by org_role (typically Viewer); the organisation is configured by org_name (defaults to Main Org.).
Browser Grafana
------- -------
| |
|--GET /-------------->|
| |--no cookie, no Authorization header
| |--lookup anonymous user
| |--assign role: Viewer
| |<--200 OK (dashboard renders)
| |
|<--200 OK-------------|
The anonymous user is a real row in the user table with is_anonymous = 1. Grafana creates it on first request if it does not exist. Every unauthenticated request becomes a request as that user.
The legitimate use case is narrow: a public status page, a read-only corporate dashboard behind a reverse proxy that does its own authentication (and uses Grafana Public Dashboards as the modern alternative), or a demonstration install. The illegitimate use case is broader: an internal Grafana that the operator enabled to skip the “configure SSO” step. The cost of the illegitimate case is paid in incidents, not in outages.
Why a sysadmin cares
Anonymous access changes the threat model. The boundary moves from “anyone who can log in” to “anyone who can reach the port.” Inside the corporate firewall that means every employee, every contractor, every device with a VPN client, every developer’s laptop on the guest Wi-Fi that has been compromised.
The production failure shapes:
- The unconfigured default. Many Grafana distribution packages enable anonymous access in
grafana.iniout of the box. A team that deploys Grafana without reading the file has anonymous enabled and does not know. - The internal-network assumption. A Grafana on a 10.0.0.0/8 subnet is reachable from every laptop on every VLAN that can route to it. The boundary is not the subnet; the boundary is the corporate firewall.
- The data-source leak. A Grafana with anonymous Viewer can still query data sources that do not require explicit credentials. A data source without
basicAuthset has its labels, queries, and metadata readable by anyone. A Loki data source withoutbasicAuthexposes the labels on every log stream. - The audit-log gap. The anonymous user has one entry in the audit log per request, all attributed to “Anonymous.” A real user doing something wrong is invisible in the audit log.
- The “I’ll add auth later” pattern. Anonymous is enabled so the team can show progress. Six months later, SSO is not configured, the data sources are populated, the dashboards are public, and “add auth later” has become a project that nobody owns.
How it works
Grafana’s authentication chain runs in a defined order. Anonymous access is one of the backends the chain consults.
Request arrives
|
v
Session cookie present?
|
+--yes--> validate against secret_key, look up user, allow
|
v (no)
Authorization header present?
|
+--yes--> validate API key or service-account token, allow
|
v (no)
[auth.anonymous] enabled = true?
|
+--yes--> log in as anonymous user, assign org_role, allow
|
v (no)
401 Unauthorized, redirect to /login
The role assigned to the anonymous user is set by org_role in [auth.anonymous]. Allowed values are Viewer, Editor, and Admin. The allowed value in production is Viewer. The Admin value turns Grafana into a public-write installation; there is no legitimate use case.
The organisation the anonymous user lands in is set by org_name. The default is Main Org., which corresponds to id = 1 in the org table. The combination of enabled = true, org_name = Main Org., org_role = Admin is the worst-case shape: every visitor is an admin in the default org.
[auth.anonymous]
enabled = true
org_name = Main Org. # id = 1 by definition
org_role = Admin # the unsafe shape
How to configure it
The production default: disabled
# /etc/grafana/grafana.ini
[auth.anonymous]
enabled = false
# The defaults below are inert when enabled = false.
org_name = Main Org.
org_role = Viewer
# READ-ONLY: confirm anonymous is disabled.
curl -fsS -o /dev/null -w "%{http_code}\n" https://grafana.example.com/api/health
# 200
curl -fsS -o /dev/null -w "%{http_code}\n" https://grafana.example.com/api/org
# 401
# An unauthenticated request to a protected endpoint returns 401, not 200.
The legitimate case: a public status page
A read-only status page is the canonical use case. The Grafana Public Dashboards feature (Enterprise in Grafana Cloud, OSS in Grafana 11 with the right licence; check the licence terms for your install) is the modern alternative and is preferred when available because it is per-dashboard, not install-wide.
# /etc/grafana/grafana.ini
[auth.anonymous]
enabled = true
# A dedicated org, not the default Org 1.
org_name = Status
org_role = Viewer
# CONFIGURATION: create the Status org once.
curl -fsS -X POST https://grafana.example.com/api/orgs \
-H "Authorization: Bearer ${GF_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"Status"}'
# {"orgId":2,"message":"Organization created"}
# CONFIGURATION: provision the data sources the status org should see.
# /etc/grafana/provisioning/datasources/status.yaml
# /etc/grafana/provisioning/datasources/status.yaml
apiVersion: 1
datasources:
- name: Prometheus-Status
type: prometheus
uid: prom-status
orgId: 2
url: http://prometheus:9090
access: proxy
isDefault: true
jsonData:
timeInterval: 30s
# No basicAuth; the data source is read by the anonymous Viewer role.
secureJsonData: {}
The reverse-proxy alternative
For a public status page hosted at status.example.com, terminate auth at the reverse proxy and use auth.proxy instead of anonymous access. The anonymous feature stays disabled in Grafana.
# /etc/grafana/grafana.ini
[auth.proxy]
enabled = true
header_name = X-WEBAUTH-USER
header_property = username
auto_sign_up = false
enable_login_token = false
# A dedicated header value for the anonymous case; the proxy injects
# this for unauthenticated requests to the status sub-path.
whitelist = 127.0.0.1/32
# /etc/nginx/sites-available/status.conf
location / {
# The status sub-path always carries the anon header.
if ($uri ~ ^/status/) {
proxy_set_header X-WEBAUTH-USER "status-viewer";
}
proxy_pass http://grafana_upstream;
}
How to validate it
# READ-ONLY: anonymous disabled. An unauthenticated request returns 401.
curl -fsS -i https://grafana.example.com/api/org | head -3
# HTTP/1.1 401 Unauthorized
# content-type: application/json
# {"message":"Unauthorized"}
# READ-ONLY: an authenticated request returns 200.
curl -fsS -H "Authorization: Bearer ${GF_SA_TOKEN}" \
https://grafana.example.com/api/org
# {"id":1,"name":"Main Org."}
# READ-ONLY: a misconfigured anonymous install returns 200 for everyone.
# Watch for this; if it returns 200 without a token, anonymous is on.
curl -fsS -o /dev/null -w "%{http_code}\n" https://grafana.example.com/api/org
# 200 # bad; anonymous is enabled
# 401 # good; auth is required
# READ-ONLY: inspect the configured value (live, not from disk).
curl -fsS -H "Authorization: Bearer ${GF_SA_TOKEN}" \
https://grafana.example.com/api/frontend/settings | jq '.anonymous'
# {"enabled":false,"orgName":"Main Org.","orgRole":"Viewer"}
# READ-ONLY: confirm the data sources the anonymous org can see.
# If orgName = Status, the anonymous user should only see Status org datasources.
curl -fsS https://grafana.example.com/api/datasources | jq '.[] | {name,type}'
# [{"name":"Prometheus-Status","type":"prometheus"}]
# READ-ONLY: confirm the response does not leak other org data.
# /api/search is the common leak vector.
curl -fsS https://grafana.example.com/api/search | jq '.[].title'
# ["Service uptime"]
A clean validation: an unauthenticated request to a protected endpoint returns 401, the response from /api/frontend/settings reports anonymous.enabled = false, and an authenticated request through the SSO backend works as expected.
How it can fail
The high-frequency anonymous-access failure modes from real Grafana installs.
- Anonymous enabled in the default org.
[auth.anonymous] enabled = true, org_name = Main Org.. The symptom is/api/orgreturning 200 for unauthenticated requests and theadminuser listed as the org admin of the org the anonymous user lands in. org_role = Adminaccidentally. The operator copies a[auth.anonymous]block from a public-facing install into an internal one. The symptom is every visitor having Org Admin; the audit log fills with edits attributed to “Anonymous.”- Data sources without credentials. A Loki data source with
basicAuth = falseexposes every log stream label to the anonymous Viewer. The symptom is a leak of tenant IDs, customer IDs, and request IDs from the label space to anyone who can reach the URL. - Public dashboards through the wrong feature. The operator enables anonymous access so the status page works, but does not restrict the data source list. The symptom is the entire dashboard tree (including internal SRE runbooks and customer-data panels) being reachable through the same URL pattern as the status page.
- The “auth.proxy” pattern leaking into anonymous.
auth.proxyis configured withwhitelist = 0.0.0.0/0and no header validation. Every request is treated as a fresh login. The symptom is the same as anonymous enabled: 200 on/api/orgwithout credentials. auth.anonymoussurvives a Grafana upgrade. A newgrafana.initemplate in the new package re-enables anonymous because the operator’s overrides were at the bottom of the file and a comment marker changed. The symptom is a quiet regression discovered by the next penetration test.
How to troubleshoot it
The diagnostic order is “is the install actually requiring auth, or is it just pretending to?”
- Probe
/api/orgwithout credentials. 200 means anonymous orauth.proxywildcard is active. 401 means the boundary holds. - Inspect
/api/frontend/settings. The response includes the liveanonymousconfiguration as Grafana sees it. Compare tografana.ini. - Read
[auth.anonymous]ingrafana.ini. Confirmenabled = falseorenabled = truewith the intendedorg_nameandorg_role. - Check
grafana-cliplugins for the legacy “anonymous” UI flag. Some older installs toggle anonymous through Grafana’s admin UI and the value is stored in the database, not in the file. - Inspect the
usertable for thelogin = anonymous, is_anonymous = 1row. If it exists and the install is supposed to be auth-required, the row is harmless but the configuration is wrong. - For
auth.proxyfalse positives: confirmwhitelistis the exact proxy CIDR; a0.0.0.0/0whitelist produces the same symptom as anonymous enabled.
Security implications
- Anonymous removes the audit log’s value. Every action becomes attributed to “Anonymous.” A real attacker becomes indistinguishable from a curious employee.
- Data source labels are visible to the anonymous role. A Loki data source with high-cardinality labels leaks the label schema; a Prometheus data source with exemplars leaks span IDs; a Tempo data source with derived fields leaks trace structure.
- Dashboard JSON is visible to the anonymous role. A dashboard contains the queries, the panel layout, and the variables. A panel that filters by
customer_idexposes the existence of that field and the operator’s segmentation. org_role = Adminis a self-service privilege escalation. The anonymous user can grant itself Editor or Admin on any folder the org admin can see. There is no legitimate use case fororg_role = Admin.- Public Dashboards is the safer alternative for the public-status-page use case because the permission scope is per-dashboard, not per-install. A misconfigured public dashboard does not expose the rest of the install.
Performance implications
- Anonymous removes the database write per login. With SSO, every login writes to
login_attempt,user_last_seen, and the audit log. Anonymous writes none of these. The performance benefit is small and the security cost is large. - Data source queries are unchanged. The query rate through the proxy is the same whether the request comes from an authenticated user or the anonymous user.
- Session-table lookups are skipped. Anonymous requests skip the
user_tokenlookup. The performance benefit is invisible at any scale where Grafana itself is the bottleneck.
Production guidance
[auth.anonymous] enabled = falseas the production default.- If anonymous is required, a dedicated org (
Status),org_role = Viewer, and a curated data source list. - No data source with high-cardinality labels in the anonymous org.
- Public Dashboards (where licensed) instead of install-wide anonymous for status pages.
- A penetration test on every release that explicitly probes
/api/orgwithout credentials. - The Grafana upgrade process must compare the post-upgrade
grafana.inito the pre-upgrade file; anonymous re-enabling is a known regression path.
Verification
You should now be able to answer:
- Why is
enabled = true, org_name = Main Org., org_role = Adminthe worst-case anonymous configuration, and what should be substituted for each value? - What does the anonymous user expose from the data source layer that an authenticated Viewer cannot see?
- Why is Public Dashboards (where licensed) a safer alternative to install-wide anonymous access for a public status page?
- How does a misconfigured
auth.proxy whitelistproduce the same observable symptom as anonymous enabled? - What is the operational cost of leaving anonymous access on “temporarily” while SSO is being configured?
Quiz
Knowledge check · 8 questions
Q1. Which configuration is the unsafe shape for [auth.anonymous]?
Q2. An anonymous Viewer role prevents the data source labels from being exposed to unauthenticated visitors.
Q3. Which of these are true about Grafana Public Dashboards as an alternative to anonymous access for a status page?
Q4. An unauthenticated curl to /api/org returns 200 instead of 401. Which two configurations could each produce this symptom?
Q5. Name one safe configuration for [auth.anonymous] if the use case legitimately requires it.
Q6. Which is the modern preferred alternative to install-wide anonymous access for a public status page, where licensed?
Q7. Anonymous access and auth.proxy with a wildcard whitelist produce the same observable symptom for /api/org without credentials.
Q8. Which of these are appropriate operational defaults for a production Grafana install?
Passing score: 75%. Answers are checked in this browser.