Back to tutorials
Tutorial

HTTP Security Headers Tutorial (2026): Add HSTS, CSP, and Permissions-Policy on Nginx/Apache for Safer Hosting

HTTP security headers tutorial for 2026: set HSTS, CSP, and key headers on Nginx/Apache with real configs and checks.

By Anurag Singh
Updated on Jun 28, 2026
Category: Tutorial
Share article
HTTP Security Headers Tutorial (2026): Add HSTS, CSP, and Permissions-Policy on Nginx/Apache for Safer Hosting

Most hosting breaches don’t start with a dramatic “hack.” They start with a browser doing what you implicitly allowed. That can mean loading scripts from anywhere. It can also mean allowing your pages to be framed or calling an endpoint over plaintext during a weird redirect.

This HTTP security headers tutorial shows how to add modern headers on Nginx or Apache in 2026. You’ll confirm they’re being sent, then roll them out without breaking WordPress, WooCommerce, or admin panels.

If you manage more than one site, set this once at the web-server layer. Then stop chasing app-level exceptions. Your reverse proxy or web server touches every response.

What you’ll build (and what it protects)

  • HSTS: forces HTTPS and prevents SSL stripping after the first secure visit.
  • Content-Security-Policy (CSP): reduces XSS impact by controlling where scripts, images, fonts, frames, and connections can load from.
  • Permissions-Policy: turns off browser features your site doesn’t use (camera, mic, geolocation).
  • Clickjacking protection: blocks your site from being framed by another origin.
  • MIME sniffing + referrer control: prevents content-type confusion and reduces data leakage.

You’ll also get a rollout pattern that won’t punish you. You’ll stage headers per site and run CSP in report-only mode first. You’ll also avoid the classic “enabled HSTS and locked myself out” mistake.

Prerequisites and a safe rollout plan

You’ll need:

  • A VPS or dedicated server where you can edit Nginx or Apache configs (Ubuntu 24.04/26.04 LTS, Debian 12/13, AlmaLinux 9/10, Rocky 9/10 are all fine).
  • A working TLS certificate for every hostname you’ll enforce HTTPS on.
  • Command-line access (SSH) and a way to reload the web server.

If you want control without spending your weekends on updates and restarts, managed VPS hosting is a practical middle ground. You still get root-level flexibility. You also get help with security and maintenance.

Rollout rule: start per-site, not globally. Confirm nothing breaks. Then consider a shared include.

If you’re also migrating or changing DNS soon, do the header work after cutover. That way you’re not debugging two variables at once.

(If you are planning a move, this pairs well with low-downtime DNS cutover steps.)

Step 1: Confirm HTTPS is truly complete (before HSTS)

HSTS is unforgiving. Before you enable it, make sure HTTPS works for:

  • https://example.com and https://www.example.com (and any other hostnames you’ll include)
  • All redirects (HTTP → HTTPS and canonical host redirects)
  • Renewal automation (Let’s Encrypt, AutoSSL, or your CA)

Quick checks:

curl -I http://example.com
curl -I https://example.com
curl -I https://www.example.com

HTTP should return a clean 301/308 to HTTPS. HTTPS should return 200/301 with no certificate warnings.

If Let’s Encrypt renewals are flaky, fix that first. This tutorial assumes TLS is stable. Otherwise you’re building your own downtime.

(For renewal failures, see SSL renewal troubleshooting.)

Step 2: Add headers on Nginx (per server block)

On Ubuntu/Debian, you’ll typically edit:

  • /etc/nginx/sites-available/example.com
  • then symlink to /etc/nginx/sites-enabled/

Inside the server block that handles HTTPS (port 443), add:

# --- Security headers (start conservative) ---
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;

# Prefer CSP Report-Only first; move to enforced CSP after you confirm.
add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' data: https:; frame-ancestors 'self'; base-uri 'self';" always;

# Clickjacking defense (CSP frame-ancestors is the modern control; this is a fallback)
add_header X-Frame-Options "SAMEORIGIN" always;

# HSTS: do NOT enable includeSubDomains/preload yet.
add_header Strict-Transport-Security "max-age=86400" always;

Why these values? They’re conservative by design. They also behave well on most WordPress setups, including sites behind a CDN.

The CSP still allows third-party resources over HTTPS for now. At the same time, it blocks framing and restricts base-uri.

Test and reload:

nginx -t
systemctl reload nginx

Verify the headers:

curl -sI https://example.com | egrep -i "strict-transport-security|content-security-policy|permissions-policy|x-frame-options|x-content-type-options|referrer-policy"

Step 3: Add headers on Apache (VirtualHost 443)

On Ubuntu/Debian, your vhost is often in:

  • /etc/apache2/sites-available/example.com.conf

Make sure headers module is enabled:

apache2ctl -M | grep headers || a2enmod headers

Then, inside the <VirtualHost *:443> block:

# --- Security headers (start conservative) ---
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()"

Header always set Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' data: https:; frame-ancestors 'self'; base-uri 'self';"

Header always set X-Frame-Options "SAMEORIGIN"

# HSTS: begin with 1 day during rollout.
Header always set Strict-Transport-Security "max-age=86400"

Validate and reload:

apache2ctl configtest
systemctl reload apache2

Step 4: Move CSP from Report-Only to Enforced (without guesswork)

A strict CSP can break payment widgets, analytics, tag managers, chat tools, and embedded maps. Use a workflow that makes breakage visible before users hit it:

  1. Run Content-Security-Policy-Report-Only for a few days.
  2. Collect violations (browser devtools + optional reporting endpoint).
  3. Tighten sources and remove https: wildcards where you can.
  4. Switch to enforced Content-Security-Policy.

