maxuru ~ % cat grafana/security-headers
Grafana security headers and CSP
/etc/grafana/grafana.ini[security]content_security_policy = truex_content_type_options = truex_xss_protection = falsestrict_transport_security = truestrict_transport_security_max_age_seconds = 31536000The one that is backwards
Section titled “The one that is backwards”x_xss_protection = trueThat 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.
CSP is off by default
Section titled “CSP is off by default”content_security_policy = falseGrafana 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 = truePlugins 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.
What is already right
Section titled “What is already right”Two defaults do not need changing, and knowing that saves time:
x_content_type_options = true—nosniffis 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 viaform_action_additional_hostsand aframe-ancestorsdirective rather than leaving framing open to everyone.
If a reverse proxy sits in front
Section titled “If a reverse proxy sits in front”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.
Related
Section titled “Related”- Enable TLS and secure cookies — HSTS depends on it being right.
- nginx security headers — the full case against X-XSS-Protection.