← Back to blog
28 June 2026

HTTP Security Headers in .htaccess: Copy-Paste Config

A plain walkthrough of HTTP security headers on cPanel shared hosting: the full .htaccess block, what each header actually does, how CSP will break your site before it helps it, and real before/after numbers from a South African cPanel site.

Why Security Headers Matter (and What Google Lighthouse Actually Flags)

Run a Lighthouse audit on most South African WordPress or static sites hosted on cPanel shared hosting and you will get a passing performance score alongside a Best Practices score of 78 or lower. Dig into the warnings and you will find the same cluster of issues every time: missing Content Security Policy, no HSTS, no X-Frame-Options.

These are not theoretical problems. HTTP Archive Web Almanac 2024 security data shows that the majority of sites on the public web still lack both CSP and HSTS. South African shared-hosting sites are probed constantly by automated scanners looking for clickjacking candidates, XSS injection points, and mixed-content pages. The scanners do not care that your site is small or local.

Lighthouse flags the absence of a CSP under Best Practices. It also flags missing X-Content-Type-Options. The securityheaders.com scanner gives you a letter grade (F through A+) based on which headers are present. An unprotected cPanel site typically lands on a D or F out of the gate.

The fix is a single .htaccess block. Apache on cPanel supports mod_headers, which is what you need for Header set directives. On most WHM/cPanel stacks it is already enabled. If your directives are silently ignored, that is the first thing to check.

The Full .htaccess Block: Copy-Paste and Go

Put this inside your document root .htaccess file, before any WordPress rewrite rules. The block below is safe to deploy on a standard cPanel shared host running Apache 2.4 with mod_headers enabled.

# -------------------------------------------------------
# HTTP Security Headers
# Requires mod_headers (enabled by default on most cPanel stacks)
# -------------------------------------------------------
<IfModule mod_headers.c>

    # Force HTTPS for 1 year, include subdomains, allow preload
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

    # Prevent MIME-type sniffing
    Header always set X-Content-Type-Options "nosniff"

    # Block iframe embedding on other domains (clickjacking protection)
    Header always set X-Frame-Options "SAMEORIGIN"

    # Control referrer data sent to third parties
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    # Restrict browser feature access
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"

    # Content Security Policy (permissive start, tighten after testing)
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none';"

</IfModule>

A few things to note before you save and upload. The always keyword tells Apache to set the header on all responses including error pages, not just 200 OK. Without it, your 301 redirects and 404 pages leave the headers off. The IfModule wrapper means the block fails silently if mod_headers is missing rather than returning a 500.

The CSP in that block is intentionally loose. It allows inline scripts and eval, which you need for most WordPress sites. You will tighten it once you know exactly what your site loads. More on that below.

Header by Header: What Each One Does and Why

Strict-Transport-Security (HSTS)

HSTS tells the browser: for the next 31536000 seconds (one year), connect to this domain over HTTPS only, no exceptions. If a user types http://yourdomain.co.za, the browser upgrades the request before it leaves the device. No round-trip to your server, no opportunity for a downgrade attack.

includeSubDomains extends this to every subdomain. Only add it if all your subdomains have valid SSL certificates. On AutoSSL-managed cPanel hosts this is usually fine. preload lets you submit the domain to browser preload lists so the HTTPS-only behaviour is baked into Chrome and Firefox even before the first visit.

One warning: do not set HSTS until you have a working SSL certificate. If you deploy HSTS on an HTTP-only site, you will lock visitors out for a year.

X-Content-Type-Options

nosniff stops the browser from guessing the MIME type of a response. Without it, a browser might execute a file served as text/plain as JavaScript if the content looks script-like. Attackers exploit this by uploading disguised files. One directive, no trade-offs, no breakage risk.

X-Frame-Options

DENY blocks your site from loading inside any iframe on any domain. SAMEORIGIN allows iframes from your own domain only. Use SAMEORIGIN if you embed your own pages in iframes somewhere (a common pattern for checkout pages or embedded forms). Use DENY if you have no iframes at all.

Clickjacking attacks layer an invisible iframe over a legitimate page and trick users into clicking buttons they cannot see. The attack is old but still active. This header kills it.

Referrer-Policy

strict-origin-when-cross-origin sends the full URL as the referrer when navigating within your own domain, but trims it to just the origin (no path, no query string) when linking to external sites. This prevents leaking sensitive URL parameters (think /reset-password?token=abc123) to third-party analytics or ad networks.

