Back to tutorials
Tutorial

Security Headers Setup Guide Tutorial (2026): Configure HSTS, CSP, and Clickjacking Protection on Nginx, Apache, or cPanel

Security headers setup guide tutorial for HSTS, CSP, and anti-clickjacking on Nginx, Apache, and cPanel—plus safe testing steps.

By Anurag Singh
Updated on Sep 04, 2026
Category: Tutorial
Share article
Security Headers Setup Guide Tutorial (2026): Configure HSTS, CSP, and Clickjacking Protection on Nginx, Apache, or cPanel

Your TLS can be perfect and your SSH locked down. You can still leak sessions, enable clickjacking, or make XSS easier than it should be. That’s the gap HTTP response headers close. This security headers setup guide tutorial shows you how to add the right headers on Nginx, Apache, and cPanel/WHM, plus a test workflow that won’t take your WordPress site offline.

You’re not aiming for “maximum security at any cost.” You’re aiming for a dependable baseline that survives real hosting. Think mixed plugins, CDNs, multiple vhosts, and client sites that change every week.

What you’ll set up (and what each header actually prevents)

Before you edit configs, be clear about what you’re buying. These headers won’t make a site “unhackable.” They do remove common browser-side failure modes.

  • HSTS (Strict-Transport-Security): forces browsers to use HTTPS and reduces SSL stripping risks.
  • CSP (Content-Security-Policy): limits where scripts/styles/images can load from, reducing XSS impact.
  • X-Frame-Options and/or CSP frame-ancestors: blocks clickjacking by preventing framing.
  • X-Content-Type-Options: nosniff: stops MIME sniffing quirks that can help attackers.
  • Referrer-Policy: reduces URL leakage (tokens, internal paths) through the Referer header.
  • Permissions-Policy: restricts browser features (camera, geolocation) that your site doesn’t need.
  • Cross-Origin-* headers (optional baseline): helps control resource sharing and data exposure across origins.

If you manage client sites, implement this where you can stage changes properly. If you want a clean place to do that without provider limits, start with a HostMyCode VPS and reuse the same rules across sites.

Prerequisites: confirm HTTPS, decide your rollout strategy

Don’t turn on HSTS until HTTPS is stable everywhere you serve content. That includes valid certificates, clean redirects, and no forgotten HTTP endpoints.

Watch for old APIs, webhooks, and “random” subdomains you still rely on.

  1. Confirm HTTPS works on the primary domain and www (if used).
  2. Inventory subdomains (mail, cpcalendars, autodiscover, staging, old app subdomains). Decide whether HSTS should cover them.
  3. Plan CSP rollout in “report-only” mode first, then enforce.
  4. Pick a test host: staging domain, or a single low-risk vhost first.

If certificate automation is unreliable, fix that first. Use this internal guide to resolve renewal failures: SSL renewal troubleshooting steps.

Baseline header set (safe defaults for most hosted websites)

This set is meant to be boring—in a good way. It usually won’t break real sites, and it gives you a solid floor.

You’ll tailor CSP later.

Strict-Transport-Security: max-age=86400
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
X-Frame-Options: SAMEORIGIN

Why is HSTS only 1 day? You want proof that redirects and cert renewals are stable before you lock browsers in for months. After a week of clean monitoring, raise it.

Note on clickjacking protection: modern practice prefers Content-Security-Policy: frame-ancestors. That said, X-Frame-Options is still widely enforced. On shared hosting and mixed client stacks, SAMEORIGIN is a sensible baseline.

Security headers setup guide tutorial for Nginx (single site)

On a typical Ubuntu/Debian Nginx install, edit the SSL server block for the site. Common paths include:

  • /etc/nginx/sites-available/example.com
  • /etc/nginx/conf.d/example.com.conf

Put the headers inside the server { } block that handles listen 443 ssl;.

Use always so headers are returned on error responses too. This helps on maintenance pages and on 4xx/5xx responses.

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

  # Baseline security headers
  add_header Strict-Transport-Security "max-age=86400" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
  add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
  add_header Cross-Origin-Opener-Policy "same-origin" always;
  add_header Cross-Origin-Resource-Policy "same-site" always;
  add_header X-Frame-Options "SAMEORIGIN" always;

  # (Optional) Start CSP in Report-Only mode first
  # 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:;" always;

  # Your existing location blocks...
}

Validate and reload:

nginx -t
systemctl reload nginx

Pitfall: if you run Nginx as a reverse proxy in front of Apache, set headers at the edge (Nginx). Avoid setting them again on Apache.

