Back to tutorials
Tutorial

Nginx Security Headers Configuration Tutorial (2026): CSP, HSTS, and Safer Defaults on a VPS

Nginx security headers configuration tutorial (2026): set CSP, HSTS, and modern browser protections without breaking your site.

By Anurag Singh
Updated on Aug 08, 2026
Category: Tutorial
Share article
Nginx Security Headers Configuration Tutorial (2026): CSP, HSTS, and Safer Defaults on a VPS

One missing header can turn a small bug into a real incident. Think session theft, admin takeovers, and worse. If you host WordPress, a client portal, or any login page on Nginx, treat security headers as table stakes. Put them in your baseline config, not in the “we’ll harden it later” bucket.

This Nginx security headers configuration tutorial shows how to deploy HSTS, CSP, and safer defaults on an Ubuntu VPS. You’ll start with a low-risk header set and confirm it works. Then you’ll tighten it in small steps, without breaking checkout flows, embeds, or the WordPress admin.

What you’ll set up (and what you should not set blindly)

  • HSTS to force HTTPS (powerful; can lock you into HTTPS if misused)
  • Content Security Policy (CSP) to limit where scripts/styles/images can load from (easy to break if rushed)
  • Clickjacking protection via frame-ancestors (modern) or X-Frame-Options (legacy)
  • MIME sniffing protection with X-Content-Type-Options: nosniff
  • Referrer Policy to reduce URL leakage
  • Permissions Policy to disable unneeded browser APIs (camera, mic, etc.)

You will not paste an “ultra strict” CSP that instantly blocks Google Tag Manager, Stripe, or your WordPress editor. Instead, you’ll start in Report-Only mode. You’ll see what would be blocked, then tighten from there.

Prerequisites on your VPS

Assumptions for this tutorial:

  • Ubuntu 24.04 or 24.10 VPS, Nginx 1.24+ (Ubuntu packages) or newer
  • A working HTTPS site on Nginx
  • SSH access as a sudo user

If HTTPS isn’t set up yet, follow HostMyCode’s Let’s Encrypt setup guide tutorial (2026) first.

HSTS only helps once HTTPS is stable and renewals are reliable.

Need a clean VPS for Nginx work? Start with a HostMyCode VPS, then add these headers as part of your initial hardening checklist.

Step 1: Find your active Nginx server blocks and include points

On Ubuntu, common paths are:

  • /etc/nginx/nginx.conf
  • /etc/nginx/sites-available/ and /etc/nginx/sites-enabled/
  • /etc/nginx/conf.d/

List enabled sites:

ls -l /etc/nginx/sites-enabled/

Dump the full config. This is useful for spotting where include happens:

sudo nginx -T | less

Decide where headers should live:

  • Per-site headers in a file included by each server { } block (recommended for multi-site VPS)
  • Global headers in http { } (only if every site is the same and you understand inheritance)

Step 2: Create a safe baseline header file (works for most sites)

Create a dedicated snippet file. This keeps server {} blocks readable.

It also makes later audits much easier.

sudo nano /etc/nginx/snippets/security-headers-basic.conf

Paste this baseline set:

# /etc/nginx/snippets/security-headers-basic.conf
# Baseline security headers for HTTPS sites.
# Add inside the HTTPS server block (port 443).

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=(), payment=()" always;

# Legacy clickjacking protection (works widely)
add_header X-Frame-Options "SAMEORIGIN" always;

# Reduce data exposure in error pages and some older clients
add_header X-XSS-Protection "0" always;

Notes:

  • X-XSS-Protection is intentionally set to 0. Modern browsers ignore it or handle it inconsistently, and “enabling” it can create weird edge cases.
  • Permissions-Policy is deliberately conservative. If your site uses WebRTC, maps, or browser-based payment flows, you’ll need to adjust it.

Step 3: Add HSTS carefully (only after HTTPS is correct)

HSTS tells browsers to refuse HTTP. That’s what you want, once you’re sure you can support it.

The catch: browsers cache the policy. A bad rollout can linger.

Create a separate snippet so you can roll HSTS out in phases:

sudo nano /etc/nginx/snippets/security-headers-hsts.conf
# /etc/nginx/snippets/security-headers-hsts.conf
# Start with a short max-age while validating.
add_header Strict-Transport-Security "max-age=86400" always;

