Why Security Headers Matter
HTTP security headers tell the browser how to handle your site's content safely. Configured correctly, they shut down a whole class of attacks: an attacker plants a script in the comments section and the browser refuses to run it because of Content-Security-Policy; a third-party site tries to embed your login page in an iframe and X-Frame-Options blocks it; an image turns out to be HTML and X-Content-Type-Options: nosniff stops the browser from second-guessing the content type.
None of these headers slow down your site or require touching business code — the cost is essentially zero, and the payoff is a shrunken attack surface. Below are complete Nginx and Apache configurations plus an explanation of each header.
Complete Nginx Security Header Configuration
# /etc/nginx/nginx.conf or the server block of your site config
# === Core security headers ===
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Legacy XSS filter (deprecated, kept for compatibility)
add_header X-XSS-Protection "0" always;
# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permission control (restrict browser APIs)
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
# === HSTS (HTTP Strict Transport Security) ===
# Enable only after HTTPS is confirmed working
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# === CSP (Content Security Policy) ===
# Tune to your needs; too strict can break functionality
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
style-src 'self' 'unsafe-inline' https:;
img-src 'self' data: https:;
font-src 'self' data: https:;
connect-src 'self' https:;
frame-ancestors 'self';
form-action 'self';
base-uri 'self';
" always;
Apache Configuration
# .htaccess or httpd.conf
# Core security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header Reference
| Header | Purpose | Recommended Value | Risk |
|---|---|---|---|
X-Frame-Options |
Prevent clickjacking (page embedded in an iframe) | SAMEORIGIN |
Low |
X-Content-Type-Options |
Prevent MIME sniffing attacks | nosniff |
Low |
Strict-Transport-Security |
Enforce HTTPS connections | max-age=63072000; includeSubDomains |
Medium |
Referrer-Policy |
Control the HTTP Referer header | strict-origin-when-cross-origin |
Low |
Permissions-Policy |
Restrict browser API access | Restrict per need | Low |
Content-Security-Policy |
Prevent XSS and data injection | Site-specific | High |
What each header really does
X-Frame-Options is the oldest clickjacking defense: SAMEORIGIN only lets same-origin pages embed yours in an iframe. Modern browsers also support the frame-ancestors directive (inside CSP); when both exist, CSP wins — so set both.
Strict-Transport-Security (HSTS) tells the browser "this domain must only ever be reached over HTTPS," eliminating the downgrade risk on the very first HTTP request. max-age=63072000 is 730 days, includeSubDomains covers subdomains, and preload submits the domain to the browser's built-in HSTS preload list. Caution: once HSTS is live, the browser forces HTTPS — if a subdomain hasn't got a certificate yet, users simply can't reach it. That's why it's rated medium risk.
Referrer-Policy controls how much referrer information leaks when a page navigates. strict-origin-when-cross-origin is the default sweet spot: full URL on same-origin, only the origin cross-origin, and nothing when going from HTTPS to HTTP.
Permissions-Policy replaces the old Feature-Policy and restricts browser APIs like camera, microphone, and geolocation. Setting unused capabilities to empty shrinks what malicious scripts can reach.
Content-Security-Policy is the most complex and most powerful header, and the one most likely to break your page. It declares resource sources per directive: script-src for scripts, style-src for styles, img-src for images, connect-src for fetch/XHR. Being too strict nukes inline scripts, CDN assets, and third-party analytics alike — so always "report first, tighten later."
Order matters when rolling out, too: enable everything in staging first, watch for a week of regressions and console errors, then roll out to production gradually. Once a header is sent, the browser remembers state like HSTS, so rollback isn't instant — "small steps, progressively stricter" beats a one-shot lockdown. With CSP in particular, leave a transition period in Content-Security-Policy-Report-Only mode to collect violation reports and see which resources real pages actually trigger, then decide which domains deserve whitelisting.
CSP Configuration Guide
Relaxed policy (suits most sites)
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' https:;
style-src 'self' 'unsafe-inline' https:;
img-src 'self' data: https:;
font-src 'self' data: https:;
connect-src 'self' https:;
frame-ancestors 'self';
form-action 'self';
" always;
Strict policy (for security-sensitive sites)
add_header Content-Security-Policy "
default-src 'none';
script-src 'self';
style-src 'self';
img-src 'self';
connect-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
" always;
Verification Tools
- securityheaders.com — Header rating
- CSP Evaluator — CSP policy assessment
- SSL Labs — SSL/TLS configuration rating
curl -sI https://example.com | grep -i "^\(x-\|strict\|content\|referrer\|permissions\)"— local check
Verifying After Configuration
- Confirm every header is returned:
curl -sI https://your-domain.com | grep -i "content-security\|strict-transport\|x-frame\|x-content-type\|referrer-policy\|permissions-policy"; - Open securityheaders.com, enter the domain, and check the grade is A or A+;
- Paste your CSP into CSP Evaluator and look for obvious holes (like mixing
'unsafe-inline'with external scripts); - Finally walk through your main pages, login flow, and payment flow in a real browser to make sure nothing broke.
FAQ and Troubleshooting
Subdomains unreachable after enabling HSTS? Make sure each subdomain has a valid HTTPS certificate before adding includeSubDomains. In an emergency, removing HSTS won't clear clients' cached enforcement until max-age expires — be extra careful with production changes.
CSP too strict and the page styles are a mess? Switch back to the relaxed policy, or use report-uri/report-to to collect violation reports first, see which resources are blocked, whitelist them one by one, then tighten.
Headers set but curl doesn't show them? Check whether Nginx's add_header lives inside a location block — add_header in location overrides the server-level header of the same name. On Apache, confirm mod_headers is enabled.
A Real-World Case
A content site kept getting injected with malicious scripts: attackers stuffed <script src="https://evil.example/x.js"> into article bodies. After enabling a strict CSP (script-src 'self'), external scripts were refused outright; combined with X-Content-Type-Options: nosniff and Referrer-Policy, the injection attack surface effectively closed. The whole change took about an hour with zero production incidents.
Configuration Checklist
- X-Frame-Options: SAMEORIGIN
- X-Content-Type-Options: nosniff
- Strict-Transport-Security configured
- Referrer-Policy configured
- Permissions-Policy configured
- Content-Security-Policy configured and tested
- securityheaders.com grade A or A+
- Site functionality verified after configuration