Duplicate headers confuse scanners and can create inconsistent browser behavior. If that’s your setup, follow this internal walkthrough: Nginx in front of Apache reverse proxy setup.

Apache 2.4 setup (Ubuntu/Debian/AlmaLinux/Rocky): headers + vhost placement

Apache needs mod_headers for this. Enable it, then reload.

# Ubuntu/Debian
a2enmod headers
systemctl reload apache2

On AlmaLinux/Rocky/CentOS Stream, mod_headers is often already available. Confirm it’s loaded, then reload:

httpd -M | grep headers
systemctl reload httpd

Add the headers inside your SSL vhost. You’ll usually find it here:

  • /etc/apache2/sites-available/example.com-le-ssl.conf (Ubuntu/Debian + certbot)
  • /etc/httpd/conf.d/ssl.conf or per-site file in /etc/httpd/conf.d/ (RHEL-family)
<VirtualHost *:443>
  ServerName example.com
  ServerAlias www.example.com

  # Baseline security headers
  Header always set Strict-Transport-Security "max-age=86400"
  Header always set X-Content-Type-Options "nosniff"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
  Header always set Cross-Origin-Opener-Policy "same-origin"
  Header always set Cross-Origin-Resource-Policy "same-site"
  Header always set X-Frame-Options "SAMEORIGIN"

  # (Optional) CSP as report-only first
  # 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:;"

  # Existing SSL config...
</VirtualHost>

Reload Apache:

# Ubuntu/Debian
systemctl reload apache2

# AlmaLinux/Rocky
systemctl reload httpd

cPanel/WHM method: apply headers without breaking client sites

cPanel complicates header management. You may have multiple web servers (Apache, LiteSpeed) plus account-level overrides.

You need an approach that survives rebuilds and doesn’t require editing every user’s .htaccess.

Recommended approach for most WHM servers: set a baseline at the server level. Add per-domain exceptions only when you have to.

Option A: Apache includes via WHM (best for consistency)

Many cPanel installs let you inject Apache config via includes. The UI labels can differ by version. The durable pattern stays the same: use an include file that cPanel preserves across rebuilds.

Create an include file (example path used by cPanel-managed Apache includes):

/etc/apache2/conf.d/includes/post_virtualhost_global.conf

Add baseline headers (Apache syntax):

# Global headers for SSL vhosts (verify scope on your server)
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always set X-Frame-Options "SAMEORIGIN"

Where to put HSTS? Scope HSTS to SSL vhosts only.

If your include applies to non-SSL vhosts too, don’t set HSTS globally. Put it in an SSL-only include/template or set it per vhost.

Rebuild and restart Apache using cPanel’s supported flow (exact command names can vary by version; common pattern is):

/scripts/rebuildhttpdconf
systemctl reload httpd

If you run LiteSpeed, headers often still follow Apache config. But they don’t always behave the way you expect.

Verify with curl (next section) rather than trusting assumptions.

Option B: Per-site .htaccess (only for single sites or exceptions)

For one WordPress site on shared hosting, adding headers in public_html/.htaccess is fine.

It’s also the right place for exceptions. For example, a client may genuinely need framing for an embedded app.

<IfModule mod_headers.c>
  Header always set X-Content-Type-Options "nosniff"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>

Pitfall: don’t stack the same header in .htaccess, vhost config, and global includes. Choose one source of truth, then override deliberately.

Roll out HSTS safely (and avoid locking yourself out)

HSTS works because browsers cache it. That also means a bad HTTPS change plus a long max-age can strand users until the cache expires.

  1. Start small: max-age=86400 (1 day).
  2. Verify redirects: HTTP should 301 to HTTPS for all key hostnames.
  3. Fix mixed content: update old hard-coded http:// assets.
  4. Increase gradually: 1 week → 1 month → 6 months.

Example Nginx header for a stable site:

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

Should you use includeSubDomains and preload? Only if you control every subdomain and you’re ready for the long-term operational commitment.

If clients create subdomains whenever they feel like it, it’s usually the wrong move.

Build a CSP without breaking WordPress (Report-Only first)

CSP is where sites most often break. Common casualties include checkout flows, analytics, page builders, and editors.

Treat it like a rollout, not a one-shot change. Start permissive in Report-Only, see what the site actually loads, then tighten.

Step 1: start with a tolerant Report-Only policy

This example allows common hosting patterns while giving you a structure to refine. It’s a first pass for a real WordPress/WooCommerce site, not a score-chasing policy.

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:;
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
  style-src 'self' 'unsafe-inline' https:;

Why the unsafe allowances? Many WordPress themes and plugins still ship inline scripts/styles. Some rely on eval-like behavior.

