HTTP Security Headers — Complete Implementation Guide for All Major Web Servers Print

  • HTTP Security Headers, Apache, HAProxy, Eclipse Jetty, IIS, OpenResty, LiteSpeed/OpenLiteSpeed, Apache Tomcat, Node.js (Express/Fastify/bare http), Lighttpd, Nginx, Caddy, Traefik
  • 432

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.

HeaderWhat it doesRecommended 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=()
Note on Content-Security-Policy: The 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>
Use 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;
}
Important: In Nginx, 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.
Reload Nginx
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 ....

LiteSpeed's LiteSpeed Cache (LSCache) plugin for WordPress can also inject some security headers — check under LiteSpeed Cache → Tweaks → HTTP Response Header before adding headers twice.

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 linecurl -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 location block that overrides the server-level add_header directives — Nginx does not inherit headers from parent blocks when a child block has its own add_header.
  • In Apache, confirm mod_headers is enabled: apache2ctl -M | grep headers
  • A CDN (Cloudflare, Cloudfront) may be stripping or caching a response that predates your change — try curl -I directly 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.


Was this answer helpful?

« Back