Permissions-Policy

Permissions-Policy replaced the old Feature-Policy header. It controls whether browser APIs like the camera, microphone, geolocation, and payment handler are available to your page and any embedded iframes. Setting them to empty parentheses disables them. If you run a site that genuinely needs geolocation, adjust accordingly.

Content-Security-Policy: The One That Will Break Your Site

Every other header in this list is safe to add immediately. CSP is different. A strict CSP will break inline scripts, Google Analytics, Facebook Pixel, embedded fonts, and anything else that does not match your allowlist. Deploy CSP wrong and your site goes white for visitors while you get no error in your cPanel error log because the browser blocked it silently.

The correct approach, documented by Google, is to start with Report-Only mode. This sends the same CSP header but does not enforce it. Violations get reported to a URL you control (or logged in the browser console) without breaking anything live.

# CSP Report-Only: test your policy before enforcing it
# Violations appear in browser DevTools console (Security or Console tab)
# Replace the enforced Content-Security-Policy header with this during testing
<IfModule mod_headers.c>
    Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'none';"
</IfModule>

Leave Report-Only running for a week. Open DevTools on your site and look for CSP violation messages in the console. Every blocked resource tells you exactly what to add to your allowlist. Once you stop seeing violations, flip Content-Security-Policy-Report-Only to Content-Security-Policy.

The directives that cause the most breakage on typical cPanel WordPress sites:

One South African-specific gotcha: if you use PayFast, you need form-action payment.payfast.io https://www.payfast.co.za in your CSP. PayFast redirects the form POST to their domain and a strict form-action 'self' will kill the transaction without any obvious error on screen.

Testing Your Headers: securityheaders.com, Lighthouse, and curl

Once the block is in place, confirm the headers are actually being sent. The fastest way is curl from your local terminal:

# Check which security headers your site is sending
# -I fetches headers only, -L follows redirects
curl -I -L https://yourdomain.co.za 2>/dev/null | grep -iE "(strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy)"

# Expected output (example):
# strict-transport-security: max-age=31536000; includeSubDomains; preload
# x-content-type-options: nosniff
# x-frame-options: SAMEORIGIN
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: camera=(), microphone=(), geolocation=(), payment=()
# content-security-policy: default-src 'self'; ...

If you see nothing in the grep output, mod_headers is disabled or your .htaccess changes are not being picked up. On cPanel hosts, .htaccess files are read from the document root of the domain. If your WordPress install is in a subdirectory, the block needs to go in that subdirectory's .htaccess, not the parent.

For a scored report, paste your domain into securityheaders.com. It grades each header and tells you what is missing. An A rating requires all six headers covered in this post. An A+ requires HSTS preloading.

Lighthouse (in Chrome DevTools or via npx lighthouse https://yourdomain.co.za --view) will update your Best Practices score once CSP and X-Content-Type-Options are present. The score improvement is typically 10 to 20 points depending on what else is flagged.

Real Results: Before/After on a South African cPanel Site

One of the sites we manage on cPanel shared hosting is a small WordPress business site running on LiteSpeed with Cloudflare in front. Before adding security headers, the numbers looked like this:

The missing headers were not causing visible user problems. The site loaded fine. But scanners were finding it regularly. Wordfence logs showed probing for known XSS vectors about 40 times a day from rotating IPs, mostly in Eastern Europe and Asia. Not targeted, just automated noise looking for misconfigured sites.

After adding the full block above (with a permissive CSP to avoid breakage):

The three violations were easy fixes: add the CDN domain to script-src, convert the inline handler to an external script. After those two changes the console was clean and we flipped from Report-Only to enforced CSP.

What we gave up: nothing that mattered. The headers added about 400 bytes to each response. No plugin broke. The only friction was the CSP Report-Only phase, which took about three days of checking the console before we were confident enough to enforce.

The Lighthouse jump from 68 to 92 came entirely from the CSP and X-Content-Type-Options flags resolving. Performance scores did not change because headers have no effect on load time. This is a security and best-practices win, not a speed win.

If you want us to audit your cPanel site's current header posture and set this up, or if you are unsure about your PayFast CSP config or a WordPress plugin that is throwing violations, get in touch with the nel404labs team. We handle this as part of ongoing site maintenance, so the context from your existing setup is already there.

Need something custom built? We work with founders and agencies who know what they want and need someone who can actually deliver it.

Let's chat