HTTP headers you should set on every site
The response headers that make a site safer and faster: HSTS, Content-Security-Policy, X-Content-Type-Options, frame protection, Referrer-Policy, Permissions-Policy and Cache-Control, with Nginx and Caddy snippets.
Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?
Response headers are instructions to the browser that travel with every page. A handful of them close whole classes of attacks (clickjacking, protocol downgrade, content sniffing, data leaks through the Referer) and another handful decide whether the site is fast on the second visit. They are set once, in the web server or reverse proxy, and apply to everything behind it. Check any site’s current headers with curl -sI https://example.com.
Security headers
- Strict-Transport-Security (HSTS):
max-age=31536000; includeSubDomains. Tells the browser to use HTTPS for this host for the next year, even if a link or the user typeshttp://. Only send it over HTTPS and only when every subdomain is ready for HTTPS. Start with a shortmax-agesuch as300to test, because a wrong HSTS header is hard to take back. Add; preloadonly if you then submit the domain to the browser preload list. - Content-Security-Policy (CSP): controls where scripts, styles, images, frames and connections may load from. The strongest defense against cross-site scripting, and the most work. A sensible starting point is
default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self', then add the hosts you really use. Deploy first asContent-Security-Policy-Report-Onlyand watch the browser console for violations before enforcing. - X-Content-Type-Options:
nosniff. Stops the browser from guessing a file type and executing something that was served as text. No downside; always set it. - X-Frame-Options:
DENYorSAMEORIGIN. Prevents your pages being embedded in someone else’s iframe (clickjacking). Superseded by CSP’sframe-ancestors, but still useful for older browsers; set both. - Referrer-Policy:
strict-origin-when-cross-origin(the modern browser default, worth making explicit) sends the full URL to your own pages but only the origin to other sites, and nothing when going from HTTPS to HTTP. Useno-referrerif URLs contain anything sensitive. - Permissions-Policy: turns off browser features your site does not use, for your pages and any embedded frames:
camera=(), microphone=(), geolocation=(), payment=(). - Cross-Origin-Opener-Policy:
same-originisolates your window from pages that open it, which blocks some cross-window attacks. Add Cross-Origin-Resource-Policy:same-originfor assets that should not be embedded elsewhere. Needed for features such asSharedArrayBuffer.
Headers to remove
- Server:
nginx/1.24.0and X-Powered-By:ExpressorPHP/8.3.2tell attackers exactly which versions to look up. Nginx:server_tokens off;hides the version (removing the header entirely needs the headers-more module). Caddy:header -Server. Express:app.disable('x-powered-by'). PHP:expose_php = Offinphp.ini. - X-XSS-Protection: obsolete, removed from modern browsers and could introduce problems in old ones. Do not set it; if you inherit
X-XSS-Protection: 1; mode=block, deleting it is fine.
Caching headers
- Cache-Control on hashed assets (
app.3f2a9c.js, fonts, images that never change in place):public, max-age=31536000, immutable. The browser keeps them for a year and does not even revalidate. - Cache-Control on HTML:
no-cache(always revalidate, but may store) ormax-age=0, must-revalidate. Pages change and link to the new asset names, so they must not be stale. For personal or authenticated pages useprivate, no-store. - ETag or Last-Modified: let the browser revalidate with a conditional request and get a tiny
304 Not Modifiedinstead of the whole file. Nginx and Caddy send them for static files by default. - Vary: tells caches which request headers change the response, for example
Vary: Accept-Encodingwhen you compress, orVary: Originwhen CORS headers depend on the origin. Set automatically by most servers for compression. - Content-Type with a charset for text:
text/html; charset=utf-8. A missing charset is a source of garbled accents and, historically, encoding-based XSS.
Nginx
Set them in the server block of the HTTPS site. always makes Nginx add the header to error responses as well. Note the Nginx rule that bites everyone: if a location block contains any add_header of its own, it inherits none of the parent’s, so either repeat them or put them in an included file.
server {
listen 443 ssl;
http2 on;
server_name www.example.com;
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" 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;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
location ~* \.(js|css|woff2|png|jpg|svg|webp)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
include /etc/nginx/snippets/security-headers.conf;
}
location / {
add_header Cache-Control "no-cache";
include /etc/nginx/snippets/security-headers.conf;
proxy_pass http://127.0.0.1:3000;
}
}
In that layout, /etc/nginx/snippets/security-headers.conf contains the six add_header security lines, so each location gets both its cache header and the security set. Run sudo nginx -t before reloading.
Caddy
www.example.com {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "camera=(), microphone=(), geolocation=()"
-Server
}
@assets path *.js *.css *.woff2 *.png *.jpg *.svg *.webp
header @assets Cache-Control "public, max-age=31536000, immutable"
header Cache-Control "no-cache"
reverse_proxy localhost:3000
}
Caddy applies header directives to every response in the site, including from reverse_proxy, and the more specific matcher wins for Cache-Control. Caddy sends HSTS only if you add it; it does not set it by default.
Check your work
curl -sI https://www.example.com | sort
curl -sI https://www.example.com/static/app.js | grep -i cache-control
Online checkers such as securityheaders.com and the MDN HTTP Observatory grade a public site and explain each missing header. Aim for all security headers present before you tune the CSP; the CSP is the one that takes iteration.
Official documentation
- HTTP headers reference on MDN
- OWASP HTTP Headers Cheat Sheet
- Cache-Control on MDN
Did this work for you?
No votes yet. Be the first.
If it did not work, the report form below tells the editors what went wrong.
See also
- 0019 fetch with timeout, retries and proper error handling A reusable fetchJson helper for Node 20+ and browsers that adds a timeout, retries with backoff for temporary failures, and clear errors… Intermediate · Step by step · Any system Script Async code · Making requests 15 Oct 2026
- 0081 How an HTTP request works: methods, headers, and body An HTTP request is a short text message a client sends to a server: a method and path, some headers, and sometimes… Beginner · Explains an idea · Any system Article Making requests 15 Oct 2026
- 0083 Test a JSON API with curl A runnable bash script showing the curl commands you need most when testing an API: GET, POST JSON, custom headers, Bearer tokens,… Beginner · Step by step · Linux, macOS, Windows Script Authentication · Making requests 15 Oct 2026
- 0084 Tiny JSON echo server for testing HTTP clients A standard-library Python server that echoes back the method, path, query, headers, and body of every request, and can return any status… Intermediate · Step by step · Any system Script Files & automation · Making requests 15 Oct 2026
- 0085 fetch with timeout, retries, and exponential backoff A drop-in fetch wrapper that times out slow requests, retries network errors and 429/5xx responses with jittered exponential backoff, and honors Retry-After. Advanced · Step by step · Any system, Browser Script Async code · Making requests 15 Oct 2026
- 0087 CORS explained: why the browser blocks your request CORS is the browser rule that decides whether JavaScript on one origin may read a response from another. The server opts in… Intermediate · Explains an idea · Browser Article Async code · Making requests 15 Oct 2026
Repos in the wild
- Requests: HTTP for humans psf/requests Requests is the classic Python library for calling web APIs and downloading pages, with a simple API for GET and POST requests, headers, JSON and sessions. GitHub
- Axios axios/axios A promise-based HTTP client for browsers and Node.js with interceptors, automatic JSON handling, request cancellation and a consistent API in both environments. GitHub
- curl and libcurl (curl/curl) curl/curl The source of curl, the command-line tool for transferring data with URLs, and libcurl, the C library behind it that many other programs use for HTTP. GitHub
- Caddy: the web server with automatic HTTPS caddyserver/caddy Caddy is an open-source web server and reverse proxy written in Go that obtains and renews TLS certificates on its own, with a configuration file short enough to memorize and HTTP/1, 2 and 3 support out of the box. GitHub
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- Dominik +6,907 chars