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.

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.

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.