ObservabilityLXXIX · Securing GrafanaSecureGrafana
Anonymous Access Discipline
What you'll learn
- Explain what the [auth.anonymous] block does in Grafana 11.x and what the org_role default grants to an unauthenticated visitor
- Configure a public status-page Grafana with anonymous Viewer access to a single folder and disable anonymous everywhere else
- Recognise the false economy of enabling anonymous access as an alternative to fixing SSO
- Diagnose an audit log full of anonymous logins and identify whether the exposure is intentional
- Disable anonymous access once SSO is verified and document the public-status-page exception as a deliberate, reviewed decision
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 instance is deployed behind the company SSO portal. The operator enables anonymous access because a status dashboard for customers has to load without a login. The default org_role for anonymous is Viewer, which sounds safe. Three months later the operator notices that the audit log is full of anonymous logins from IP addresses that are not the status page render farm, and that one of those IP addresses belongs to a competitor that discovered the URL. The status dashboard shows the production error budget burn rate and the last twelve hours of incident timelines. The Viewer role was the right org role; the folder-scoped permissions were not.
This lesson is about closing that gap. Anonymous access is a deliberate exception, not a default. Production Grafana has anonymous either because there is a documented public surface, or not at all.
What it is
The [auth.anonymous] block in grafana.ini enables an unauthenticated identity for visitors to the Grafana URL. With the block enabled and org_role = Viewer, every request that does not carry a valid session cookie is logged in as the user named anonymous with the Viewer role for the configured organisation.
The block has four settings:
- enabled — defaults to false. Without this, anonymous access is off and Grafana returns the login form for unauthenticated requests.
- org_name — the organisation the anonymous user is bound to. Defaults to Main Org. Every Grafana install on earth has a Main Org (org id = 1); changing the name does not change the id.
- org_role — the role granted to the anonymous user within org_name. Defaults to Viewer. Editor and Admin are allowed but produce the failure shape described below.
- hide_version — defaults to true. Whether to suppress the Grafana version from the footer.
Unauthenticated browser ----> Grafana
|
v
Is [auth.anonymous] enabled?
|
yes: log in as `anonymous` with org_role
|
no: return /login
The fundamental property: anonymous is a real Grafana user. The session cookie issued to an anonymous browser is signed with secret_key, just like any other session. An anonymous Viewer can read every dashboard the Viewer role can read; an anonymous Editor can edit them.
Why a sysadmin cares
Anonymous access is the cheapest possible way to expose Grafana and the most expensive possible way to expose it. The decision tree is short.
- Status page or public dashboard. Anonymous Viewer on a single folder is the right pattern. The folder contains exactly the dashboards meant for public consumption; every other resource in the org is unaffected.
- Internal Grafana behind SSO. Anonymous must be off. There is no operational benefit to enabling it and the security cost is unbounded.
- Demo or evaluation Grafana. Anonymous Viewer is acceptable for time-bounded evaluation, on a non-production host, with no real data behind it.
The false economy is enabling anonymous access as an alternative to fixing SSO. The argument is usually: SSO is hard, anonymous is a single config line, the dashboards are read-only anyway. The counter-argument is: anyone who can reach the URL can read the dashboards, and Viewer on the wrong folder is a data leak. The cheaper path is to fix the SSO integration; the more expensive path is to enable anonymous and discover the leak later.
How it works
Every unauthenticated request flows through the same session path as an authenticated one. The difference is the source of the user identity.
1. Browser requests / without a session cookie
2. Grafana checks the session cookie; none present
3. Grafana checks [auth.anonymous] enabled
- yes: resolve the anonymous user; create a session
- no: redirect to /login
4. Grafana issues a session cookie (HMAC-signed, HttpOnly)
5. Subsequent requests carry the cookie and read as anonymous
The anonymous user is resolved at request time, not provisioned in advance. Grafana creates the user record in the database on first request if one does not exist. The org_role from the block is the role applied at login; subsequent changes to the block do not retroactively change the role of an existing anonymous user.
The default organisation (org id = 1) is the organisation the anonymous user lands in when org_name is not set. Every Grafana install on earth has org id = 1. Folder permissions within org 1 are what determine what the anonymous user can actually see.
How to configure it
Default off
# /etc/grafana/grafana.ini
[auth.anonymous]
enabled = false
org_name = Main Org.
org_role = Viewer
hide_version = true
The block stays in the file as documentation. enabled = false is the production default for any Grafana that does not have a deliberate public surface.
A public status-page Grafana
# /etc/grafana/grafana.ini
[auth.anonymous]
enabled = true
org_name = Status
org_role = Viewer
hide_version = true
[users]
# No self-signup; no new user accounts through anonymous.
allow_sign_up = false
# /etc/grafana/provisioning/access-control/folders.yaml
apiVersion: 1
folders:
- uid: status
title: Status
permissions:
- role: Viewer # every Viewer in org `Status` reads this folder
permission: 0
- role: Editor # org-wide Editor does not get this folder
permission: 0
A second Grafana install on a separate host, behind a separate TLS termination, with no other data sources, is the right shape. Do not enable anonymous on the main Grafana.
nginx in front of the status-page Grafana
# /etc/nginx/conf.d/status-grafana.conf
server {
listen 443 ssl http2;
server_name status.example.com;
ssl_certificate /etc/nginx/certs/status.crt;
ssl_certificate_key /etc/nginx/certs/status.key;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# The status page is public. No auth_request here.
# Restrict by IP at the network layer if business policy requires.
allow all;
}
}
How to validate it
# READ-ONLY: confirm anonymous access is off in the main Grafana.
curl -fsS -i https://grafana.example.com/api/org | head -1
# HTTP/1.1 401 Unauthorized
# READ-ONLY: confirm the same on a status-page Grafana.
curl -fsS -i https://status.example.com/api/org | head -1
# HTTP/1.1 200 OK
# READ-ONLY: confirm an anonymous session issues a cookie.
curl -fsS -i https://status.example.com/login | grep -i 'set-cookie'
# Set-Cookie: grafana_anon=...; Path=/; HttpOnly; Secure; SameSite=Lax
# READ-ONLY: confirm the anonymous user lands in the configured org.
curl -fsS -b 'grafana_anon=' https://status.example.com/api/org
# {"id":2,"name":"Status"}
# READ-ONLY: enumerate audit log entries for anonymous actor.
curl -fsS -H "Authorization: Bearer ${GF_SA_TOKEN}" \
'https://grafana.example.com/api/audit?from=2026-08-01&to=2026-08-14' \
| jq '.[] | select(.actor == "anonymous") | {action, ip}'
# {"action":"dashboard.viewed","ip":"203.0.113.17"}
# {"action":"dashboard.viewed","ip":"203.0.113.17"}
# READ-ONLY: confirm only the intended folder is accessible anonymously.
curl -fsS -b 'grafana_anon=' https://status.example.com/api/search?query=
# [{"id":1,"uid":"status","title":"Status","type":"dash-folder"}]
# Only the /Status folder is reachable.
# READ-ONLY: confirm org_role is Viewer and not Editor.
curl -fsS -H "Authorization: Bearer ${GF_SA_TOKEN}" \
https://status.example.com/api/org/users | jq '.[] | select(.login == "anonymous") | {login, role}'
# {"login":"anonymous","role":"Viewer"}
How it can fail
The high-frequency anonymous-access failure shapes from real Grafana installs.
- org_role = Editor under [auth.anonymous]. Every visitor to the URL can edit every dashboard in the configured org. The symptom is dashboards appearing, disappearing, and being modified by anonymous with source IPs that are not the render farm.
- Anonymous on the main Grafana by accident. The operator copied the status-page Grafana ini block into the main Grafana during a refactor and restarted. The main Grafana now serves every internal dashboard to anyone who can reach the URL. The symptom is an audit log full of anonymous actor rows for internal dashboard views.
- Anonymous org_role = Viewer but folder permissions open. A folder with no ACL inherits the org-wide Viewer role. Every dashboard in the org is reachable through anonymous. The symptom is anonymous read access to dashboards the operator thought were internal-only.
- hide_version = false on a public Grafana. The Grafana version is in the footer; a CVE scanner matches the version to a published advisory in seconds. The fix is hide_version = true plus a reverse proxy that strips the X-Grafana-Version header.
- Status page Grafana promoted to internal use. A new product team starts using the status-page Grafana because it is the one Grafana that loads without SSO. Internal dashboards land in the org; the org_role for anonymous is unchanged; the new dashboards are public by accident.
- Anonymous enabled, then SSO enabled without disabling it. Both paths work in parallel. Users who should be forced through SSO can read everything by ignoring the SSO redirect and browsing to a dashboard URL directly.
How to troubleshoot it
The diagnostic order matters: anonymous traffic looks like normal read traffic, with an actor field of anonymous in the audit log.
- Confirm the block state. grep -A 5 ‘[auth.anonymous]’ in /etc/grafana/grafana.ini. enabled = true or false is the ground truth; the audit log is downstream of the block.
- Count anonymous actor rows. GET /api/audit with a filter on actor=anonymous over the last 24 hours. A non-zero count on the main Grafana is a signal; a non-zero count on the status-page Grafana is expected.
- Inspect the source IPs. Cross-reference the IPs against the render farm allowlist and the public DNS resolver set. IPs outside both are unexpected visitors.
- Inspect the folder ACL. GET the folder permissions endpoint for every folder the anonymous user can reach. A missing permission row on a folder means the org-wide role applies.
- Test the boundary. curl with no cookie against an internal dashboard URL. If 200, the dashboard is reachable anonymously. If 401, the dashboard is correctly gated.
Security implications
- Anonymous Viewer is a public read surface. Treat the URL as public, even when it is behind a VPN. The only thing standing between the visitor and the data is the URL.
- Anonymous Editor is always an incident. There is no legitimate production use case. Treat the activation as a compromise and restore from provisioning.
- Folder permissions are the only boundary that matters. The org_role is the ceiling; the folder ACL is the floor. Without explicit folder ACLs, the floor is the org-wide role.
- The audit log records anonymous as the actor. This is the audit signal; there is no separate log stream for anonymous traffic.
- hide_version suppresses one fingerprinting signal. A motivated attacker has other ways to learn the version; the setting is hygiene, not a defence.
Performance implications
- Anonymous sessions are full sessions. Grafana does not differentiate anonymous sessions from authenticated ones at the database level. A render farm that hammers the status-page Grafana produces a row in the user_session table per worker; without rotation the table grows.
- The audit log grows with every read. A public Grafana with thousands of readers per minute fills the audit log in hours. Filter the audit log on action=dashboard.viewed; consider dropping dashboard.viewed rows from long-term retention.
- Folder permissions apply per request. A Grafana with hundreds of folders and an anonymous Viewer pays one ACL lookup per dashboard request. The cost is the same as for an authenticated Viewer.
Production guidance
- Anonymous access off on every Grafana by default.
- Anonymous access enabled only on a dedicated status-page Grafana, on a separate host, with no internal data sources.
- org_role = Viewer; folder ACLs grant Viewer only on the public folder.
- hide_version = true; reverse proxy strips X-Grafana-Version.
- Audit log review on a fixed cadence; any anonymous actor row on the main Grafana is a signal.
- Disable anonymous access in the same change that enables SSO; do not leave both running in parallel.
Verification
You should now be able to answer:
- What does the [auth.anonymous] block grant to an unauthenticated visitor, and where is the ceiling set?
- Why is enabling anonymous access on the main Grafana the false economy, and what is the correct alternative for a public dashboard?
- How do folder ACLs interact with the anonymous org_role, and what is the floor if a folder has no ACL?
- How does an audit review distinguish anonymous traffic from authenticated traffic?
- What is the recovery path if org_role = Editor under [auth.anonymous] is discovered in production?
Quiz
Knowledge check · 8 questions
Q1. The [auth.anonymous] block in Grafana 11.x grants an unauthenticated visitor:
Q2. Enabling anonymous access on the main Grafana as an alternative to fixing SSO is the cheapest way to ship a Grafana that works for everyone.
Q3. Which of these are required for a public status-page Grafana to be safe?
Q4. A folder with no ACL row under a Grafana with [auth.anonymous] enabled and org_role = Viewer is reachable by:
Q5. Name the audit log field that distinguishes an anonymous actor from an authenticated one.
Q6. What is the correct production response if org_role = Editor is found under [auth.anonymous] on a main Grafana?
Q7. Setting hide_version = true under [auth.anonymous] suppresses the Grafana version string from the public-facing footer.
Q8. Which of these are true about anonymous sessions in Grafana 11.x?
Passing score: 75%. Answers are checked in this browser.