Skip to main contentSkip to footer

Security headers

Security headers are lines in the response of your web server that tell the browser what it must refuse: guessing file types, showing your pages inside foreign frames, passing full addresses to other sites, falling back to HTTP or loading scripts from unknown hosts. Hive writes them for you, generates the matching web server configuration and checks what really reaches a visitor. This page explains every header, the three ways it can take and what to do when the check reports a problem.

What is included in which plan

All header settings sit under Protection Settings → Firewall & Bots → Security Headers. The master switch Send security headers is off after installation; the quickstart recommendation switches it on. The three basic headers are part of the free plugin. The advanced set comes with the Pro add-on from the Professional plan on and works in Local Shield as well as in the Community Network.

HeaderPlanValue after installation
X-Content-Type-OptionsFreenosniff
X-Frame-OptionsFreeSAMEORIGIN
Referrer-PolicyFreestrict-origin-when-cross-origin
Strict-Transport-Security (HSTS)Professional and upOff; the recommendation for Professional switches it on, without preload
Permissions-PolicyProfessional and upgeolocation=(), camera=(), microphone=(), payment=(), usb=(), magnetometer=()
Content-Security-PolicyProfessional and upOff, empty policy
Cross-Origin-Opener-Policy, -Resource-Policy, -Embedder-PolicyProfessional and upOff

Every header is only sent while Send security headers is on. When a plan ends, Hive stops sending the advanced headers and keeps the basic three. Headers you have copied into the web server configuration are not affected by either: the web server keeps sending them until you remove them there.

Three ways to the visitor

The panel How the headers reach your visitors above the header rows shows the state of each route on your site.

  1. Web server configuration. nginx or Apache add the headers to every response: pages from a page cache, images, CSS and JavaScript files, the sign-in page and error pages. This is the most compatible route. Hive generates the configuration from your settings.
  2. Extended protection. The firewall that runs before WordPress (see Firewall and WAF) sends the headers as well. It reaches pages a caching plugin delivers through PHP, for example from advanced-cache.php. It works on front-end requests through index.php only, not in wp-admin, and sends HSTS only over HTTPS.
  3. WordPress alone. Hive adds the headers to every front-end page WordPress builds. Pages from a page cache, static files and everything the web server answers itself go out without them.
Your setupRoute to use
No page cacheWordPress alone is enough. Switch the headers on and run the header check.
Page cache plugin that serves pages through PHP (WP Rocket, WP Super Cache, W3 Total Cache and others in their usual setup)Extended protection, or the web server configuration
Cache served by the web server without PHP: Rocket-Nginx, static rewrite rules in .htaccess, a cache in the hosting panelOnly the web server configuration
You want the headers on images, files and the sign-in page tooOnly the web server configuration
Hosting without access to the server configurationApache with .htaccess: the Apache lines; otherwise Extended protection

Whatever you choose, set each header in one place only. A header the web server sets on its own is invisible to WordPress, so Hive cannot skip it, and it then arrives twice. The header check finds this.

Setting it up step by step

1

Switch the headers on

Open Protection Settings → Firewall & Bots → Security Headers, switch on Send security headers and leave the three basic headers at their recommended values. Click Save changes. All header rows, the switch included, sit behind Show technical details; Expert mode opens them by default.

2

Add the advanced headers (Pro add-on)

Switch on HTTP Strict Transport Security with a HSTS max-age (seconds) of 300 for the first days. Check the Permissions-Policy against the features your site uses. Put the Content-Security-Policy in Report only mode, as described below. Leave the Cross-Origin headers off for now. Save.

3

Run the header check

Open Server configuration and header check and click Check the headers of the home page. If every row says Arrives once, as set here, you are done with the delivery. Any other result is explained in The header check.

4

Close the gap on cached pages

If the check shows Missing on cached pages, switch on Extended Protection under Server & Maintenance → Server and check again. If the headers are still missing, your cache delivers pages without PHP; continue with step 5.

5

Move the headers to the web server (optional)

Copy the generated lines for nginx or Apache into the server configuration and reload the web server. Then switch off Send security headers in Hive and save. Run the check again: every header should now show Set by the web server or another plugin, which is the correct result for this route.

