Grafana serves as a central visualization and control plane for observability data. It often has broad access to datasources and must be protected as a critical security boundary. This guide covers security measures for production Grafana deployments.
Grafana’s security model encompasses multiple layers:
Key risks include unauthorized data access, credential exposure, and privilege escalation through misconfigured permissions.
Configure firewall rules to restrict Grafana access:
# Allow Grafana server port (3000) only from trusted networks
ufw allow from 10.0.0.0/8 to any port 3000 proto tcp
ufw allow from 192.168.1.0/24 to any port 3000 proto tcp
# Block external access
ufw deny from any to any port 3000 proto tcp
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: grafana-network-policy
spec:
podSelector:
matchLabels:
app: grafana
ingress:
- from:
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- protocol: TCP
port: 3000
Bind Grafana to specific interfaces:
# /etc/grafana/grafana.ini
[server]
http_addr = 127.0.0.1
http_port = 3000
domain = grafana.internal.company.com
root_url = https://grafana.internal.company.com/
For containerized deployments:
# docker-compose.yml
services:
grafana:
ports:
- "127.0.0.1:3000:3000"
Configure secure user settings:
# /etc/grafana/grafana.ini
[users]
allow_sign_up = false
allow_org_create = false
auto_assign_org = true
auto_assign_org_role = Viewer
[auth]
disable_login_form = false
disable_initial_admin_creation = false
Grafana provides built-in roles with granular permissions:
| Role | Permissions |
|---|---|
| Viewer | View dashboards and panels |
| Editor | Create/edit dashboards, manage folders |
| Admin | Full organization access, user management |
| Server Admin | Global administrative access |
Configure folder-level permissions:
[rbac]
enabled = true
LDAP Authentication:
# /etc/grafana/grafana.ini
[auth.ldap]
enabled = true
config_file = /etc/grafana/ldap.toml
allow_sign_up = false
# /etc/grafana/ldap.toml
[[servers]]
host = "ldap.company.com"
port = 636
use_ssl = true
start_tls = false
ssl_skip_verify = false
bind_dn = "cn=admin,dc=company,dc=com"
bind_password = "${LDAP_BIND_PASSWORD}"
OAuth2 Integration:
[auth.generic_oauth]
enabled = true
name = "Company SSO"
client_id = "${OAUTH_CLIENT_ID}"
client_secret = "${OAUTH_CLIENT_SECRET}"
scopes = "openid profile email"
auth_url = "https://sso.company.com/oauth/authorize"
token_url = "https://sso.company.com/oauth/token"
api_url = "https://sso.company.com/oauth/userinfo"
SAML Authentication:
[auth.saml]
enabled = true
idp_metadata_url = "https://sso.company.com/saml/metadata"
assertion_attribute_name = "name"
assertion_attribute_login = "login"
assertion_attribute_email = "email"
Enable multi-factor authentication:
[auth]
oauth_allow_insecure_email_lookup = false
[security]
# Enable TOTP for all users
enforce_totp = true
For enterprise features, integrate with identity providers that support MFA.
Secure API access with tokens:
# Create service account token
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin_token>" \
-d '{"name": "monitoring-service", "role": "Viewer"}' \
http://localhost:3000/api/serviceaccounts
Enable HTTPS for Grafana:
[server]
protocol = https
cert_file = /etc/grafana/certs/grafana.crt
cert_key = /etc/grafana/certs/grafana.key
min_tls_version = "1.2"
Configure TLS at the reverse proxy level:
Nginx Configuration:
server {
listen 443 ssl http2;
server_name grafana.company.com;
ssl_certificate /etc/nginx/certs/grafana.crt;
ssl_certificate_key /etc/nginx/certs/grafana.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Configure TLS for datasource connections:
{
"name": "Prometheus",
"type": "prometheus",
"url": "https://prometheus:9090",
"secureJsonData": {
"tlsCACert": "-----BEGIN CERTIFICATE-----\n...",
"tlsClientCert": "-----BEGIN CERTIFICATE-----\n...",
"tlsClientKey": "-----BEGIN PRIVATE KEY-----\n..."
},
"jsonData": {
"tlsAuth": true,
"tlsAuthWithCACert": true,
"tlsSkipVerify": false
}
}
Restrict API endpoint access:
| Endpoint | Risk Level | Access Control |
|---|---|---|
/api/dashboards/* |
Medium | Authenticated users |
/api/datasources/* |
High | Admin only |
/api/users/* |
High | Admin only |
/api/org/* |
High | Admin only |
/api/admin/* |
Critical | Server Admin only |
/api/search |
Low | Authenticated users |
/api/health |
Low | Public (configurable) |
Disable anonymous API access:
[auth.anonymous]
enabled = false
Configure security headers and access controls:
[security]
# Content Security Policy
csp_template = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data:; font-src 'self'; connect-src *; frame-ancestors 'none';"
# Cookie security
cookie_secure = true
cookie_samesite = strict
# XSS protection
x_xss_protection = true
# Prevent clickjacking
allow_embedding = false
Implement least-privilege datasource access:
[datasources]
# Limit datasource creation to admins
editors_can_admin = false
Grafana stores sensitive data in its database:
Secure sensitive configuration:
# Use environment variables for secrets
[database]
password = ${GF_DATABASE_PASSWORD}
[auth.generic_oauth]
client_secret = ${GF_OAUTH_CLIENT_SECRET}
[smtp]
password = ${GF_SMTP_PASSWORD}
For Kubernetes deployments:
apiVersion: v1
kind: Secret
metadata:
name: grafana-secrets
type: Opaque
data:
admin-password: <base64-encoded>
ldap-bind-password: <base64-encoded>
Secure alerting configurations:
{
"notifiers": [
{
"type": "webhook",
"settings": {
"url": "https://alerts.company.com/webhook",
"authorization_scheme": "Bearer",
"authorization_credentials": "${WEBHOOK_TOKEN}"
}
}
]
}
Protect audit logs and session data:
Enable audit logging:
[audit]
enabled = true
log_path = /var/log/grafana/audit.log
[audit.filters]
# Log all authentication events
authentication = true
# Log dashboard changes
dashboards = true
# Log datasource modifications
datasources = true
# Log user management
users = true
Configure reverse proxy access logging:
access_log /var/log/nginx/grafana_access.log combined;
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time"';
Monitor for suspicious activity:
# Alert on failed login attempts
- alert: GrafanaFailedLogins
expr: increase(grafana_http_request_duration_seconds_count{handler="/login", status="401"}[5m]) > 10
for: 2m
labels:
severity: warning
annotations:
summary: "Multiple failed login attempts on Grafana"
# Alert on datasource access errors
- alert: GrafanaDatasourceErrors
expr: increase(grafana_datasource_request_total{status="error"}[5m]) > 20
for: 5m
labels:
severity: warning
# Alert on admin API access
- alert: GrafanaAdminAPIAccess
expr: increase(grafana_http_request_duration_seconds_count{handler=~"/api/admin/.*"}[1m]) > 0
labels:
severity: info
Track active sessions:
[session]
provider = redis
provider_config = "addr=localhost:6379,prefix=sessions"