Security Headers & HTTPS Best Practices
Essential Security Headers
Header | Effect
--------------------------------|----------------------------------------------
Strict-Transport-Security | Force HTTPS, tell browser to always use HTTPS
Content-Security-Policy | Control what resources the page can load
X-Content-Type-Options | Prevent MIME sniffing
X-Frame-Options | Prevent clickjacking (mostly replaced by CSP)
Referrer-Policy | Control how much referrer info is sent
Permissions-Policy | Control browser feature access (camera, GPS)
Cross-Origin-Opener-Policy | Isolate browsing context (enable SharedArrayBuffer)HSTS (HTTP Strict Transport Security)
HSTS tells browsers to always use HTTPS for the domain, even if the user types http://. After first visit, the browser refuses plain HTTP connections.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000 — remember for 1 year (minimum for preload list)
includeSubDomains — apply to all subdomains
preload — submit to browser preload lists (hardcoded HTTPS before first visit)
To join the preload list: https://hstspreload.org
Warning: preload is difficult to undo — only add when all subdomains support HTTPSContent Security Policy (CSP)
Content-Security-Policy: default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.myapp.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /csp-report
Directives:
default-src — fallback for all other directives
script-src — JavaScript sources
style-src — CSS sources
img-src — Image sources
connect-src — fetch/XHR/WebSocket destinations
frame-ancestors — who can embed this page (replaces X-Frame-Options)
upgrade-insecure-requests — auto-upgrade http: resources to https:// Test CSP without enforcing — use Report-Only first
// Content-Security-Policy-Report-Only: <policy>; report-uri /csp-report
// CSP report endpoint
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body['csp-report']);
res.sendStatus(204);
});Setting Security Headers
// Next.js — next.config.js
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * blob: data:; font-src 'self'",
},
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};# Check headers with curl
curl -I https://example.com
# Test with securityheaders.com (automated scan)
# Test with Mozilla Observatory: observatory.mozilla.org
# Check specific header
curl -sI https://example.com | grep -i "strict-transport"Mixed Content
Mixed content occurs when an HTTPS page loads resources (images, scripts, styles) over HTTP. Browsers block mixed content — active content (scripts) always, passive content (images) with warnings.
<!-- Bad: HTTP resource on HTTPS page -->
<script src="http://cdn.example.com/app.js"></script>
<!-- Fix 1: Use HTTPS URL -->
<script src="https://cdn.example.com/app.js"></script>
<!-- Fix 2: Protocol-relative URL (inherits page protocol) -->
<script src="//cdn.example.com/app.js"></script>
<!-- Fix 3: CSP upgrade-insecure-requests header — auto-upgrades all http: to https: -->
<!-- Content-Security-Policy: upgrade-insecure-requests -->HTTPS Redirect Patterns
// Next.js middleware — redirect HTTP to HTTPS
import { NextResponse } from 'next/server';
export function middleware(request) {
if (request.headers.get('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
return NextResponse.redirect(`https://${request.headers.get('host')}${request.nextUrl.pathname}`, 301);
}
return NextResponse.next();
}# Nginx redirect
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
# Verify redirect chain (should be single redirect, not chain)
curl -IL http://example.comOCSP Stapling
OCSP (Online Certificate Status Protocol) lets clients check if a certificate is revoked. OCSP Stapling has the server pre-fetch the OCSP response and include it in the TLS handshake — faster and more private than client fetching it separately.
# Nginx OCSP Stapling config
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
# Verify stapling is working
openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null \
| grep -A 17 'OCSP response:'Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free