6

Raise HSTS and enforce the CSP

After a few days without problems raise the HSTS max-age to 31536000 (one year). Switch the Content-Security-Policy to Enforce once the browser console stays clean on every page type.

Every header in detail

HeaderProtects againstRecommendedCan break
X-Content-Type-OptionsMIME sniffing: an upload that claims to be an image is run as a scriptOn, nosniffPractically nothing
X-Frame-OptionsClickjacking: a foreign site shows your page in an invisible frame and tricks visitors into clickingSAMEORIGINDENY also blocks frames between your own pages; any value blocks embedding on other domains
Referrer-PolicyPaths and parameters of your addresses leaking to other sitesstrict-origin-when-cross-originno-referrer hides you from the referral statistics of partner sites
Strict-Transport-SecurityDowngrade to HTTP and SSL stripping on open networks300 while testing, then 31536000Any page or file that still loads over HTTP; with includeSubDomains every subdomain without a valid certificate
Permissions-PolicyBrowser features used by an injected scriptTurn off what you do not useStore finder (geolocation), Apple Pay and Google Pay (payment), video calls (camera, microphone)
Content-Security-PolicyCross-site scripting and injected scriptsReport only first, every external service listed, then EnforceTheme scripts, tracking, consent tool, embedded videos and maps
Cross-Origin-Opener-PolicyAttacks through windows your pages open or that open themsame-origin if you use no sign-in or payment popups from other domainsPayPal, Google and social sign-in popups
Cross-Origin-Resource-PolicyOther sites loading your images and scripts into their pagessame-origin only without a CDN host nameHotlinks and files your pages load from another host name of your own, such as a CDN
Cross-Origin-Embedder-PolicyNeeded only for special browser features (SharedArrayBuffer, precise timers)Off, unless a developer asks for itMost third-party embeds: videos, maps, web fonts

What the headers look like in a response:

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

HSTS. Once a browser has seen the header, it connects over HTTPS only for as long as max-age says, and you cannot take that back for visitors who already have it. Turn it on only when every page and every file loads over HTTPS. The stored default is 63072000 (two years), which is also the upper limit; start with 300 (five minutes). HSTS includeSubDomains extends the rule to every subdomain, mail and shop subdomains included. HSTS preload asks the browsers to ship your domain in their built-in list (hstspreload.org); it needs includeSubDomains and at least one year, and getting off the list takes months. Hive sends HSTS only over HTTPS. Behind a proxy that ends TLS, WordPress has to recognise the request as HTTPS, otherwise Hive leaves HSTS out.

Permissions-Policy. feature=() turns a feature off, feature=(self) allows it for your own pages only. The default turns off payment; a shop with Apple Pay or Google Pay buttons needs payment=(self) or has to remove the entry, otherwise the buttons do not appear.

X-Frame-Options and the CSP. Current browsers follow the frame-ancestors directive of an enforced Content-Security-Policy first. X-Frame-Options stays for older browsers; keep both on the same line, for example SAMEORIGIN and frame-ancestors 'self'.

Content-Security-Policy step by step

A CSP is a list of sources the browser may load scripts, styles, images, fonts, frames and connections from. Everything else is refused. It is the strongest header and the one that breaks the most, so it has two modes: Report only sends the policy as Content-Security-Policy-Report-Only, nothing is blocked and every violation appears in the browser console. Enforce sends Content-Security-Policy and blocks.

  1. Start with the WordPress starter policy. Set Content-Security-Policy mode to Report only and paste the policy below into Content-Security-Policy. It fits a WordPress site without external services. Most themes need 'unsafe-inline' for scripts and styles, so it is included.
  2. Open every page type. Home page, a post, a page, the contact form, the shop, the cart and the checkout, with the cookie banner accepted, because tracking and embeds often load only after consent. Open the browser console with F12; every source the policy does not allow is listed there with the directive it violates.
  3. Add each external source to its directive. A script host belongs in script-src, a host the page sends data to in connect-src, a video or map frame in frame-src, a font host in font-src. Separate directives with semicolons and hosts with spaces. Save, reload and repeat until the console stays quiet.
  4. Switch to Enforce. Set the mode to Enforce, open the page types once more and keep an eye on forms and checkout for a few days.

