Short answer: Add security headers where responses are generated or passed through: in the Apache virtual host or .htaccess with Header always set, in the Nginx server block with add_header … always, or as response header rules in your CDN. A solid baseline for most HTTPS sites is Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options with CSP frame-ancestors, Referrer-Policy and Permissions-Policy, with a full Content-Security-Policy added later after testing. Set each header in one place only, then verify with curl -sI on several page types.
Most explanations of security headers stop at “add this header”. In practice, the hard part is knowing where to add it on your particular setup, and making sure it appears on every page without duplicates. This guide collects copy-ready configurations for the most common environments – Apache and LiteSpeed, Nginx, WordPress hosting and CDNs – with the pitfalls that cause headers to go missing. If you want to understand what each header does first, see the related reading at the end.
The baseline set
These values are safe for most sites that already run fully on HTTPS:
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'; upgrade-insecure-requests
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()Adjust before deploying: start HSTS with a short max-age if you are unsure every page works over HTTPS; allow payment or geolocation for self if your checkout or store locator uses them; allow partner domains in frame-ancestors if they embed your pages. A complete Content-Security-Policy that restricts scripts needs its own report-only testing phase and is best added as a separate project.
Before changing anything, record what your site sends today with curl -sI https://example.com. Some hosts, CDNs and CMS platforms already add a few of these headers, and knowing the starting point makes it easy to spot duplicates and to confirm that your change actually made a difference. Save the output in your site notes together with the date.
Apache and LiteSpeed
Make sure mod_headers is enabled (on Debian and Ubuntu: a2enmod headers, then reload). Add the headers to the HTTPS virtual host, or to .htaccess in the web root on shared hosting:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'; upgrade-insecure-requests"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
</IfModule>Notes:
alwaysmakes Apache add the header to error responses and redirects too, not only to successful pages.setreplaces an existing header with the same name, which avoids duplicates if the application also sends one. Usesetifemptyif you want the application’s value to win.- LiteSpeed servers read the same
.htaccessdirectives. - If a subfolder has its own
.htaccesswith header rules, check that it does not remove or override the root rules.
Nginx
Add the headers to each HTTPS server block, or to an include file used by all of them:
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'; upgrade-insecure-requests" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;The most important Nginx pitfall: add_header directives are inherited from the server block into a location block only if that location has no add_header of its own. A location that adds, for example, a Cache-Control header for images or a CORS header for fonts silently drops all your security headers for those URLs. Solutions: repeat the security headers in such locations, or put them in a snippet file and include it everywhere. Run nginx -t before reloading.
WordPress and other CMS hosting
WordPress itself sends only a few headers (for example X-Frame-Options on the login and admin pages). You have three options:
Server configuration or .htaccess (preferred)
Use the Apache or Nginx examples above. This covers every response, including images, feeds and plugin endpoints, and does not depend on PHP running.
A security or header plugin
On hosting where you cannot edit server files, a plugin can add headers to pages generated by WordPress. It will not cover static files served directly by the server, and it stops working if the plugin is disabled. Choose one plugin for this job; two plugins setting the same header create conflicts.
Hosting panel settings
Some managed WordPress hosts offer header settings in their dashboard or apply sensible defaults. Check what is already sent before adding your own, to avoid duplicates.
IIS and Windows hosting
On Microsoft IIS, custom response headers are set in the site’s web.config file or through IIS Manager under “HTTP Response Headers”. A web.config section looks like this:
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
</customHeaders>
</httpProtocol>
</system.webServer>While you are there, remove the X-Powered-By header with a <remove name="X-Powered-By" /> line, since it reveals the framework in use.
CDNs and reverse proxies
If a CDN sits in front of your site, you can add headers there with response header rules or edge functions. Advantages: headers apply to cached and uncached responses alike, and you manage them in one place even across several origin servers. Things to check:
- Some CDNs have dedicated HSTS settings; use them rather than a generic rule, and do not also set HSTS on the origin with a different value.
- Decide whether the CDN rule should overwrite or keep headers sent by the origin.
- Remember that direct access to the origin (bypassing the CDN) will not have the headers; restrict origin access to the CDN where possible.
Verify on every page type
After deploying, check headers on a representative set of URLs, not just the home page:
for u in / /about/ /contact/ /wp-login.php /wp-content/uploads/logo.png /nonexistent-page/; do
echo "== $u"; curl -sI "https://example.com$u" | grep -i -E "strict-transport|x-content-type|x-frame|content-security|referrer-policy|permissions-policy"
doneLook for three problems: headers missing on some URLs (location blocks, subfolder overrides, static files), headers appearing twice with different values (server plus plugin or CDN), and typing mistakes in names or values. Browsers ignore malformed headers silently, so a typo means no protection. Then open the site in a browser and check the console for Content-Security-Policy or Permissions-Policy violations that indicate something legitimate was blocked. The OWASP Secure Headers Project maintains current recommended values if you want to compare.
Troubleshooting common problems
- 500 error after editing .htaccess:
mod_headersis not enabled or the host does not allow header directives. Wrapping rules in<IfModule mod_headers.c>prevents the error but also means nothing is added – ask the host. - Header present on pages but not on redirects or 404 pages: the
alwayskeyword is missing in Apache or Nginx. - Site layout or scripts broken after adding CSP: the policy is too strict; switch to
Content-Security-Policy-Report-Onlywhile you complete the allow-list. - Embedded page stopped working on a partner site: X-Frame-Options or frame-ancestors now blocks framing; allow the partner’s origin in frame-ancestors for the pages they embed.
- Changes not visible: the CDN or a page cache still serves old responses. Purge the cache and test again with curl.
Keep headers from disappearing
Headers are easy to lose. A server rebuild resets configuration to defaults, a hosting migration leaves the old .htaccess behind, a CDN change drops custom rules, a developer adds a new Nginx location block. A few habits prevent this:
- Keep server configuration in version control, including header snippets.
- Document where each header is set – server, CDN or plugin – in your site notes.
- Add the verification loop above to your post-deployment and post-migration checklist.
- Use external monitoring that checks headers regularly, so a regression is noticed within days rather than at the next annual review.
How Site AI Audit helps
Site AI Audit checks which security headers your site sends as part of every report, alongside the SSL certificate, HTTPS redirect and exposed software versions. Each missing or weak header comes with a plain-language explanation and the line to add, ranked by impact. After deploying your headers, a re-check confirms they are in place, and monitoring on paid plans alerts you if something breaks later. Run a free check.
Related reading
- HTTP Security Headers Explained: What Each One Does
- HSTS Explained: How to Enable Strict-Transport-Security Safely
- Content Security Policy for Beginners: A Practical Setup Guide
- Permissions-Policy Header: Control Camera, Location and More
The bottom line
Adding security headers is mostly about putting a handful of lines in the right place: Header always set on Apache, add_header … always on Nginx, a single plugin or server rule on WordPress, or response header rules at your CDN. Set each header once, watch out for Nginx location blocks and duplicate sources, verify on several page types, and make the check part of every deployment so the headers stay in place.
FAQ
Where is the best place to add security headers?
As close to the edge as practical and in one place only: the web server configuration or the CDN. Plugins work too, but they only cover pages generated by the application and stop working if disabled.
Why are my Nginx headers missing on some pages?
Usually because a location block has its own add_header directive, which stops inheritance from the server block. Repeat the security headers in that location or include them from a shared snippet.
Can I add security headers on shared hosting?
Usually yes, through .htaccess on Apache or LiteSpeed hosting, or through the hosting panel. If .htaccess changes cause errors, ask the host whether mod_headers is enabled.
Is it a problem if a header appears twice?
It can be. Browsers may combine or reject duplicate values, and two different Content-Security-Policy headers are both enforced. Keep one source for each header.
Do security headers need to be on images and other files?
The most important protection applies to HTML pages, but headers such as X-Content-Type-Options and HSTS are also useful on other responses. Server-level configuration covers all of them automatically.



