Problem: Jetpack Boost Cache was causing missing security headers. Long story short my site kept “losing” security headers whenever Jetpack Boost served a cached page, and .htaccess rules wouldn’t stick. The fix that I found was to set headers at the earliest possible time which was at the PHP layer using auto_prepend_file (via .user.ini), plus a tiny MU plugin to cover /wp-admin. As adding the headers like that in wp_admin bricked all of the admin pages. Now I get A+ on securityheaders.com with or without cache hits. More security analysis and updates coming soon
The problem
- Security scanners said my site was missing headers like:
Strict-Transport-SecurityContent-Security-Policy: upgrade-insecure-requestsX-Content-Type-Options: nosniffReferrer-PolicyPermissions-PolicyX-Frame-Options
- Curls looked inconsistent:
http://…(301) showed headers.https://…(200) sometimes showed no headers.- When
X-Jetpack-Boost-Cache: hitappeared, headers often disappeared.
Why? Well (after much diagnostics) apparently Jetpack Boost serves cached HTML via the advanced-cache drop-in before WordPress hooks (like send_headers) and outside the directory where your .htaccess lives. That means:
- Per-directory
.htaccessrules did not run for cached hits. - WP hooks may not fire on cached hits.
- On some hosts, the HTTPS vhost doesn’t honor
.htaccessheader directives (or blocks advancedexprsyntax), leading to 500s when I tried clever conditionals.
What didn’t work (or only partially worked)
.htaccessheader rules at the web root: worked for some paths (e.g., HTTP → 301), but not reliably for HTTPS 200 responses and not for Boost cached pages..htaccessin the Boost cache folders: didn’t help; Boost serves the HTML through PHP, not by mapping URLs into that directory.- Only using a WP plugin with
send_headers: worked for dynamic pages, but not for Boost cached hits.
Disabling boost cache was the easiest “fix” but I wanted to be able to use caching. Using a different caching provider is always an option, but I have been using Boost caching and I use it for other features. Managing settings on another set of plugins trying to get me to pay an exorbitant amount doesn’t sound fun.
How to fix missing security headers in WordPress with Jetpack Boost enabled
If you don’t care about the how and just want the fix. Click the button below and install it as you would a standard plugin for your WP site. Upload the ZIP in Plugins → Add New → Upload.
1) Add a global PHP “prepend” (covers everything, even cache hits)
Create wp-content/shm-prepend.php:
<?php
// Skip admin/login here (we’ll add admin headers via MU plugin).
$__uri = $_SERVER['REQUEST_URI'] ?? '';
$__script = $_SERVER['SCRIPT_NAME'] ?? '';
if (strpos($__uri, '/wp-admin/') !== false || strpos($__script, 'wp-login.php') !== false) {
return;
}
if (!function_exists('sh_set_header_once')) {
function sh_set_header_once($name, $value) {
if (function_exists('headers_list')) {
foreach (headers_list() as $h) {
if (stripos($h, $name . ':') === 0) return;
}
}
header($name . ': ' . $value);
}
}
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on')
|| (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
if ($is_https) {
sh_set_header_once('Strict-Transport-Security', 'max-age=2592000');
}
sh_set_header_once('Content-Security-Policy', 'upgrade-insecure-requests');
sh_set_header_once('X-Content-Type-Options', 'nosniff');
sh_set_header_once('Referrer-Policy', 'strict-origin-when-cross-origin');
sh_set_header_once('Permissions-Policy', 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), bluetooth=(), fullscreen=(self)');
sh_set_header_once('X-Frame-Options', 'SAMEORIGIN');
// Optional if a scanner nags:
// sh_set_header_once('X-XSS-Protection', '1; mode=block');
Create or edit .user.ini in your WordPress root (same folder as wp-config.php):
auto_prepend_file=wp-content/shm-prepend.php
Use the relative path; it avoids
open_basedirissues on shared hosts.
2) Exclude /wp-admin from prepend and add an MU fallback (fixes admin 500s)
Create wp-admin/.user.ini:
auto_prepend_file=
This disables the prepend for the entire admin area.
Now add a tiny MU plugin to send the same headers for admin and login:
Create wp-content/mu-plugins/security-headers-admin.php (create the mu-plugins folder if needed):
<?php
add_action('send_headers', function () {
$uri = $_SERVER['REQUEST_URI'] ?? '';
$scr = $_SERVER['SCRIPT_NAME'] ?? '';
$is_admin = (strpos($uri, '/wp-admin/') !== false) || (strpos($scr, 'wp-login.php') !== false);
if (!$is_admin) return;
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on')
|| (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
if ($is_https) header('Strict-Transport-Security: max-age=2592000');
header('Content-Security-Policy: upgrade-insecure-requests');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), bluetooth=(), fullscreen=(self)');
header('X-Frame-Options: SAMEORIGIN');
// header('X-XSS-Protection: 1; mode=block');
}, 10);
Now:
- Front-end + cached pages → headers via prepend (early, cache-proof)
- Admin/login → headers via MU plugin (late, but stable)
How I tested
# Home page (often cached)
curl -s -D - -o /dev/null https://your-site.tld/ | sed -n '1,25p'
# Expect: X-Jetpack-Boost-Cache: hit (if using Boost) and all headers
# Dynamic endpoint (bypasses page cache)
curl -I https://your-site.tld/wp-json/
# Expect: all headers
# Redirect path sanity check
curl -I -L http://your-site.tld/
# Expect: headers on the 301 and final 200
# Admin (should NOT 500 now)
curl -I -k https://your-site.tld/wp-admin/
When everything’s green, run external scanners:
- securityheaders.com – I’m getting A+ now.
- SSL Labs for TLS config.
- Mozilla Observatory for a broader web hygiene check. (I’ve got a bit of work to do here)