The WordPress starter policy from the plugin:

default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'

Example: Google Tag Manager with Google Analytics 4. The tag manager loads its script from www.googletagmanager.com, and Analytics sends its measurements to two Google host families. Add them to script-src and connect-src:

default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'

Each further tag in the container brings its own hosts, which the console names. img-src already allows images from every HTTPS host in the starter policy, so tracking pixels need no entry there. In report-only mode browsers ignore frame-ancestors; X-Frame-Options covers that until you enforce.

CSP report URI. Optional. An HTTPS address that receives every violation as a JSON report. Hive sends it as report-uri and, for current browsers, as report-to csp-endpoint together with a Reporting-Endpoints header:

Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri https://example.com/csp-report; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://example.com/csp-report"

Privacy: every report contains the IP address of the visitor and the address they visited. An external reporting service therefore processes personal data on your behalf and needs a data processing agreement, and your privacy policy has to mention it. For most sites the browser console during the report-only phase is enough; leave the field empty then.

The generated web server configuration

Under Server configuration and header check Hive shows the configuration for nginx and for Apache, built from the saved settings, each with a Copy button. Save first after a change. With Send security headers off there is nothing to generate: switch it on, save, copy, then switch it off again once the configuration is live. Keep a copy of the lines, because you need the same steps after every later change. With the basic three and HSTS the output looks like this:

# ReportedIP Hive: security headers for nginx.
# Paste into the server { } block that serves HTTPS, then run: nginx -t && systemctl reload nginx
# A location { } block with its own add_header inherits none of these lines.
# Repeat them there, or keep them in one file and include it in both places.
# Afterwards switch off "Send security headers" in Hive, otherwise every header is sent twice.
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000" always;
# ReportedIP Hive: security headers for Apache (.htaccess or virtual host).
# Afterwards switch off "Send security headers" in Hive, otherwise every header is sent twice.
<IfModule mod_headers.c>
	Header always set X-Content-Type-Options "nosniff"
	Header always set X-Frame-Options "SAMEORIGIN"
	Header always set Referrer-Policy "strict-origin-when-cross-origin"
	Header always set Strict-Transport-Security "max-age=31536000" "expr=%{HTTPS} == 'on'"
</IfModule>

What to watch out for:

  • always. Without it nginx adds headers only to successful responses. With it they also reach error pages and redirects.
  • Location blocks do not inherit. nginx passes add_header lines down only to blocks that have none of their own. A location block that sets its own header, typically Cache-Control for images or the cache location of Rocket-Nginx, loses all security headers. Keep the lines in one file and include it in the server block and in every such location block.
  • HTTPS server block. Paste the nginx lines into the block that serves port 443. A block that only redirects HTTP to HTTPS does not need them.
  • Test before reload. nginx -t checks the syntax; reload only when it reports success.
  • IfModule on Apache. If mod_headers is missing, Apache skips the block instead of answering every request with a 500 error. The check then shows the headers as missing, and the host has to enable the module.
  • HSTS on Apache. The expr=%{HTTPS} == 'on' condition limits HSTS to HTTPS, so the same lines work in a .htaccess that serves both protocols.
  • Switch off "Send security headers" afterwards. Otherwise WordPress adds every header a second time to the pages it builds.

The include file for nginx, as an example:

# /etc/nginx/snippets/security-headers.conf holds the generated add_header lines
server {
    listen 443 ssl;
    server_name example.com;
    include snippets/security-headers.conf;

    location ~* \.(css|js|jpg|jpeg|png|webp|svg|woff2)$ {
        add_header Cache-Control "public, max-age=2592000";
        include snippets/security-headers.conf;
    }
}

The header check

Check the headers of the home page loads the home page of your site three times from the server itself: twice like a visitor, so that the second request is answered by a page cache if there is one, and once with an extra query parameter that bypasses the cache. It then compares what arrives with your settings. Only your own address is requested; nothing is sent to reportedip.com. The check covers the home page; to look at other pages, files or the sign-in page use the network tab of the browser tools, curl -sI https://example.com/ or the free Security Headers tool.

