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:

Grade ladder from F to A+ A ladder showing which header unlocks each step from F up to A plus on securityheaders.com. F no headers C + nosniff + X-Frame-Options B + HSTS + Referrer-Policy A + CSP + Permissions-Policy A+ CSP without unsafe-inline Each rung adds the header that unlocks it.
Each grade step on securityheaders.com is unlocked by adding the named header; the final A+ step requires a CSP that forbids inline script.

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.

Caddy

example.com {
    header {
        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=()"
        -Server
    }
}

Caddy applies a top-level header block to every response it emits, including its own error pages and the automatic HTTP-to-HTTPS redirect, so there is no always equivalent to remember. The -Server line strips the identifying Server token — not graded, but one less fingerprint. If you serve a site under a handle or route block that sets its own headers, repeat the security headers there too, because Caddy evaluates the nearest matching header directive rather than merging them.

Diagnostic & Verification Steps

Confirm the grade from the command line so it is reproducible in CI, then confirm the underlying headers with curl. The grader is a pipeline: it fetches the URL you submit, follows every redirect, reads the headers on the final response, applies a presence check, then applies the one content gate on the CSP before writing the letter into the X-Grade header.

How securityheaders.com computes a grade A five-stage pipeline from scan request through header read, presence check and content gate to the X-Grade response header. 1 · Scan request, followRedirects=on curl -sI .../?q=URL 2 · Grader fetches your URL chases 301/302 to the final page 3 · Reads the final response headers error and redirect pages need them 4 · Presence check: six headers any missing lowers the letter 5 · Content gate: script-src clean? unsafe-inline/eval caps you at A X-Grade: A+ (clean) or A (unsafe) the machine-readable grade
The grader reads headers only from the final response after redirects, so a header that is missing on the redirect target or an error page reads as missing even when your app sets it.

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.

Two operational traps produce a lower grade than your config suggests. First, apex versus www: if you submit example.com but it 301s to www.example.com, the grade reflects the www host’s headers, not the apex — verify the vhost that actually terminates the request carries the full set. Second, the public analysis page caches results; append hide=on to keep the scan off the public results wall and re-run after a config change rather than trusting a stale letter. The API is rate-limited, so in CI space out calls and treat a missing x-grade header (rather than a low one) as a transient fetch failure to retry, not a regression to fail on.

Edge Cases, Security Implications & Safe Rollback

The grade is a presence signal, and three traps make it misleading. The letter rewards a fixed set of tokens; a large class of real hardening sits entirely outside the calculation, so a perfect score and a soft configuration can coexist.

What the grade scores versus what it ignores Two columns contrasting the headers that move the securityheaders.com letter against real hardening it never measures. Counts toward the letter Not scored (set it anyway) HSTS present CSP present (reaches A) script-src without unsafe-* X-Frame-Options: DENY X-Content-Type-Options Referrer-Policy Permissions-Policy object-src / base-uri 'none' Cookie Secure/HttpOnly/SameSite Cross-Origin-Opener-Policy CSP allowlist soundness Report-Only CSP (no credit) TLS and certificate quality
The left column is the entire basis of the letter; everything on the right is real hardening the grade never inspects, which is why an A+ is a floor rather than a verdict.

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.