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
- Run curl -sI https://example.com and confirm each header appears once, with the value you expect.
- Check an error page (a missing URL) and a static file too: headers are often missing there.
- Open the site in a private window with the developer console visible and look for CSP violations.
- 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
VendorPassport
Vendor security profileOne link instead of forty questionnaires. Suppliers get one verified, always up-to-date security profile to share with every customer, instead of filling the same long security questionnaire again and again.
- Who it's forSMB suppliers · Banks and NBFCs with many vendors
- What you getShareable passport page · Weekly re-checks · Buyer dashboard
StackAudit
Infrastructure and compliance auditOne audit. Every angle. Runs our checks together and gives you one report covering cloud cost, security, backups, compliance and readiness, with the most important fixes first and what they save.
- Who it's forCTOs preparing for funding or due diligence · Regulated companies (fintech, NBFC, health)
- What you getCombined audit report (HTML, print to PDF) · Each tool's full report · Top fixes with savings