The point is to measure first, then remove allowances where you can.

Step 2: add a reporting endpoint (optional but useful)

If you don’t have a report collector, browser devtools will still get you there. A basic reporting endpoint just speeds up triage.

If you have an app path that can accept JSON reports, you can add:

Content-Security-Policy-Report-Only: ...; report-uri /csp-report;

Keep it simple: log it and rate-limit it. Don’t leave a wide-open endpoint that can be spammed.

Step 3: move to enforce, then tighten

After checkout, login, and admin workflows stay clean for a week, switch from Report-Only to Content-Security-Policy.

Then remove 'unsafe-eval' first (often doable). For higher-risk sites, work toward nonces/hashes for inline scripts.

If you need a safe change window while you tune policies, use your maintenance workflow so you don’t break orders: WordPress maintenance mode workflow.

Quick diagnostics: verify headers from the command line

Browser extensions are helpful, but they don’t replace checking the actual response. Test both the origin and any CDN layer in front of it.

Use curl to check headers

curl -I https://example.com

Confirm your header lines show up. Then hit a 404.

Headers disappearing on error pages is a common misconfig:

curl -I https://example.com/this-should-404

Check redirect chain (HTTP → HTTPS)

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

If you see anything other than a clean 301/308 to HTTPS, fix redirects before raising HSTS.

Common hosting pitfalls (and the exact fix)

  • Headers only appear on 200 responses: In Nginx, add always. In Apache, use Header always set.
  • Duplicate headers: Remove one layer. If Nginx is fronting Apache, set headers in Nginx and delete them from Apache vhosts.
  • CSP breaks admin/editor: Keep CSP off /wp-admin/ initially, or use a separate policy for admin paths.
  • Clickjacking header conflicts: Prefer frame-ancestors in CSP. If you must support embedding, set explicit allowed origins instead of disabling protection.
  • HSTS set too long on day one: Reduce to 1 day, validate, then increase. If you already set 6 months and HTTPS is broken, restore HTTPS first; browsers will keep enforcing.

Hardening checklist you can apply per site (5 minutes)

  • HTTP redirects to HTTPS for primary domain and www.
  • HSTS set to 1 day initially; plan a date to raise it.
  • X-Content-Type-Options, Referrer-Policy, Permissions-Policy set.
  • Clickjacking protection enabled (X-Frame-Options and/or frame-ancestors).
  • CSP deployed as Report-Only first (especially on WordPress/WooCommerce).
  • Validate with curl -I on 200 and 404 paths.

Where HostMyCode fits: implement once, then reuse

If you manage multiple sites, consistency is the real win. Aim for one baseline, one test routine, and predictable rollouts.

On a VPS you control, you can standardize headers across vhosts and keep changes in versioned config files.

For hands-on control (Nginx/Apache, per-site rules, and staging), choose a HostMyCode VPS. If you’d rather not maintain the OS and web stack yourself, managed VPS hosting is a practical middle ground for small teams.

If you’re rolling out security headers across client sites, do it on a VPS where you control Nginx/Apache behavior and can stage changes without surprises. HostMyCode offers a flexible HostMyCode VPS and managed VPS hosting if you want the hardening handled properly without spending your week babysitting the server.

FAQ

Will security headers break my WordPress site?

The baseline headers usually won’t. CSP is the one that can break things, which is why you should start with Content-Security-Policy-Report-Only and tighten slowly.

Should I set HSTS preload in 2026?

Only if you fully control all subdomains and you’re committed to permanent HTTPS. Preload is hard to roll back and can cause long-lived support issues in hosting environments.

Where should I set headers if I use Cloudflare or a CDN?

Set them at the edge if the CDN supports it, or at your origin server if you want one source of truth. Then verify what users receive with curl -I, because some CDNs modify or add headers.

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

You can run both. If you implement CSP, frame-ancestors is the modern control and offers more precision than X-Frame-Options.

How do I confirm headers are present on 404/500 pages?

Run curl -I https://example.com/this-should-404. If the headers disappear, adjust to add_header ... always (Nginx) or Header always set (Apache).

Summary: a safe rollout path you can repeat

Start with the baseline header pack. Verify with curl on both success and error responses.

Then raise HSTS gradually and roll out CSP in report-only mode before you enforce it. This order adds real protection without surprise outages.

If you want a clean environment to standardize these settings across sites, use a HostMyCode VPS for full control, or move to managed VPS hosting when you’d rather focus on the websites instead of the server.

Security Headers Setup Guide Tutorial (2026): Configure HSTS, CSP, and Clickjacking Protection on Nginx, Apache, or cPanel | HostMyCode