Interpreting Your securityheaders.com Grade and Fixing It
This guide is part of the Header Grading with Mozilla Observatory and securityheaders.com reference and answers the narrow question every engineer asks after a scan: my grade is X, what exactly do I add to reach A+, and what does A+ fail to tell me? securityheaders.com grades on header presence plus one content gate on the Content Security Policy. Below is the exact header set that earns A+, annotated, then the server config to emit it, then the traps that make A+ false comfort.
Configuration Syntax & Exact Values
securityheaders.com awards A+ when all six graded headers are present and the CSP forbids inline script. This is the complete header set, annotated with what each contributes to the grade:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=()
Annotated by grade contribution:
Strict-Transport-Security— required for A and above. The grader checks presence, not themax-agevalue, but a real deployment uses a longmax-age; the HSTS reference covers the value choice.Content-Security-Policy— required for A; the absence ofunsafe-inline/unsafe-evalinscript-srcis the gate that separates A from A+. The'self'shown here passes the gate.object-src 'none',base-uri 'none', andframe-ancestors 'none'add real hardening the grader does not measure.X-Frame-Options: DENY— required; satisfies the framing check.frame-ancestors 'none'in the CSP also satisfies it, but keep both for legacy-browser coverage, covered in the X-Frame-Options reference.X-Content-Type-Options: nosniff— required; the exact tokennosniffis the only valid value.Referrer-Policy: strict-origin-when-cross-origin— required; any valid policy token counts. See the Referrer-Policy and Permissions-Policy reference.Permissions-Policy— required; an empty allowlist()denies the feature to all origins, which is the safest default.
Server-Side Configuration
Each block below emits the full A+ baseline. Set the headers at the edge so they appear on every response, including error pages.
Nginx
server {
listen 443 ssl;
server_name example.com;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
}
always emits each header on 4xx/5xx responses too; without it the grader can follow a redirect or hit an error page that lacks the headers and mark them missing. Re-declare these in any location block that adds its own add_header, since a block-level add_header drops all inherited ones.
Apache
<VirtualHost *:443>
ServerName example.com
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
</VirtualHost>
always sets the headers even on internally generated error documents, which the default onsuccess condition skips. Requires mod_headers.
Cloudflare
Use a Transform Rule (Rules → Transform Rules → Modify Response Header) to set each header on all proxied responses, or a Worker for finer control. In the dashboard add one “Set static” entry per header with the exact values above. Cloudflare emits these at the edge on every response, so remove any duplicate origin-level header to avoid two conflicting copies that the grader may read inconsistently.
Node/Helmet
const helmet = require('helmet');
app.use(helmet({
strictTransportSecurity: { maxAge: 63072000, includeSubDomains: true, preload: true },
contentSecurityPolicy: { directives: {
defaultSrc: ["'self'"], scriptSrc: ["'self'"], objectSrc: ["'none'"],
baseUri: ["'none'"], frameAncestors: ["'none'"]
}},
xFrameOptions: { action: 'deny' },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'geolocation=(), camera=(), microphone=()');
next();
});
Register before any route handler so early-returning responses still carry the headers. Helmet sets X-Content-Type-Options: nosniff by default.
Diagnostic & Verification Steps
Confirm the grade from the command line so it is reproducible in CI, then confirm the underlying headers with curl.
Grade via the API (the X-Grade response header is the machine-readable grade):
curl -sI "https://securityheaders.com/?q=https%3A%2F%2Fexample.com&hide=on&followRedirects=on" \
| grep -i '^x-grade:'
Expected:
x-grade: A+
Confirm the headers the grader reads:
curl -sI https://example.com | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'
Expected:
strict-transport-security: max-age=63072000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), camera=(), microphone=()
If x-grade is A and every header above is present, the CSP still contains unsafe-inline or unsafe-eval in script-src — that is the only remaining gate to A+. In DevTools, the Network tab’s Response Headers for the document request must match this output for the exact URL the grader followed; a mismatch means the scanner graded a redirect target or a different edge node.
Edge Cases, Security Implications & Safe Rollback
The grade is a presence signal, and three traps make it misleading.
- A+ with a permissive CSP is false comfort. The A+ gate only rejects literal
unsafe-inline/unsafe-evalinscript-src. A policy ofscript-src 'self' https://cdn.examplescores A+ even if that CDN serves arbitrary user-uploaded JavaScript or hosts a JSONP endpoint that reflects a callback — both are full XSS bypasses. The grade certifies the token is absent, not that the allowlist is sound. Audit the allowlist by hand; tighten with nonces or hashes from the CSP reference. - Headers that matter but do not move the grade.
object-src 'none',base-uri 'none', cookieSecure/HttpOnly/SameSiteflags, and Cross-Origin-Opener-Policy are not part of the securityheaders.com letter calculation. Their absence leaves real holes (base-tag injection, cookie theft) that an A+ will never flag. Set them regardless of the grade. - Report-Only is ignored by the grader. A
Content-Security-Policy-Report-Onlyheader earns no credit; the grader reads only the enforcingContent-Security-Policy. If your grade shows “no CSP” while you ship a report-only policy, that is expected — promote the tested policy to the enforcing header once it is clean.
There is nothing destructive to roll back when interpreting a grade; the scanner is read-only. If a CI gate blocks a deploy on a grade regression, restore the missing header at the edge rather than lowering the threshold — the threshold exists to catch exactly this drift.
Frequently Asked Questions
I have all six headers but only get an A, not A+. Why?
The CSP contains unsafe-inline or unsafe-eval in script-src. That is the single content gate between A and A+. Move inline scripts to external files or adopt nonces/hashes and remove the unsafe token.
Does the max-age value of HSTS affect the grade?
No. securityheaders.com checks that the Strict-Transport-Security header is present, not the size of max-age. Use a long max-age for real protection regardless of the grade.
My report-only CSP is not counted — is that a bug?
No. The grader counts only the enforcing Content-Security-Policy header. Content-Security-Policy-Report-Only is for tuning before enforcement and earns no grading credit by design.
Is A+ proof my site is secure? No. A+ means six headers are present and the CSP forbids inline script tokens. It does not verify the CSP allowlist is bypass-resistant, that cookies are flagged, or that TLS is sound. Treat A+ as a floor, not a finish line.
Conclusion
Reach A+ incrementally: set nosniff and framing in staging, add HSTS with a short max-age and Referrer-Policy, then introduce an enforcing CSP — first in report-only to tune it, then promoted to the enforcing header without unsafe-inline. Only after the CSP is clean in production should you raise max-age to its long value and consider preload. Gate the X-Grade in CI so a future deploy cannot silently drop a header.