Skip to content

maxuru ~ % cat grafana/security-headers

Grafana security headers and CSP

Severity: lowApplies to: Grafana 11.x / 12.x / 13.x
The fix/etc/grafana/grafana.ini
[security]
content_security_policy = true
x_content_type_options = true
x_xss_protection = false
strict_transport_security = true
strict_transport_security_max_age_seconds = 31536000
x_xss_protection = true

That is Grafana’s default, and it is the only place on this site where a current, actively maintained project still enables X-XSS-Protection for you.

The header controlled a browser XSS filter that has been removed from every current browser, and while it existed it was harmful: attackers could craft URLs that made the filter misidentify legitimate scripts and selectively disable them, introducing a vulnerability on pages that had none. Our nginx, Apache and HAProxy pages all say to delete the line. Grafana sets it by default.

In fairness, the practical impact today is close to zero — no current browser implements the filter, so the header is inert rather than dangerous on a modern client. Set it to false anyway: it costs nothing, it stops the header appearing in scan reports, and there is no argument for sending a header whose only historical effect was negative.

This is rated low for exactly that reason. It is tidiness, not risk.

content_security_policy = false

Grafana ships a policy template but does not enable it. Setting content_security_policy = true activates the template already in defaults.ini, which is tuned for Grafana’s own frontend — it uses a per-request nonce and strict-dynamic, and permits the connections Grafana needs.

The template does include 'unsafe-eval' and 'unsafe-inline' in script-src, which is weaker than a CSP written from scratch would be. That reflects what Grafana’s frontend and its plugin ecosystem require; strict-dynamic means modern browsers largely ignore the unsafe entries in favour of the nonce. It is a meaningful improvement over no policy at all, which is the actual comparison.

Turn it on in report-only mode first and read what breaks:

content_security_policy_report_only = true

Plugins are the usual source of violations, since a plugin loading resources from an origin the template does not list will be blocked. That is the policy working, but it is better discovered from a report than from a blank panel.

Two defaults do not need changing, and knowing that saves time:

  • x_content_type_options = truenosniff is already sent.
  • allow_embedding = false — Grafana refuses to be framed by default, which is the clickjacking protection. If you enable it to embed dashboards elsewhere, add the permitted origins to the CSP via form_action_additional_hosts and a frame-ancestors directive rather than leaving framing open to everyone.

Set each header in one place. Duplicated headers from Grafana and from nginx or Apache produce values the browser resolves in ways that are hard to predict, and for CSP specifically multiple policies are combined by intersection — each extra one can only further restrict, which produces breakage that looks like a CSP bug.

Where the proxy already sends a complete set, leave Grafana’s off. Where it does not, Grafana’s are fine. Just do not do both.