HTTPS, HSTS and Security Headers on Shared Hosting: Audit, Test and Roll Out a CSP via .htaccess
Security headers are a few lines of config that tell browsers how strict to be with your site: always use HTTPS, don’t guess file types, don’t let other sites frame you, and only run the scripts you approved. Getting them wrong can break your own pages, so this guide works the careful way. Audit first, build the policy from what your pages actually load, prove it in a local browser, and only then roll it out, in report-only mode first. Every command was run against this site, and the real output is shown.
Warnings
CriticalA strict Content-Security-Policy can blank out styles, fonts or scripts site-wide. Deploy it as Content-Security-Policy-Report-Only first. HSTS is sticky by design: browsers remember it for max-age seconds, and preload lists are much harder still to undo. Back up .htaccess before every edit.
What You Need
- HTTPS already working, with a valid certificate on both the bare domain and www.
- The ability to edit .htaccess. Apache or LiteSpeed, which most cPanel hosts run.
- A list of the third-party services your pages use, such as fonts, embeds or analytics. Step 3 builds it.
Audit → HTTPS → HSTS → inventory → CSP → test → roll out → re-audit
Step 1: Audit what the server sends today
Response headers aren’t in the page source, so ask the server directly:
#!/bin/sh # usage: sh headers_audit.sh example.com H="$1" echo "== redirects" for u in "http://$H/" "http://www.$H/" "https://www.$H/"; do curl -s -m 10 -o /dev/null -w "%{http_code} -> %{redirect_url} ($u)\n" "$u" || echo "no answer ($u)" done echo "== security headers on https://$H/" curl -sI -m 10 "https://$H/" | tr -d '\r' > /tmp/hdrs.txt for h in strict-transport-security content-security-policy content-security-policy-report-only \ x-content-type-options x-frame-options referrer-policy permissions-policy; do v=$(grep -i "^$h:" /tmp/hdrs.txt | cut -d' ' -f2-) printf '%-36s %s\n' "$h" "${v:-MISSING}" doneThis site, when this guide was written. It runs on LiteSpeed, and plain-HTTP results were taken directly against the origin:
== redirects 301 -> https://byteslip.org/ (http://byteslip.org/) 301 -> https://byteslip.org/ (http://www.byteslip.org/) 301 -> https://byteslip.org/ (https://www.byteslip.org/) == security headers on https://byteslip.org/ strict-transport-security max-age=31536000 content-security-policy MISSING content-security-policy-report-only MISSING x-content-type-options nosniff x-frame-options MISSING referrer-policy strict-origin-when-cross-origin permissions-policy MISSING
Redirects, HSTS,
nosniffandReferrer-Policywere already in place. The gaps were a CSP, frame protection and Permissions-Policy. Also check a 404 page and a redirect withcurl -sI. Headers that only appear on 200 responses are a common miss.Step 2: Force HTTPS and one hostname (only if the audit says so)
If
http://orwww.don’t 301 to your https apex, cPanel’s Domains page has a Force HTTPS Redirect toggle. Or use.htaccess:RewriteEngine On # 1) www -> https apex, in one hop RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L,NE] # 2) any remaining http -> https RewriteCond %{HTTPS} !=on RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,NE]The order matters. Handling
wwwfirst sendshttp://www.to the final URL in one hop, instead ofhttp://www→https://www→https://. Tested on Apache 2.4:example.comandwww.example.comover HTTP both returned301 → https://example.com/tutorials/a-b/?q=1. If your host terminates TLS at a proxy or CDN,%{HTTPS}may never be “on”. Use the host’s toggle instead, or you’ll get a redirect loop.Step 3: Get HSTS right, and think twice about preload
Strict-Transport-Securitytells browsers to use HTTPS for your host from now on. MDN says hosts must send it over HTTPS only, because browsers ignore it over plain HTTP. This site’s plain-HTTP 301 also carries the header. That’s harmless, since it’s ignored, but it’s a small tidy-up item.max-age=31536000(one year) is common. Start shorter, e.g.max-age=300, while testing.includeSubDomainscovers every subdomain, so add it only if every subdomain serves HTTPS, including ones you forgot about.preloadasks to be built into browsers. hstspreload.org requires a max-age of at least one year,includeSubDomainsandpreload, plus the HSTS header on HTTPS redirects too. It now says plainly that “HSTS preloading is not recommended” because browsers already auto-upgrade navigations. Its benefit is small, and removal is slow.
For a small site: keep one year, add
includeSubDomainsonce you’ve checked every subdomain, and skip preload.Step 4: Inventory what your pages load
A CSP is an allowlist, so first list what needs allowing. Run this on each page type (home, category, article):
# every <script> and its src, inline <style> blocks, and third-party hosts curl -s https://example.com/ > page.html grep -oE '<script[^>]*>' page.html | sort | uniq -c grep -c '<style' page.html grep -oE '(src|href)="https?://[^/"]+' page.html | sed 's/.*="//' | sort | uniq -c
On this site, that showed:
- Scripts are all same-origin (
/js/live-*.js), plus one inline script on the home page that logs a view to/api/views.php. - One inline
<style>block per page, and astyle=attribute on the home page. - Google Fonts: CSS from
fonts.googleapis.comand font files fromfonts.gstatic.com. - SVG backgrounds from
/css/. Everything else is ordinary outbound links, which CSP doesn’t restrict.
Then hash any inline scripts so you don’t need
'unsafe-inline'for scripts:python3 - <<'PY' import re, hashlib, base64 html = open('page.html').read() for m in re.finditer(r'<script>(.*?)</script>', html, re.S): print("'sha256-" + base64.b64encode(hashlib.sha256(m.group(1).encode()).digest()).decode() + "'") PYAny edit to that inline script, even whitespace, changes the hash. Moving it to a file under
/js/is the lower-maintenance option.- Scripts are all same-origin (
Step 5: Write the policy
Start from
default-src 'self'and open only what the inventory showed. Here’s the set for this site, in report-only form:<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" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()" Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'sha256-lX5OQqKOLEuX99W95cT5EIkhCdZg6DPg7binROxT90k='; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" </IfModule>
What each part does, per MDN:
script-srcallows same-origin files plus the one hashed inline script. No'unsafe-inline'and no'unsafe-eval', which is where most of a CSP’s XSS protection comes from.style-srcstill needs'unsafe-inline'because of the inline<style>blocks. Moving them to CSS files would remove that.frame-ancestorsreplaces the olderX-Frame-Options. Sending both covers old browsers. Neither can be set in a<meta>tag.object-src 'none'andbase-uri 'self'close two classic injection tricks.Permissions-Policywith()turns off features the site never uses.
Apache’s
Header always setalso applies the header to error pages and redirects. LiteSpeed reads the sameHeadersyntax from.htaccess. If your server already sets a header such as HSTS somewhere else, check the audit for duplicate lines after adding yours.Step 6: Prove it in a real browser, before it’s live
Serve a copy of your pages with the header, e.g. a local Apache using the
.htaccessabove withContent-Security-Policyenforced, and let headless Chrome report violations:google-chrome --headless=new --no-sandbox --enable-logging=stderr --v=0 \ --virtual-time-budget=5000 --dump-dom "https://example.com/" 2>&1 >/dev/null \ | grep -iE 'Content Security|violates' # no output = no violations on that page
The home page and a tutorial page both came back clean under the enforced policy. To make sure the test can actually catch a failure, break it on purpose: remove the hash and swap
font-srcto'self'. You should get:"Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-lX5OQqKOLEuX99W95cT5EIkhCdZg6DPg7binROxT90k='), ..." "Loading the font 'https://fonts.gstatic.com/s/inter/v20/....woff2' violates the following Content Security Policy directive: "font-src 'self'". The action has been blocked."Chrome even prints the hash it expected, which is a handy way to fill in a missing one.
Step 7: Roll out report-only, then enforce, then re-audit
Put the header block in the live
.htaccessasContent-Security-Policy-Report-Only. Browse every page type with DevTools open (Console tab). Report-only policies log violations without blocking anything. Once a few days pass with nothing logged, rename the header toContent-Security-Policy. Re-run the step 1 audit, check that nothing appears twice, and runcurl -sIon a 404 and a redirect. Mozilla’s HTTP Observatory (on MDN) gives a second opinion.
Tips
FAQ
Is X-XSS-Protection worth adding?
MDN marks it as non-standard and deprecated and points to a Content-Security-Policy instead, so this guide leaves it out.
Can I set these in a tag instead?
Partly. A CSP can be delivered via <meta http-equiv>, but frame-ancestors and report-only don’t work that way, and HSTS must be a real HTTP header. Server headers are the complete option.
Will a CSP break Google Fonts or YouTube embeds?
Only if you leave them out. Fonts need style-src for fonts.googleapis.com and font-src for fonts.gstatic.com. An embedded player would need frame-src for its host. Plain links to YouTube aren’t affected.
Why “always” in Header always set?
Apache’s mod_headers docs explain that “always” headers are added even to non-2xx responses such as redirects and error pages. Without it, those responses can go out without your headers.
Should I preload HSTS?
For most small sites, no. hstspreload.org itself now recommends against it, removal takes a long time, and it commits every subdomain to HTTPS.
Sources
- MDN: Strict-Transport-Security
- hstspreload.org: HSTS preload list submission and requirements
- RFC 6797: HTTP Strict Transport Security
- MDN: Content Security Policy guide
- MDN: Content-Security-Policy header
- MDN: Content-Security-Policy-Report-Only
- MDN: CSP frame-ancestors
- MDN: X-Frame-Options
- MDN: X-Content-Type-Options
- MDN: Permissions-Policy
- Apache HTTP Server: mod_headers
- LiteSpeed docs: Security response headers
- cPanel docs: Domains (Force HTTPS Redirect)
- OWASP: HTTP Headers Cheat Sheet