ResultMeaningWhat to do
Arrives once, as set hereThe header arrives exactly once with your value, on the cached and on the uncached page.Nothing.
Missing on cached pagesWordPress sends the header, but the page cache delivers pages without it.Switch on Extended Protection. If it is already on, your cache works without PHP: use the web server configuration.
Missing on every pageHive plans the header, but it arrives neither from the cache nor past it. Something between WordPress and the visitor removes it, such as a proxy, a CDN or the hosting setup, or the home page does not come from WordPress at all.Set the header in the web server configuration, and check the CDN or proxy for rules that remove response headers.
Sent twice, by the web server and hereThe same header arrives more than once, usually because the web server or hosting sets it and Hive sends it too. Browsers then use one of the values or apply both, which is rarely what you want.Keep one source: remove the header from the server, or turn it off here. After moving everything to the server, switch off Send security headers.
Another value arrives, the web server or a plugin sets itOne value arrives, but not yours. The web server, the hosting, a CDN or another plugin sets the header and wins.Find the other source and decide where the header lives. Change the value there or remove it there.
Set by the web server or another pluginHive does not plan this header, because it is switched off here or not part of your plan, but it arrives.Expected after moving the headers to the web server. Check that the value is the one you want.

Two further messages can appear instead of the table:

  • "No security header arrives at the home page, neither from WordPress nor from the web server." Nothing is set anywhere. Switch on Send security headers and save, or check that your server configuration is active.
  • "The home page could not be loaded" with an error message. The server cannot reach its own address: a password protection on a staging site, a firewall of the hosting that blocks requests from the server to itself, or a domain that points somewhere else. Check the headers from outside with the tools above.

The register on System Status raises the suggestion Cached pages miss the security headers when the headers are on, a page cache is active and Extended Protection is not running. It disappears once Extended Protection runs. If you use the web server configuration and have switched off Send security headers, it is not raised.

Troubleshooting

A header arrives twice, but I set it only in Hive

Then the hosting sets it too. Common places are the security or SSL options of the hosting panel (an HSTS switch is frequent), a .htaccess written by another plugin or by the host, the nginx directives field of the panel, and CDN rules that add headers. Search for the header name in those places, keep one source and run the check again.

The check shows "Missing on cached pages" although Extended Protection is on

Your page cache delivers the files without PHP, for example Rocket-Nginx, static rewrite rules or a server cache of the hosting. Only the web server configuration reaches those pages. With Rocket-Nginx include the header file in its cache location blocks as well, because they set their own headers.

After the change, cached pages still show old values

A page cache stores the headers together with the page when it builds the cache file, depending on the plugin. Clear the page cache and the CDN cache after every header change, then check again.

Where do I put the lines in a hosting panel?

Apache lines go into the .htaccess in the WordPress directory, above the WordPress block. Panels with nginx usually offer a field for additional nginx directives per domain (in Plesk under Apache & nginx Settings, in ISPConfig on the Options tab); paste the add_header lines there. Some panels put that field inside a location block, so run the check afterwards. On shared hosting without either option, use Extended Protection.

Payment buttons, the map or the video disappeared

Look at the browser console. A CSP violation names the blocked host: add it to the matching directive. A missing Apple Pay or Google Pay button points at payment=() in the Permissions-Policy, a missing store finder location at geolocation=(). Broken embeds after switching on Cross-Origin-Embedder-Policy mean the header has to go off again.

A subdomain is no longer reachable

HSTS with includeSubDomains forces HTTPS on every subdomain. A subdomain without a valid certificate stays unreachable for every visitor who has seen the header, until the max-age runs out. Give the subdomain a certificate, and switch includeSubDomains off if you cannot.

The headers are missing in wp-admin and on the sign-in page

That is expected: WordPress and Extended Protection add the headers to front-end pages only. The web server configuration covers wp-admin and the sign-in page too.

Last updated: · Maintained by the ReportedIP team

Security Focused
GDPR Compliant
Made in Germany
Back to Docs