Websites & apps

Add security headers in Nginx (HSTS, CSP, X-Frame-Options)

Add the HTTP security headers browsers use to protect your visitors, including HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and a starter Content Security Policy.

IntermediateLinux server5 min read

Security headers are short instructions your server sends with every page. They tell the browser to always use HTTPS, refuse to be framed by other sites, and only run scripts from places you trust. They cost nothing in speed and close off whole classes of attacks such as clickjacking and some cross-site scripting. This guide adds a solid set to Nginx in one reusable snippet.

Before you start

  • A Linux VPS with Nginx serving your site over HTTPS. Several of these headers only make sense on a secure site. See Redirect HTTP to HTTPS in Nginx.
  • SSH access with sudo.
  • A list of outside services your pages load from, such as Google Fonts, analytics or a CDN. You need it for the Content Security Policy.

The headers and what they do

  • Strict-Transport-Security (HSTS): tells browsers to use HTTPS for your domain for a set time, even if someone types http://.
  • X-Content-Type-Options: nosniff: stops browsers guessing a file's type, so an uploaded text file cannot be run as a script.
  • X-Frame-Options: controls whether other sites can show your pages in a frame. Prevents clickjacking.
  • Referrer-Policy: limits how much of your URL is shared with other sites when visitors click links.
  • Permissions-Policy: turns off browser features you do not use, such as the camera or geolocation.
  • Content-Security-Policy (CSP): lists the sources the browser may load scripts, styles, images and frames from. The most powerful header, and the one most likely to break things if rushed.

Create a reusable snippet

Put the headers in one file so every site can include it:

Bash
sudo mkdir -p /etc/nginx/snippets
sudo nano /etc/nginx/snippets/security-headers.conf
Nginx
add_header Strict-Transport-Security "max-age=300" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; frame-ancestors 'self'; object-src 'none'; base-uri 'self'" always;

Why the always flag matters

Without always, Nginx only adds headers to successful responses (200, 301, 302, 304 and a few others). Error pages such as 404 and 500 would go out without them. always adds them to every response.

Include it in your site

In the HTTPS server block of each site:

Nginx
server {
    listen 443 ssl;
    server_name example.com;

    include snippets/security-headers.conf;
}

Keep your existing directives in the block; only add the include line.

Warning: add_header inheritance is all or nothing. If a location block has even one add_header of its own (for example a Cache-Control header), it drops every header from the server block. Add include snippets/security-headers.conf; inside those locations too.

Roll out HSTS safely

The snippet starts HSTS at max-age=300, five minutes. Browsers enforce HSTS for the full time and visitors cannot click past a certificate error while it is active. Once you have confirmed HTTPS works on every page and renewals succeed, raise it in steps:

Nginx
add_header Strict-Transport-Security "max-age=31536000" always;

Add includeSubDomains only when every subdomain, including ones on other servers, has a valid certificate. The preload option asks browsers to hard-code your domain; it is very hard to undo, so only use it once you are certain.

Build a Content Security Policy

The snippet sends CSP as Report-Only. Browsers do not block anything yet; they print what would have been blocked in the developer console. That lets you tune the policy without breaking your site.

  1. Reload Nginx, open your site, press F12 and look at the Console.
  2. Each warning names a blocked source, such as https://fonts.googleapis.com for styles.
  3. Add trusted sources to the matching directive, for example style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com.
  4. Browse every type of page: forms, checkout, admin, embedded videos.
  5. When the console is clean, rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy.

WordPress and many page builders rely on inline scripts. You may need 'unsafe-inline' in script-src at first. It weakens the policy, but the other directives still help.

frame-ancestors 'self' is the modern version of X-Frame-Options. Keeping both supports older browsers.

Apply the change

Bash
sudo nginx -t && sudo systemctl reload nginx

Check it works

Bash
curl -sI https://example.com/ | grep -iE "strict-transport|x-content-type|x-frame|referrer-policy|permissions-policy|content-security"

All six headers should appear. Check a missing page too, to confirm always works:

Bash
curl -sI https://example.com/no-such-page | grep -i x-frame

For a graded report, run your domain through an online security header scanner such as Mozilla's HTTP Observatory or securityheaders.com.

Troubleshooting

  • Headers appear on some pages but not others: a location has its own add_header, which drops the inherited ones. Include the snippet in that location.
  • Headers appear twice: your app (WordPress plugin, framework middleware) also sets them. Keep them in one place.
  • Site layout breaks after switching CSP to enforce: a source is missing. Switch back to Report-Only, read the console and add it.
  • Embedded pages on your other domain refuse to load: X-Frame-Options SAMEORIGIN and frame-ancestors 'self' block them. Add the other domain to frame-ancestors and remove X-Frame-Options for that site.
  • Visitors cannot reach the site after a certificate problem: HSTS forces HTTPS. Fix the certificate; you cannot switch HSTS off for browsers that already saw it.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.