That’s a one-day policy. After you confirm stability (redirects, subdomains, certificate renewals), raise it gradually:

  • Week 1: max-age=86400 (1 day)
  • Week 2: max-age=604800 (7 days)
  • Week 3+: max-age=31536000 (1 year)

Avoid includeSubDomains until you’ve verified every subdomain you operate supports HTTPS.

Also avoid preload unless you understand the HSTS preload list and the (non-trivial) removal process.

Step 4: Add CSP in Report-Only mode first (so you don’t break production)

CSP is where you get meaningful XSS protection. It’s also where most rollouts go sideways.

Real sites often depend on more third-party resources than anyone remembers.

Create a CSP report-only snippet:

sudo nano /etc/nginx/snippets/security-headers-csp-report-only.conf
# /etc/nginx/snippets/security-headers-csp-report-only.conf
# Start permissive, then tighten after reviewing reports.
# Replace example.com with your domain and a real endpoint.

add_header Content-Security-Policy-Report-Only "default-src 'self'; \
base-uri 'self'; \
object-src 'none'; \
frame-ancestors 'self'; \
img-src 'self' data: https:; \
font-src 'self' data: https:; \
style-src 'self' 'unsafe-inline' https:; \
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; \
connect-src 'self' https:; \
upgrade-insecure-requests; \
report-uri https://example.com/csp-report" always;

This policy is intentionally forgiving. Use it to inventory what your pages actually load.

Once you have that list, drop 'unsafe-eval' first. Then work toward removing 'unsafe-inline'. For many sites, nonces/hashes are the end goal for scripts.

If you don’t want to run a report collector yet, you can still use Report-Only without report-uri. In that case, rely on browser devtools instead. It’s slower, but fine for smaller deployments.

Step 5: Include snippets in your Nginx HTTPS server block

Open your site config. Example:

sudo nano /etc/nginx/sites-available/example.com

Inside the server block for port 443, add:

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # SSL config ...

    include /etc/nginx/snippets/security-headers-basic.conf;
    include /etc/nginx/snippets/security-headers-hsts.conf;
    include /etc/nginx/snippets/security-headers-csp-report-only.conf;

    # your location blocks...
}

If you also have an HTTP server block that redirects to HTTPS, do not add HSTS there.

Browsers only accept HSTS over HTTPS.

Step 6: Validate configuration and reload safely

Test config syntax:

sudo nginx -t

Reload without dropping connections:

sudo systemctl reload nginx

Quick check the headers locally:

curl -I https://example.com

You should see strict-transport-security, x-content-type-options, and the rest in the response.

Step 7: Confirm headers aren’t being overwritten (common pitfall)

Nginx header inheritance can surprise you. This shows up most often when you set headers in multiple places (global + server + location).

A classic mistake is adding add_header inside a location block. You can end up dropping headers on other responses.

Use nginx -T and search for duplicates:

sudo nginx -T | grep -n "add_header" | head

If you run a reverse proxy or CDN, verify headers all the way to the browser. Some CDNs strip or normalize headers depending on rules.

If your stack is Nginx in front of another service, make sure your proxy chain preserves headers on upstream responses. HostMyCode’s Nginx real IP configuration tutorial (2026) is a helpful companion when a proxy sits between users and your origin.

Step 8: Tighten CSP from “Report-Only” to “Enforce” (a controlled rollout)

After a few days of normal traffic, you’ll have a realistic allow-list. Move carefully.