Practical method (no external reporting required): open the site in Chrome/Firefox. Then check DevTools Console for CSP violations.

Don’t just test the homepage. Hit the pages that usually load the most third-party code:

  • Homepage
  • Login page (/wp-login.php)
  • Checkout/cart pages (WooCommerce)
  • Any page with embeds (YouTube, maps)

Once it’s quiet, swap the report-only header for an enforced policy:

add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https://cdn.example.com https://www.google-analytics.com; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:; frame-ancestors 'self'; base-uri 'self';" always;

The key change is intent. List the specific domains you trust, instead of allowing https: across the board.

Step 5: Upgrade HSTS safely (and avoid “preload” mistakes)

HSTS usually rolls out in three steps:

  • Stage A (testing): max-age=86400 (1 day)
  • Stage B (steady): max-age=15552000 (180 days)
  • Stage C (strict): max-age=31536000; includeSubDomains (1 year + subdomains)

Only use includeSubDomains if every subdomain supports HTTPS reliably. That includes old hosts like mail., webmail., cpanel., staging hosts, and forgotten microsites.

About preload in 2026: treat it like a one-way change for most organizations. Once you add preload and submit to the browser preload list, you’re committing to HTTPS everywhere long term.

That can be the right call. Wait until you’ve had months of clean renewals and predictable redirects.

Recommended “steady” header for most production sites:

add_header Strict-Transport-Security "max-age=15552000" always;

Step 6: Apply headers across multiple sites (includes, not copy/paste)

After the first site is stable, centralize the baseline. This keeps you from hand-editing ten vhosts.

Nginx include pattern:

  1. Create /etc/nginx/snippets/security-headers.conf:
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Strict-Transport-Security "max-age=15552000" always;
  1. Include it inside each HTTPS server block:
include /etc/nginx/snippets/security-headers.conf;

Apache include pattern: on Debian-based systems, you can add a site-local include or use conf-available.

cat > /etc/apache2/conf-available/security-headers.conf <<'EOF'
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Strict-Transport-Security "max-age=15552000"
EOF

a2enconf security-headers
apache2ctl configtest
systemctl reload apache2

Keep CSP per-site when you can. Different sites almost always depend on different third-party domains.

Step 7: WordPress and WooCommerce gotchas (what usually breaks)

These are the repeat offenders on hosted WordPress:

  • Inline scripts/styles: many themes use inline blocks. Start with 'unsafe-inline' for style-src, then tighten later.
  • Payment providers: Stripe, PayPal, Razorpay, and 3DS flows often need frame-src and connect-src allowances.
  • CDNs and image optimization: add your CDN hostname to img-src and font-src if fonts are served there.
  • Admin/editor tools: page builders may load assets from their own domains.

If you already have a staging copy, test the enforced CSP there first. HostMyCode’s staging workflow guide is a solid reference: WordPress staging site tutorial.

Step 8: Quick diagnostics checklist (10 minutes)

  • Headers present on 200 and 301? Use curl -I on both canonical and redirected URLs.
  • Headers missing on errors? Ensure you used always (Nginx) / Header always set (Apache).
  • Mixed content warnings? Fix those before increasing HSTS max-age.
  • Site embedded in other domains? If you need legitimate framing (partner portal), adjust frame-ancestors.
  • Admin panels impacted? Apply headers to public vhosts first. Don’t rush global changes on shared admin hosts.

If you suspect a broader compromise while you’re hardening, pause and triage first. You’ll get better results if you contain the issue and rotate credentials before tightening policies.

Keep this runbook handy: VPS incident response tutorial.

Step 9: Make it repeatable (change control + backups)

Security headers are configuration. Treat them like production code:

  • Keep a dated backup of changed config files (/etc/nginx/, /etc/apache2/).
  • Reload, don’t restart, unless you have to.
  • Document your CSP exceptions per site (a small SECURITY_HEADERS.md is enough).

If your server hosts revenue-critical sites, automate offsite backups before tightening policies. Use incremental backups with retention and regular restore drills.

See restic + S3 incremental backups for a practical approach.

Summary: the “good enough” header set for most hosting servers

If you want one baseline to deploy now, start here:

  • Enable now: X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options
  • HSTS: start at 1 day, move to 180 days after a week of clean renewals and redirects
  • CSP: run report-only, then enforce per site with explicit allowlists

Need a clean place to implement this (with predictable performance and root access)? Start with a HostMyCode VPS for full control, or move straight to managed VPS hosting if you’d rather not own every operational detail.

If you’re standardizing security across several sites, do it on infrastructure you can control end to end. HostMyCode’s VPS plans give you root access for Nginx/Apache tuning, and managed VPS hosting helps you keep updates, TLS, and hardening work on schedule.

FAQ

Will these headers break WordPress?

The baseline headers usually won’t. CSP is the one that can break themes and plugins. That’s why you start with Report-Only and tighten it in small steps.

Should I set HSTS with includeSubDomains?

Only if every subdomain you operate supports HTTPS and will keep supporting it. If you have old hosts like mail. or one-off staging subdomains, wait.

Do I need both X-Frame-Options and frame-ancestors?

frame-ancestors in CSP is the modern control. Keeping X-Frame-Options is a harmless compatibility fallback for older clients.

How do I verify headers quickly after changes?

Use curl -I for a fast check. Then confirm in your browser’s Network tab that the headers show up on real responses (including redirects and cached assets).

HTTP Security Headers Tutorial (2026): Add HSTS, CSP, and Permissions-Policy on Nginx/Apache for Safer Hosting | HostMyCode