Deploying a valid TLS certificate encrypts traffic in transit, but encryption alone does not stop a browser from executing injected cross-site scripting (XSS) payloads, loading your checkout page inside a hidden clickjacking iframe, or leaking sensitive URL tokens in Referer headers. Those browser-side guardrails are controlled entirely by HTTP response headers. Yet when we inspect production web apps after a deployment or domain cutover, HTTP security headers are frequently missing or silently stripped by a child location block. Below is a practical engineering guide to auditing your HTTP response headers in 10 minutes, configuring the six essential headers in Nginx and Node/Express, and rolling out a strict Content-Security-Policy (CSP) without breaking third-party scripts.
Why Valid HTTPS Is Only Half of Browser Transport Security
As I covered in my earlier guides on how to check an SSL certificate in 10 minutes and fix common HTTPS errors and how to audit DNS records before a domain migration, infrastructure migrations often focus on DNS resolution and port 443 TLS handshakes. However, even with TLS 1.3 enabled, a user who types example.com into their address bar still initiates their very first request over plain http:// on port 80 unless their browser has cached a Strict-Transport-Security (HSTS) policy.
Even after the HTTPS redirect completes, modern browsers rely on explicit response headers from your origin server or CDN edge to know:
- Which script, style, connect, and frame origins are trusted (
Content-Security-Policy) - Whether another site is allowed to embed your application inside an
<iframe>(X-Frame-Optionsand CSPframe-ancestors) - Whether the browser may guess ("sniff") executable JavaScript out of an uploaded file labeled
text/plain(X-Content-Type-Options) - Which hardware APIs (camera, microphone, geolocation, payment) embedded scripts are allowed to invoke (
Permissions-Policy)
You can inspect all of these headers in one request from your terminal or by running your live URL through the PixelValut HTTP Security Headers Checker.
Comparison Table: The 6 Essential HTTP Security Headers in 2026
Bookmark this reference table when auditing any production web application or API gateway:
| Header Name | Attack Mitigated | Recommended Production Value | Common Misconfiguration Pitfall |
|---|---|---|---|
Strict-Transport-Security (HSTS) |
SSL stripping & first-visit http:// downgrade attacks |
max-age=63072000; includeSubDomains; preload |
Adding includeSubDomains when a legacy internal subdomain still runs on plain HTTP |
Content-Security-Policy (CSP) |
Cross-Site Scripting (XSS), malicious script injection & clickjacking |
default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' (plus vetted origins) |
Leaving 'unsafe-eval' and wildcard https: in script-src, which renders CSP ineffective |
X-Content-Type-Options |
MIME-type sniffing of user uploads into executable HTML/JS | nosniff |
Omitting the header on static asset buckets (S3/CDN) that serve user-uploaded files |
X-Frame-Options |
UI redressing / clickjacking in older browsers |
DENY or SAMEORIGIN
|
Setting ALLOW-FROM (deprecated and ignored by modern browsers; use CSP frame-ancestors instead) |
Referrer-Policy |
Leaking session tokens, reset links, or private paths via Referer
|
strict-origin-when-cross-origin |
Using unsafe-url, which sends the full URL query string to every external link and analytics pixel |
Permissions-Policy |
Unauthorized access to browser sensors & APIs by third-party scripts | camera=(), microphone=(), geolocation=(), payment=() |
Using the deprecated Feature-Policy header syntax instead of structured Permissions-Policy tuples |
Step 1: Inspect Your Live Response Headers with curl
Before editing any server blocks, inspect the exact headers returned on your final HTTPS destination (-L follows redirects, -I sends a HEAD request):
curl -sIL https://your-domain.com | grep -iE "^(http/|strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|server:|x-powered-by:)"
Look at two things in the output:
-
Are security headers only present on the
301redirect or on the finalHTTP/2 200response? Note that browsers ignoreStrict-Transport-Securityheaders sent over plainhttp://—HSTS must be returned on thehttps://response. -
Are information-disclosure headers leaking version numbers? If you see
Server: nginx/1.24.0orX-Powered-By: PHP/8.3.6, addserver_tokens off;in Nginx andexpose_php = Offinphp.ini(orapp.disable('x-powered-by')in Express).
Step 2: How to Roll Out a Content Security Policy (CSP) Without Breaking Production
Content-Security-Policy is the most powerful header in web security—and the easiest one to break a frontend with if you deploy it blindly. If your marketing team uses Google Tag Manager, Stripe Elements, or Cloudflare Turnstile, a naive default-src 'self' will block their scripts immediately.
The safe engineering workflow has three phases:
-
Generate a Baseline Policy: List every legitimate domain your frontend loads scripts, styles, fonts, images, XHR/fetch requests (
connect-src), and iframes (frame-src) from. You can build and validate the exact directive syntax interactively using the PixelValut CSP Generator. -
Deploy in
Report-OnlyMode First: Instead of enforcingContent-Security-Policyon day one, sendContent-Security-Policy-Report-Only. Browsers will execute all page resources normally while logging any policy violations to the DevTools console and your reporting endpoint. -
Switch to Enforcing Mode: Once 48–72 hours of production traffic show zero false-positive violations, rename the header from
Content-Security-Policy-Report-OnlytoContent-Security-Policy.
Step 3: Production-Ready Nginx Configuration (And Avoiding the add_header Inheritance Trap)
One of the most common Nginx bugs in production is silent header dropping. In Nginx, if you define add_header directives at the server block level, and then add even a single add_header inside a nested location block (such as add_header Cache-Control "public, immutable"; for static assets or a FastCGI block), Nginx drops all parent add_header directives for that location.
To fix this cleanly, keep your security headers in a dedicated snippet file (/etc/nginx/snippets/security-headers.conf) and include it in every location block that defines its own headers—or use the headers-more-nginx-module:
# /etc/nginx/snippets/security-headers.conf
server_tokens off;
# 1. Force HTTPS for 2 years across all subdomains
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# 2. Prevent MIME-sniffing & clickjacking
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# 3. Control Referer leakage & browser hardware APIs
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
# 4. Content Security Policy (adjust origins for your app)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.stripe.com; frame-src https://js.stripe.com; frame-ancestors 'self'; object-src 'none'; base-uri 'self';" always;
Always include the always parameter at the end of each add_header directive so Nginx sends the security headers on 4xx and 5xx error responses as well as 200 OK responses.
The 10-Minute HTTP Security Header Audit Checklist
Run through this checklist after every infrastructure change, CDN update, or framework upgrade:
- [ ] Verify HSTS on the
https://Response: ConfirmStrict-Transport-Securityhas amax-ageof at least31536000(1 year), and only enableincludeSubDomainsafter verifying every subdomain supports HTTPS. - [ ] Check Both HTML Routes and Static/API Endpoints: Test your homepage (
/), an authenticated API route (/api/v1/health), and a static asset (/assets/app.js) to ensure nested Nginxlocationblocks did not strip your security headers. - [ ] Eliminate
X-XSS-Protection: 1; mode=block: Modern browsers have removed their legacy XSS Auditor because it introduced cross-site leaks; either omitX-XSS-Protectionor set it to0while relying on a realContent-Security-Policy. - [ ] Lock Down
frame-ancestorsandX-Frame-Options: Set bothX-Frame-Options: DENY(orSAMEORIGIN) and CSPframe-ancestors 'none'(or'self') so neither modern nor legacy browsers allow third-party iframe embedding. - [ ] Strip
ServerandX-Powered-ByBanners: Remove exact web server and runtime version strings from response headers. - [ ] Run an Automated External Header Scan: Test your live domain with the PixelValut Security Headers Checker to verify every header directive and grade in seconds.
Frequently Asked Questions (FAQ)
Do I still need X-Frame-Options if I have frame-ancestors in my CSP?
Yes, keeping X-Frame-Options: DENY (or SAMEORIGIN) alongside CSP frame-ancestors is recommended for defense-in-depth. Modern browsers prioritize CSP frame-ancestors when both are present, while legacy clients and older embedded webviews still fall back to X-Frame-Options.
Why did my Nginx security headers disappear on my /static/ or /storage/ routes?
Because of Nginx's add_header inheritance rule: any location block that declares its own add_header (for example, add_header Cache-Control "public, max-age=31536000";) completely overrides all add_header statements defined in the parent server block. Move your security headers into a reusable snippet and include that snippet inside any location block that sets custom caching or CORS headers.
What is the safest max-age when turning on HSTS for the first time?
When enabling HSTS on a domain for the very first time, start with a short max-age=300 (5 minutes) or max-age=86400 (24 hours) without preload. Once you verify that no subdomains or internal tools break over HTTPS, increase max-age to 31536000 (1 year) or 63072000 (2 years).
Should JSON REST APIs return a Content-Security-Policy header?
Yes. Even if an API endpoint is only meant to return application/json, returning Content-Security-Policy: default-src 'none'; frame-ancestors 'none' along with X-Content-Type-Options: nosniff prevents browsers from ever rendering an API response as an HTML document or framing it if a route misbehaves.
How can I build a custom CSP header without memorizing every directive name?
Use the free PixelValut CSP Generator to select your allowed script, style, image, font, and connect sources visually and copy the ready-to-use Nginx, Apache, or Cloudflare header string.

Top comments (0)