I deploy a lot of servers. Like, a lot. And every single time, before I open port 443 to the world, I add the same five HTTP security headers. Not because some blog told me to, not because a scanner flagged them, but because I got tired of cleaning up messes that could have been prevented by three lines in nginx.
Here are the ones I actually configure, and why.
Content-Security-Policy
This is the big one. CSP tells the browser where it's allowed to load resources from. Without it, any script injection ( and there will be script injections ) can phone home to whatever sketchy domain it wants.
Here's what I start with on most projects:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://images.unsplash.com; connect-src 'self'Tight default, explicit allows. If something breaks, I open it up one domain at a time. Never the other way around. Starting loose and tightening later means you'll never tighten later.
Strict-Transport-Security
HSTS. If you have HTTPS ( and you should ), this header tells browsers "always use HTTPS for this domain, don't even try HTTP". It kills those downgrade attacks where someone on the same coffee shop WiFi tries to strip your connection back to HTTP.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadOne year max-age, include all subdomains, and submit to the HSTS preload list. Once you're on the preload list, browsers won't even attempt HTTP to your domain. It's aggressive and I love it.
Warning: don't add preload until you're sure every subdomain works on HTTPS. You can't undo it quickly.
X-Content-Type-Options
One line, zero reason to skip it:
X-Content-Type-Options: nosniffBrowsers sometimes try to be "helpful" by guessing the content type of a response. Someone uploads a .txt file that's actually HTML? The browser might just render it as HTML and execute any scripts inside. Nosniff tells the browser: "don't guess, use what I told you." Three bytes of configuration that prevents a whole class of attacks.
X-Frame-Options
This one prevents clickjacking. If your site can be embedded in an iframe on a malicious domain, an attacker can overlay invisible elements on top of your legit buttons. Users think they're clicking "Download Report" but they're actually clicking "Grant Admin Access".
X-Frame-Options: DENYI use DENY on everything. If I actually need to embed something in an iframe ( rare ), I switch to SAMEORIGIN. But 99% of my projects? Nobody needs to iframe my site. DENY it.
Referrer-Policy
Most people don't think about this one, but it leaks information. When a user clicks a link on your site to go somewhere else, the browser sends a Referer header with the full URL they came from. That URL might contain session tokens, search queries, or internal paths you don't want shared.
Referrer-Policy: strict-origin-when-cross-originSame-origin requests still get the full referrer ( useful for analytics ). Cross-origin requests only send the origin, not the full path. It's a reasonable middle ground. If you're paranoid, use no-referrer. If you don't care, you're wrong.
The nginx config
I drop this in every server block. Here's the full block for nginx:
# /etc/nginx/snippets/security-headers.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://images.unsplash.com; connect-src 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;Then in my site config:
server {
listen 443 ssl http2;
server_name example.com;
include snippets/security-headers.conf;
# ... rest of config
}One include, five headers, done. I use the same snippet file on every server I manage.
What I skip
X-XSS-Protection. It's deprecated. Modern browsers don't need it, and it can actually make things worse. Skip it.
Permissions-Policy ( formerly Feature-Policy ). I add it when I need to restrict camera, microphone, or geolocation. But for most projects I deploy, those APIs aren't used, so I don't bother. Add it when you need it, not before.
Five headers. Two minutes. Every server. No excuses.
Test your headers at securityheaders.com. If you're not scoring A or A+, go back and fix it before you ship. :)