HTTP Security Headers: What They Are and How to Add Them
HTTP security headers like HSTS, CSP, and X-Frame-Options protect your site from clickjacking, XSS, and MIME sniffing. Learn how to add them on Nginx and Apache.
What are HTTP security headers?
HTTP security headers are directives sent by your web server in the HTTP response that instruct the browser to enable (or disable) certain security features. They are one of the easiest and most effective ways to protect your website and its users from common attacks like cross-site scripting (XSS), clickjacking, and data injection.
The good news: adding them is usually a few lines of server configuration. The bad news: most websites are missing at least some of them.
Essential security headers
Strict-Transport-Security (HSTS)
HSTS tells the browser to always use HTTPS when connecting to your site, even if the user types http://. This prevents protocol downgrade attacks and cookie hijacking over insecure connections.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
max-age=63072000-- remember this rule for 2 years (in seconds)includeSubDomains-- apply to all subdomainspreload-- eligible for browser HSTS preload lists (see hstspreload.org)
Important: Only enable HSTS after you are certain that HTTPS works correctly on your site and all subdomains. Once a browser receives this header, it will refuse to connect over HTTP for the specified duration. See our guide on TLS certificates to make sure your certificates are healthy first.
Content-Security-Policy (CSP)
CSP controls which resources (scripts, styles, images, fonts, frames) the browser is allowed to load. It is the most powerful defence against XSS attacks.
A strict CSP:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
A more permissive CSP (for sites using third-party scripts):
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; frame-ancestors 'none'
Tip: Start with Content-Security-Policy-Report-Only to test your policy without breaking the site:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
This logs violations without blocking anything, so you can fine-tune the policy.
X-Frame-Options
Prevents your site from being embedded in an iframe on another domain. This protects against clickjacking attacks.
X-Frame-Options: DENY
Options:
DENY-- never allow framingSAMEORIGIN-- allow framing only from your own domain
Note: frame-ancestors in CSP is the modern replacement, but X-Frame-Options should still be set for older browser compatibility.
X-Content-Type-Options
Prevents browsers from MIME-sniffing a response away from the declared Content-Type. This stops attacks where a browser interprets a file differently than intended (e.g., treating a text file as executable JavaScript).
X-Content-Type-Options: nosniff
There is only one valid value. Always set it.
Referrer-Policy
Controls how much referrer information is included when navigating away from your site. This prevents leaking sensitive URLs (with tokens, session IDs, or internal paths) to third-party sites.
Referrer-Policy: strict-origin-when-cross-origin
Common values:
no-referrer-- never send referrer informationstrict-origin-when-cross-origin-- send full URL for same-origin requests, only the origin for cross-origin requests, and nothing for downgrades (HTTPS to HTTP)same-origin-- only send referrer for same-origin requests
Permissions-Policy
Controls which browser features (camera, microphone, geolocation, payment, etc.) your site is allowed to use. This limits the damage if your site is compromised -- an attacker cannot enable the webcam, for example.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
The () means "disabled for all origins." Specify domains if needed:
Permissions-Policy: camera=(self "https://meet.yourcompany.com"), microphone=(self)
How to check your current headers
Using securityheaders.com
Visit Security Headers and enter your URL. It grades your site A+ to F and lists missing headers with explanations.
Using curl
curl -sI https://yourcompany.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"
Using browser DevTools
Open DevTools (F12) > Network tab > click any request > Headers tab. Check the response headers for security headers.
How to add headers on Nginx
Add these directives to your server block (usually in /etc/nginx/sites-enabled/ or /etc/nginx/conf.d/):
server {
listen 443 ssl http2;
server_name yourcompany.com;
# HSTS (2 years)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# CSP (adjust to your needs)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
# Clickjacking protection
add_header X-Frame-Options "DENY" always;
# MIME sniffing protection
add_header X-Content-Type-Options "nosniff" always;
# Referrer policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions policy
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# ... rest of your config
}
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
Important: The always parameter ensures headers are sent even on error responses (404, 500, etc.). Without it, Nginx only adds headers on successful responses.
How to add headers on Apache
Add these to your vhost configuration or .htaccess:
<IfModule mod_headers.c>
# HSTS (2 years)
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# CSP (adjust to your needs)
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
# Clickjacking protection
Header always set X-Frame-Options "DENY"
# MIME sniffing protection
Header always set X-Content-Type-Options "nosniff"
# Referrer policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Permissions policy
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
</IfModule>
Make sure mod_headers is enabled:
sudo a2enmod headers
sudo apachectl configtest
sudo systemctl reload apache2
Common mistakes
CSP too strict breaks the site
A CSP that blocks inline scripts ('unsafe-inline' not included in script-src) will break most websites that use inline JavaScript. Start with Content-Security-Policy-Report-Only, monitor violations, then tighten gradually.
HSTS before HTTPS is fully working
If you enable HSTS but your HTTPS is misconfigured (expired cert, mixed content), users will be locked out of your site for the duration of max-age. Always verify HTTPS is working on all pages and subdomains before enabling HSTS.
Missing "always" in Nginx
Without always, Nginx does not add headers to error responses. An attacker-triggered 404 page without security headers can still be exploited.
Duplicate headers
If headers are set in multiple places (main config, vhost, .htaccess, application code), they can conflict or duplicate. Check with curl -sI to verify the final output.
Forgetting subdomains
Your main domain might have perfect headers, but api.yourcompany.com or staging.yourcompany.com might have none. Check every subdomain.
Also ensure your TLS cipher suites are properly configured -- security headers and strong TLS work together to protect your users.
How SurfaceScan helps
SurfaceScan checks HTTP security headers on every web-facing host in your attack surface. It flags missing headers, weak configurations (like CSP with unsafe-eval), and HSTS that is set with a too-short max-age. Findings appear in the Vulnerabilities section with the specific header that is missing or misconfigured, the affected URL, and recommended values. Because SurfaceScan checks all your subdomains automatically, you catch gaps on staging or API servers that manual checks might miss.
Related articles
TLS Certificate Expired: How to Fix and Prevent
An expired TLS certificate causes browser security warnings. Learn how to renew it quickly with Let's Encrypt or commercial CAs, and prevent it from happening again.
Weak TLS Cipher Suites: How to Fix and Harden Your HTTPS
Weak TLS cipher suites like RC4 and 3DES leave your HTTPS connections vulnerable. Learn how to identify weak ciphers and configure strong ones on Nginx and Apache.