Change one thing at a time:

  1. Remove obvious dead allowances (domains that never appear).
  2. Replace broad https: allowances with specific domains (e.g. https://www.googletagmanager.com).
  3. Remove 'unsafe-eval' first. Many sites don’t need it.
  4. Only then work on removing 'unsafe-inline', which can be hard for WordPress and some page builders.

When you’re ready to enforce, create an enforced snippet:

sudo nano /etc/nginx/snippets/security-headers-csp.conf
# /etc/nginx/snippets/security-headers-csp.conf
add_header Content-Security-Policy "default-src 'self'; \
base-uri 'self'; \
object-src 'none'; \
frame-ancestors 'self'; \
img-src 'self' data: https:; \
font-src 'self' data: https:; \
style-src 'self' 'unsafe-inline' https:; \
script-src 'self' 'unsafe-inline' https:; \
connect-src 'self' https:; \
upgrade-insecure-requests" always;

Then swap the include line:

# include /etc/nginx/snippets/security-headers-csp-report-only.conf;
include /etc/nginx/snippets/security-headers-csp.conf;

Reload Nginx again:

sudo nginx -t && sudo systemctl reload nginx

WordPress-specific notes (what usually breaks)

WordPress admin and page builders often pull scripts from:

  • Your own domain (normal)
  • https://fonts.googleapis.com and https://fonts.gstatic.com (fonts)
  • Payment providers (Stripe, PayPal), analytics, tag managers
  • Embedded content providers (YouTube, Vimeo)

If you run WooCommerce with Stripe, start by allowing Stripe domains only where needed.

You can scope CSP per virtual host, or even per path. Do that only after the baseline is stable.

Performance tip: if you’re already using Nginx caching, keep CSP changes in mind when chasing “missing” or “stale” admin assets. This pairs well with HostMyCode’s Nginx caching tutorial (2026).

Quick diagnostic checklist: verify headers, redirects, and TLS behavior

  • HTTP → HTTPS redirect: curl -I http://example.com should return 301/308 to HTTPS.
  • HTTPS headers present: curl -I https://example.com shows HSTS and CSP (report-only or enforce).
  • No mixed content: browser console should be clean after you enable upgrade-insecure-requests.
  • Admin/login flows: log in, reset password, checkout (if e-commerce), embedded widgets.
  • Certificate renewals: schedule and verify renewals. If renewals fail, HSTS will amplify the pain.

If you’re seeing renewal failures, work through HostMyCode’s TLS certificate renewal troubleshooting tutorial (2026) before you bump HSTS to a year.

Hardening tip: don’t advertise your stack

You can also reduce what you disclose in responses. Nginx doesn’t leak much by default.

Still, disabling server tokens is sensible cleanup:

sudo nano /etc/nginx/nginx.conf
http {
    server_tokens off;
    # ...
}

Then reload Nginx.

Common mistakes (and how to avoid them)

  • Enabling long HSTS too early: keep max-age short until you’ve validated everything.
  • Setting CSP to “perfect” on day one: use Report-Only first. Let real traffic tell you what’s needed.
  • Using X-Frame-Options only: it’s fine as a baseline, but aim for frame-ancestors in CSP as the modern control.
  • Breaking ACME challenges: security rules can accidentally block /.well-known/acme-challenge/. Keep your Let’s Encrypt location rule simple.
  • Config sprawl: if you manage many vhosts, use snippets and keep per-site exceptions in one place.

Summary: the “good” order of operations for production

  1. Ensure HTTPS works reliably and renewals are automated.
  2. Deploy baseline headers (nosniff, referrer policy, permissions policy, clickjacking baseline).
  3. Enable short HSTS, then gradually increase.
  4. Enable CSP Report-Only, review, then enforce with minimal allowances.
  5. Re-test login, checkout, embeds, and admin tools after each tightening.

If you want this set up on infrastructure you control (with root access and predictable performance), use a HostMyCode VPS.

If you prefer less operational overhead, managed VPS hosting is a practical fit for teams that still need Nginx-level control.

If you’re standardizing headers across multiple sites, do it somewhere you can keep config consistent and reviewable. HostMyCode offers both a flexible HostMyCode VPS and hands-on managed VPS hosting for teams that want predictable hardening without manually tending every server.

FAQ: Nginx security headers on a VPS

Should I add security headers in http {} or per server {} block?

Per server {} is safer on multi-site servers. It prevents strict CSP or HSTS from bleeding into a dev vhost, staging subdomain, or legacy app.

Can I enable HSTS with includeSubDomains?

Only if every subdomain you control supports HTTPS and you’re prepared to keep it that way. Start without it, audit subdomains, then add it later.

Why start CSP in Report-Only mode?

Because production pages usually load third-party scripts you’ve forgotten about. Report-Only shows what would be blocked, without breaking the site.

Do these headers replace WAF or patching?

No. Headers reduce the impact of some attack classes (especially XSS and clickjacking), but you still need patching, backups, and access hardening.

How do I confirm my headers are visible to visitors behind Cloudflare?

Check response headers in browser devtools and with curl -I. If a CDN rule is rewriting headers, fix it there, then re-check end-to-end.

Nginx Security Headers Configuration Tutorial (2026): CSP, HSTS, and Safer Defaults on a VPS | HostMyCode