Duvaxa

10 min read · Updated 26 Oct 2026

How to set HSTS and a Content-Security-Policy on nginx and Cloudflare

In short: Step-by-step configuration for HSTS and CSP on nginx and Cloudflare, with a rollout plan that doesn't break your site and the checks to run afterwards.

Free tool: Security headers check →

HSTS and Content-Security-Policy are the two security headers that matter most and the two most likely to cause trouble if added in a hurry. This guide shows the configuration for nginx and for Cloudflare, and the order to roll it out in so that nothing breaks.

Before you start

  • Every page and every subdomain you plan to cover must work over HTTPS with a valid certificate.
  • Know which third-party scripts, styles, fonts and frames your pages load (analytics, chat widgets, payment forms, maps).
  • Have a way to roll back the change quickly.

Step 1: redirect HTTP to HTTPS

HSTS is only honoured on HTTPS responses, so first make sure every HTTP request is redirected.

# nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

On Cloudflare, turn on SSL/TLS → Edge Certificates → Always Use HTTPS. Set the SSL/TLS encryption mode to Full (strict) so the connection from Cloudflare to your server is encrypted and verified too.

Step 2: add HSTS with a short max-age

Start with a max-age of a day. If something on a subdomain turns out not to work over HTTPS, browsers will forget the rule tomorrow.

# nginx, inside the server block that listens on 443
add_header Strict-Transport-Security "max-age=86400; includeSubDomains" always;

The always parameter makes nginx add the header to error responses as well. Note an nginx quirk: add_header directives are inherited from the outer level only if the inner block has none of its own. If a location block adds any header, repeat the security headers there, or put them in a shared file you include in each block.

On Cloudflare, go to SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS), enable it, choose a Max Age, and decide on Include subdomains and Preload. Cloudflare adds the header to every HTTPS response it serves for the zone.

Step 3: raise HSTS to six months or a year

After a few days with no problems, raise max-age to 15552000 (six months) or 31536000 (one year). Only add preload if you are certain every subdomain will stay on HTTPS for good: removal from browsers' preload lists takes months.

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Step 4: write a CSP in report-only mode

Start with a strict policy and see what it would block, without blocking anything yet. Content-Security-Policy-Report-Only does exactly that: the browser applies the policy, logs violations in the console and, if you add a report endpoint, sends them to you.

add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'; form-action 'self'" always;

Browse the site with the developer console open, or collect reports for a week. Each violation names the blocked address and the directive. Add the third-party hosts you genuinely need to the matching directive, for example a payment provider's script host to script-src and its frame host to frame-src.

Step 5: deal with inline scripts and styles

Most CSP work is inline code. Avoid 'unsafe-inline' for scripts: it allows any injected script and undoes most of the policy. Instead:

  • Move inline scripts into .js files served from your own domain.
  • Where an inline script must stay, allow it by its SHA-256 hash ('sha256-…'), which only works while the script is unchanged.
  • If your server renders pages per request, use a nonce: a random value per response, added to the header and to each script tag.
  • Inline styles are lower risk. Many sites allow 'unsafe-inline' for style-src while keeping script-src strict.

Step 6: enforce the policy

When the report-only policy runs clean, switch the header name to Content-Security-Policy. Keep a report-only copy of a stricter policy running alongside if you want to tighten further later.

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'; form-action 'self'" always;

Adding headers on Cloudflare without touching the server

If you can't change the server, Cloudflare can add response headers itself. In the dashboard, go to Rules → Transform Rules → Modify Response Header, create a rule for all incoming requests (or a hostname), and set each header as a static value. Cloudflare Workers and Pages projects can also set headers in code or through a _headers file.

The other headers, in the same place

add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
server_tokens off;

Check the result

  1. Run curl -sI https://example.com and confirm each header appears once, with the value you expect.
  2. Check an error page (a missing URL) and a static file too: headers are often missing there.
  3. Open the site in a private window with the developer console visible and look for CSP violations.
  4. Run our free security headers check for a graded report and the fixes for anything still missing.

Testing a CSP change safely

Report-only mode lets you run two policies side by side: the enforced policy you have today and a stricter report-only policy you'd like to move to. Violations of the stricter policy are reported but not blocked, so you can watch real traffic for a week before switching. To receive reports, add a report-to or report-uri directive pointing at an endpoint that stores them. Several hosted services collect CSP reports, or you can write a small endpoint that logs the JSON body.

Expect noise in the reports. Browser extensions inject scripts into pages and trigger violations that have nothing to do with your site; their reports usually show chrome-extension:// or moz-extension:// sources and can be ignored.

Nonces with nginx

If your pages are rendered by an application, generate a random nonce per response, add it to the header (script-src 'nonce-RANDOM') and to each script tag (nonce="RANDOM"). The application, not nginx, should do this, because it writes the HTML. For fully static sites, hashes work better: calculate the SHA-256 of each inline script at build time and list the hashes in the policy, as this website does.

Common mistakes

  • Putting add_header in the http block and then adding one header inside a location block, which silently drops all the inherited ones for that location.
  • Forgetting always, so 404 and 500 responses go out without security headers.
  • Adding HSTS on the port 80 server block, where browsers ignore it.
  • Using Cloudflare's HSTS setting and an origin HSTS header with different values.
  • Switching the Cloudflare SSL mode to Flexible, which leaves the connection from Cloudflare to your server unencrypted. Use Full (strict).
  • Allowing https: or * in script-src to make violations go away, which allows scripts from anywhere.

Rolling back

Keep the previous configuration so you can revert in one step. For CSP, switching back to Content-Security-Policy-Report-Only is the fastest rollback: protection pauses but nothing breaks. HSTS can't be rolled back for browsers that have already seen a long max-age, which is why you raise it gradually. Setting max-age=0 tells browsers to forget the rule on their next HTTPS visit.

FAQ

Questions

Why does nginx drop my headers in some locations?

add_header is inherited only when the inner block has no add_header of its own. Repeat the security headers in those blocks or include them from a shared file.

Should I set headers on Cloudflare or on nginx?

Either works; pick one place so they don't conflict. Cloudflare is easiest for HSTS; CSP is often kept with the application because it changes when the application changes.

What max-age should HSTS use?

Start with a day while testing, then six months (15552000) or a year (31536000).

Can I use 'unsafe-inline' just for now?

For styles it's a reasonable trade-off. For scripts it removes most of CSP's protection, so use hashes or nonces instead.

How do I know my CSP isn't breaking something?

Run it in report-only mode first and watch the browser console or the violation reports before enforcing it.

Tools for this

Products that help