Skip to main content
RunBook Academy

ObservabilityXXX · Grafana SecurityGrafanaSecurity

Anonymous Access

Intermediate⏱ ~18 minbash

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

Not yet marked complete on this device.

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.ini out 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 basicAuth set has its labels, queries, and metadata readable by anyone. A Loki data source without basicAuth exposes 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.

  1. Anonymous enabled in the default org. [auth.anonymous] enabled = true, org_name = Main Org.. The symptom is /api/org returning 200 for unauthenticated requests and the admin user listed as the org admin of the org the anonymous user lands in.
  2. org_role = Admin accidentally. 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.”
  3. Data sources without credentials. A Loki data source with basicAuth = false exposes 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.
  4. 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.
  5. The “auth.proxy” pattern leaking into anonymous. auth.proxy is configured with whitelist = 0.0.0.0/0 and no header validation. Every request is treated as a fresh login. The symptom is the same as anonymous enabled: 200 on /api/org without credentials.
  6. auth.anonymous survives a Grafana upgrade. A new grafana.ini template 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?”

  1. Probe /api/org without credentials. 200 means anonymous or auth.proxy wildcard is active. 401 means the boundary holds.
  2. Inspect /api/frontend/settings. The response includes the live anonymous configuration as Grafana sees it. Compare to grafana.ini.
  3. Read [auth.anonymous] in grafana.ini. Confirm enabled = false or enabled = true with the intended org_name and org_role.
  4. Check grafana-cli plugins 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.
  5. Inspect the user table for the login = anonymous, is_anonymous = 1 row. If it exists and the install is supposed to be auth-required, the row is harmless but the configuration is wrong.
  6. For auth.proxy false positives: confirm whitelist is the exact proxy CIDR; a 0.0.0.0/0 whitelist 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_id exposes the existence of that field and the operator’s segmentation.
  • org_role = Admin is 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 for org_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_token lookup. The performance benefit is invisible at any scale where Grafana itself is the bottleneck.

Production guidance

  • [auth.anonymous] enabled = false as 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/org without credentials.
  • The Grafana upgrade process must compare the post-upgrade grafana.ini to 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 = Admin the 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 whitelist produce 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

  1. Q1. Which configuration is the unsafe shape for [auth.anonymous]?

  2. Q2. An anonymous Viewer role prevents the data source labels from being exposed to unauthenticated visitors.

  3. Q3. Which of these are true about Grafana Public Dashboards as an alternative to anonymous access for a status page?

  4. Q4. An unauthenticated curl to /api/org returns 200 instead of 401. Which two configurations could each produce this symptom?

  5. Q5. Name one safe configuration for [auth.anonymous] if the use case legitimately requires it.

  6. Q6. Which is the modern preferred alternative to install-wide anonymous access for a public status page, where licensed?

  7. Q7. Anonymous access and auth.proxy with a wildcard whitelist produce the same observable symptom for /api/org without credentials.

  8. Q8. Which of these are appropriate operational defaults for a production Grafana install?

Passing score: 75%. Answers are checked in this browser.