HSTS rollout (do this last)
Start with a short HSTS (e.g., 30 days) as above. After you’re 100% sure all your subdomains are HTTPS-only, upgrade to:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Only flip to preload when you’re committed—browsers will enforce HTTPS for your entire zone.
Common pitfalls & quick fixes
- 500 after adding
.user.ini
Path or permissions. Use a relative path to the prepend file (auto_prepend_file=wp-content/shm-prepend.php). Ensure file perms are0644. - Ensure
wp-content/shm-prepend.phpis 0644 and owned by the web user to prevent errors - Admin 500s but front-end fine
Addwp-admin/.user.ini(emptyauto_prepend_file=) and use the MU plugin shown above. - Headers still missing on cached hits
If prepend is correct and readable, cached hits will include headers. Re-check withcurl -s -D - -o /dev/nulland confirmX-Jetpack-Boost-Cache: hitappears alongside your headers.
Final checklist
wp-content/shm-prepend.phpexists (0644).user.iniin WP root points towp-content/shm-prepend.phpwp-admin/.user.inidisables prepend in adminwp-content/mu-plugins/security-headers-admin.phpsets headers for admin/login- Curls show headers on
/,/wp-json/, and/wp-admin/ - External scans (securityheaders.com, SSL Labs, Mozilla Observatory) are happy
Why this approach worked
Security headers must be added at response time. When a page cache (like Jetpack Boost) serves HTML before WordPress runs, your normal plugin hooks and some .htaccess rules won’t fire. By setting headers in a PHP file that runs before anything else (auto_prepend_file), you guarantee headers for every PHP response—cached or not—then add a minimal MU plugin for wp-admin stability. Simple, reliable, and easy to keep green.
Want my code as a plugin? I also packaged this into a small “Security Headers Manager” plugin with toggles for prepend, admin exclusion, and MU fallback. as well as a readout of your current headers. If you’d like that:
If there is any issue with this method that I am unaware of please yell at me in the comments down below, or email me all the info is on my About page.
If you enjoyed this please Share:
Leave a Reply