Step-by-step instructions for adding HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy to Apache, Nginx, IIS, LiteSpeed, Caddy, OpenResty, Tomcat, Jetty, Node.js, Lighttpd, Traefik, and more.
Check Your Domain First
Run a free health check to see exactly which security headers your site is missing before making changes.
Run Free Domain Health Check →Jump to Your Web Server
Security Headers — Quick Reference
These are the six headers checked by our Domain Health Checker. Each has a recommended minimum value — your specific requirements may vary.
| Header | What it does | Recommended minimum value |
|---|---|---|
| Strict-Transport-Security | Forces HTTPS for future visits; prevents SSL-stripping attacks | max-age=31536000; includeSubDomains |
| Content-Security-Policy | Restricts which sources can load scripts, styles, images, etc. | default-src 'self' |
| X-Frame-Options | Prevents clickjacking by controlling iframe embedding | SAMEORIGIN |
| X-Content-Type-Options | Stops browsers MIME-sniffing a response away from the declared type | nosniff |
| Referrer-Policy | Controls how much referrer URL is sent on cross-origin requests | strict-origin-when-cross-origin |
| Permissions-Policy | Restricts browser feature access (camera, microphone, geolocation, etc.) | camera=(), microphone=(), geolocation=() |
default-src 'self' starting point will block inline scripts, external fonts, and third-party CDN assets. Start there, then relax specific directives as needed for your stack. Never use default-src * or unsafe-inline in production.
Apache httpd
Apache can set headers via mod_headers, which is enabled by default on most installations. Headers can go in the global config, a VirtualHost block, .htaccess, or a <Directory> block.
Enable mod_headers (if not already active)
Shell# On Debian/Ubuntu sudo a2enmod headers sudo systemctl restart apache2 # On CentOS/RHEL — verify this line exists in httpd.conf LoadModule headers_module modules/mod_headers.so
Add to VirtualHost or .htaccess
Apache<IfModule mod_headers.c> Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" 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 Permissions-Policy "camera=(), microphone=(), geolocation=()" Header always set Content-Security-Policy "default-src 'self'" </IfModule>
Header always set (not just Header set) so headers are sent on error responses (4xx/5xx) as well as normal 200 responses — important for HSTS and CSP.
Plesk users
Go to Domains → your domain → Apache & Nginx Settings. Paste the <IfModule mod_headers.c> block into the "Additional directives for HTTP" and "Additional directives for HTTPS" fields, then click OK. See our Plesk-specific guide for screenshots.
Nginx
Add headers in the server block (for a single domain) or the http block (for all domains on the server). Nginx does not require any module activation for basic header setting.
Nginxserver { # ... your existing server block config ... add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; add_header Content-Security-Policy "default-src 'self'" always; }
add_header directives in a child block (location) will override all add_header directives set in the parent server block — they do not inherit. If you have location blocks, repeat the security headers there or use ngx_http_headers_module's always flag carefully.
sudo nginx -t && sudo systemctl reload nginx
IIS (Internet Information Services — Windows)
IIS headers are set in web.config. Place this inside the <system.webServer> section.
web.config<system.webServer> <httpProtocol> <customHeaders> <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" /> <add name="X-Frame-Options" value="SAMEORIGIN" /> <add name="X-Content-Type-Options" value="nosniff" /> <add name="Referrer-Policy" value="strict-origin-when-cross-origin" /> <add name="Permissions-Policy" value="camera=(), microphone=(), geolocation=()" /> <add name="Content-Security-Policy" value="default-src 'self'" /> </customHeaders> </httpProtocol> </system.webServer>
Alternatively, set them via IIS Manager: Select your site → HTTP Response Headers → Add.
LiteSpeed & OpenLiteSpeed
LiteSpeed reads Apache-style .htaccess files and also has its own admin panel. The easiest approach is the same mod_headers syntax used for Apache:
.htaccess<IfModule mod_headers.c> Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" 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 Permissions-Policy "camera=(), microphone=(), geolocation=()" Header always set Content-Security-Policy "default-src 'self'" </IfModule>
For OpenLiteSpeed via the WebAdmin panel: go to Virtual Hosts → your vhost → Context, add a Static context for /, and add headers under Extra Headers. Or use the Rewrite Rules tab with Header always set ....
Caddy
Caddy sets headers with the header directive in your Caddyfile. Caddy also adds HSTS automatically for HTTPS sites — you may want to explicitly set it anyway for includeSubDomains.
Caddyfileexample.com { header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Frame-Options "SAMEORIGIN" X-Content-Type-Options "nosniff" Referrer-Policy "strict-origin-when-cross-origin" Permissions-Policy "camera=(), microphone=(), geolocation=()" Content-Security-Policy "default-src 'self'" # Remove server identification headers -Server } # ... rest of your Caddyfile ... }
Reload Caddycaddy reload --config /etc/caddy/Caddyfile
OpenResty
OpenResty is Nginx with Lua bundled in. Security headers work exactly the same as Nginx — use add_header in your server or http block. You can also set headers dynamically via Lua if you need conditional logic:
nginx.conf (OpenResty)server { # Standard add_header approach — identical to Nginx add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; add_header Content-Security-Policy "default-src 'self'" always; # Or via Lua (header_filter_by_lua_block) for conditional logic header_filter_by_lua_block { ngx.header["X-Custom-Security"] = "1" } }
Apache Tomcat
Tomcat handles security headers via the HttpHeaderSecurityFilter in web.xml (Tomcat 7+). Add this inside the <web-app> element:
web.xml<filter> <filter-name>httpSecurityFilter</filter-name> <filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class> <init-param> <param-name>hstsEnabled</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>hstsMaxAgeSeconds</param-name> <param-value>31536000</param-value> </init-param> <init-param> <param-name>hstsIncludeSubDomains</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>antiClickJackingEnabled</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>antiClickJackingOption</param-name> <param-value>SAMEORIGIN</param-value> </init-param> <init-param> <param-name>xContentTypeOptionsEnabled</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>httpSecurityFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
For CSP and Referrer-Policy (not covered by HttpHeaderSecurityFilter), add a custom filter or use a Spring Security / servlet filter:
Java Servlet Filter@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Content-Security-Policy", "default-src 'self'"); response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); response.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); chain.doFilter(req, res); }
Eclipse Jetty
Jetty provides a built-in SecuredRedirectHandler and supports response header customisation via jetty-web.xml or programmatically. The cleanest cross-version approach is a Filter in web.xml:
web.xml (Jetty)<filter> <filter-name>SecurityHeadersFilter</filter-name> <filter-class>org.eclipse.jetty.servlets.HeaderFilter</filter-class> <init-param> <param-name>headerConfig</param-name> <param-value> set Strict-Transport-Security max-age=31536000;includeSubDomains, set X-Frame-Options SAMEORIGIN, set X-Content-Type-Options nosniff, set Referrer-Policy strict-origin-when-cross-origin, set Content-Security-Policy default-src 'self' </param-value> </init-param> </filter> <filter-mapping> <filter-name>SecurityHeadersFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Node.js
The recommended approach for any Node.js application is the Helmet middleware package, which sets all major security headers in one line:
npmnpm install helmet
Express.js
JavaScript (Express)const express = require('express'); const helmet = require('helmet'); const app = express(); // Helmet sets HSTS, X-Frame-Options, X-Content-Type-Options, // Referrer-Policy, Permissions-Policy and a base CSP in one call. app.use(helmet()); // Customise CSP if the default is too strict for your stack: app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", 'cdn.jsdelivr.net'], styleSrc: ["'self'", 'fonts.googleapis.com'], }, }));
Fastify
JavaScript (Fastify)const fastify = require('fastify')(); fastify.addHook('onSend', async (request, reply) => { reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); reply.header('X-Frame-Options', 'SAMEORIGIN'); reply.header('X-Content-Type-Options', 'nosniff'); reply.header('Referrer-Policy', 'strict-origin-when-cross-origin'); reply.header('Content-Security-Policy', "default-src 'self'"); reply.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); });
Bare Node.js http module
JavaScript (bare http)const http = require('http'); http.createServer((req, res) => { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); res.setHeader('Content-Security-Policy', "default-src 'self'"); res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); // ... rest of your handler ... }).listen(3000);
Lighttpd
Enable mod_setenv and add headers in lighttpd.conf or a vhost include:
lighttpd.confserver.modules += ( "mod_setenv" ) $HTTP["scheme"] == "https" { setenv.add-response-header = ( "Strict-Transport-Security" => "max-age=31536000; includeSubDomains", "X-Frame-Options" => "SAMEORIGIN", "X-Content-Type-Options" => "nosniff", "Referrer-Policy" => "strict-origin-when-cross-origin", "Permissions-Policy" => "camera=(), microphone=(), geolocation=()", "Content-Security-Policy" => "default-src 'self'" ) }
Reload Lighttpdsudo service lighttpd reload
Traefik
Traefik handles security headers via middleware. Define a middleware in your static or dynamic config, then attach it to a router.
Dynamic config (YAML)
traefik/dynamic.ymlhttp: middlewares: securityHeaders: headers: stsSeconds: 31536000 stsIncludeSubdomains: true stsPreload: false frameDeny: false customFrameOptionsValue: "SAMEORIGIN" contentTypeNosniff: true referrerPolicy: "strict-origin-when-cross-origin" permissionsPolicy: "camera=(), microphone=(), geolocation=()" contentSecurityPolicy: "default-src 'self'" routers: myApp: rule: "Host(`example.com`)" middlewares: - securityHeaders service: myService
Docker Compose labels
docker-compose.ymllabels: - "traefik.http.middlewares.security.headers.stsSeconds=31536000" - "traefik.http.middlewares.security.headers.stsIncludeSubdomains=true" - "traefik.http.middlewares.security.headers.contentTypeNosniff=true" - "traefik.http.middlewares.security.headers.frameDeny=false" - "traefik.http.middlewares.security.headers.customFrameOptionsValue=SAMEORIGIN" - "traefik.http.middlewares.security.headers.referrerPolicy=strict-origin-when-cross-origin" - "traefik.http.middlewares.security.headers.contentSecurityPolicy=default-src 'self'" - "traefik.http.routers.myapp.middlewares=security"
HAProxy
HAProxy can set response headers in the frontend or backend section of haproxy.cfg:
haproxy.cfgfrontend https_in bind *:443 ssl crt /etc/ssl/certs/example.pem # Set security headers on all responses http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains" http-response set-header X-Frame-Options "SAMEORIGIN" http-response set-header X-Content-Type-Options "nosniff" http-response set-header Referrer-Policy "strict-origin-when-cross-origin" http-response set-header Permissions-Policy "camera=(), microphone=(), geolocation=()" http-response set-header Content-Security-Policy "default-src 'self'"
Reload HAProxysudo systemctl reload haproxy
Verify Your Headers After Deploying
After adding headers, always verify them are actually being sent. Three quick ways:
- Our free Domain Health Checker — runs a real HTTP check and grades each security header: Systron.net Domain Health Check Tool/
- curl from the command line —
curl -I https://example.com— the headers appear in the response output - Browser DevTools — F12 → Network → click the main document → Headers tab
Check Your Headers Are Live
Our free Domain Health Checker grades each security header individually — Good, Warning, or Missing — and links back to this article for anything that needs fixing.
Run Free Domain Health Check →Common Issues & Troubleshooting
Headers not appearing after adding them
- Did you restart/reload the web server? Config changes are not live until the process reloads.
- In Nginx, check for a
locationblock that overrides theserver-leveladd_headerdirectives — Nginx does not inherit headers from parent blocks when a child block has its ownadd_header. - In Apache, confirm
mod_headersis enabled:apache2ctl -M | grep headers - A CDN (Cloudflare, Cloudfront) may be stripping or caching a response that predates your change — try
curl -Idirectly against your origin IP to bypass the CDN.
CSP breaks my site (images/fonts/scripts stop loading)
Start with report-only mode so you can see violations without breaking anything:
CSP Report-OnlyContent-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Check your browser console for CSP violations, then expand the policy to allow each source you actually need before switching from -Report-Only to the enforcing header.
HSTS locks me out of HTTP
HSTS tells browsers to never connect over HTTP for the specified duration. If you need to revert to HTTP (e.g. SSL expired), browsers that have cached the HSTS policy will refuse. Start with a short max-age (e.g. 86400 = 1 day) while testing, then increase to 31536000 (1 year) once HTTPS is stable.