If your free scan flagged your website as "missing security headers", it found that some of the standard HTTP headers modern browsers use to defend against common attacks aren't being sent. There are six headers that matter for a typical business website. Adding all six is usually one config-file change, costs nothing, and removes most of the easy attack patterns against your visitors.
This article covers what each header does in plain English, the exact lines to paste into nginx, Apache, or a WordPress site, and the common mistakes that break a site after rolling them out.
What HTTP headers actually are (briefly)
When a browser asks your server for a page, your server sends back two parts: the HTML the visitor sees, and a set of headers the visitor doesn't see. Headers are instructions to the browser, like "the page expires in five minutes" or "cache this for an hour" or "you may only load images from these specific domains". Security headers are the subset that tell the browser how to protect the visitor against attacks targeted at your site.
You don't need to write headers in code. They're set once in your web server config (or via a plugin if you're on WordPress) and apply to every page automatically.
The six headers that matter
1. Strict-Transport-Security (HSTS)
What it stops: an attacker on the same network as your visitor downgrading their connection from HTTPS to HTTP, then reading or modifying the traffic.
What to send:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Translated: "for the next year, the browser must use HTTPS for any URL on this domain and all subdomains, even if the user types http:// or clicks an http:// link."
Two warnings:
- Don't add
preloadto the directive until you're certain every subdomain serves HTTPS forever. Once you submit your domain to the HSTS preload list, removing it takes months. - Make sure HTTPS actually works for your whole site (every subdomain, every path) before you turn HSTS on with a long max-age. Otherwise legitimate users who hit a broken HTTPS path are locked out.
2. Content-Security-Policy (CSP)
What it stops: cross-site scripting attacks where someone injects malicious JavaScript into your page (often through a comment form or a vulnerable plugin) and uses it to steal session cookies or rewrite the page.
The full feature set is large, but a sensible starter policy for most business websites:
Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'none'
What it says, in plain English:
- Load scripts, stylesheets, and other resources only from your own domain by default.
- Allow images from your own domain, plus inline data URIs and any HTTPS source.
- Allow inline styles (most WordPress themes need this).
- Allow scripts from your own domain plus Google Tag Manager and Google Analytics.
- Allow fonts from Google Fonts.
- Allow XHR/fetch connections only to your own domain plus GA.
- Block your site from being loaded inside an iframe by anyone (anti-clickjacking).
CSP is the header most likely to break your site if you copy a strict policy without checking. Test in browser dev tools first: open the Console tab on a representative page after applying the header. Any blocked resource will log a clear error. Add the source to the appropriate *-src directive. Repeat until clean.
For a WordPress site with marketing tools, plugins, and embedded videos, the policy can get long. That's normal.
3. X-Frame-Options
What it stops: clickjacking attacks where someone embeds your login or checkout page in an invisible iframe on their site, tricks a visitor into clicking what they think is something on the attacker's page, but is actually a button on yours.
What to send:
X-Frame-Options: SAMEORIGIN
Or more strictly:
X-Frame-Options: DENY
SAMEORIGIN means your own domain can iframe your pages (useful for some preview features), but no other site can. DENY blocks all framing.
If you set CSP's frame-ancestors 'none' (in section 2 above), modern browsers ignore X-Frame-Options. But older browsers don't support frame-ancestors, so set both for maximum coverage.
4. X-Content-Type-Options
What it stops: browsers guessing the wrong content type for a file you serve and executing it as code when it should be treated as data. The classic example: a .txt file an attacker uploaded that the browser sniffs and decides is HTML, then runs.
What to send:
X-Content-Type-Options: nosniff
That's the entire header. There's no configuration. Set nosniff once and forget it. There is no reason not to send this on every public website.
5. Referrer-Policy
What it stops: leaking the URL of your site (and any sensitive query parameters) to third-party sites your users navigate to. Default browser behaviour is to send the full URL of the page they came from in the Referer header. If your URL contains a session token or a customer ID, that token leaks to every site you link out to.
A balanced policy that works for most business sites:
Referrer-Policy: strict-origin-when-cross-origin
In English: "send the full URL when staying on this site, send only the origin (domain) when linking to other HTTPS sites, send nothing at all when going from HTTPS to HTTP."
6. Permissions-Policy
What it stops: third-party scripts on your page (analytics, marketing widgets, embedded videos) silently using browser features like the camera, microphone, geolocation, or accelerometer without you knowing.
A reasonable default for a business website that doesn't need any of these features:
Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
This says: nobody on this page may access the camera, microphone, location, or use Google's FLoC tracking. Adjust per feature you actually use. If you have a "find me on the map" button, change geolocation=() to geolocation=(self) so only first-party scripts can use it.
Where to add them
nginx
Edit your server block (usually in /etc/nginx/sites-available/yoursite.com) and add these lines inside the server { ... } block:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; ..." always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
Then run nginx -t && systemctl reload nginx. Always include always so headers also apply to error pages.
Apache
Edit your .htaccess (in your site's web root) or your virtual host config:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; ..."
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()"
</IfModule>
mod_headers must be enabled (it usually is by default).
WordPress (without server access)
If your hosting plan doesn't give you nginx or Apache config access, do it via plugin:
- HTTP Headers (free, well-maintained): UI for setting all six headers per role or page.
- Really Simple SSL Pro: bundled headers controls (paid).
- Wordfence Security: also sets some of these via its WAF.
Whichever you pick, set the same six headers and use the values above. WordPress plugins occasionally have idiosyncrasies; verify with the testing step below.
Cloudflare Transform Rules
If your domain is behind Cloudflare, you can set headers at the edge without touching your origin:
- Cloudflare dashboard, your domain, Rules, Transform Rules, Modify Response Header.
- Create one rule per header. The "If" field can be
Hostname equals yourdomain.com.au. The "Then" field is the static header value. - Save. Headers apply within seconds, no origin restart needed.
This is the easiest path for non-technical site owners on Cloudflare.
Common mistakes
Setting unsafe-inline everywhere in CSP "to make it work". Defeats the purpose. If a page genuinely needs inline styles, allow only style-src 'unsafe-inline', not script-src 'unsafe-inline'. Inline scripts are the highest-risk thing CSP defends against.
Forgetting subdomains. HSTS with includeSubDomains only works if those subdomains also serve HTTPS. If blog.yourdomain.com.au is HTTP-only, the directive locks visitors out of your blog.
Long HSTS max-age before HTTPS is solid. Set max-age=300 for a week first to verify nothing breaks. Then bump to a year (31536000).
Permissions-Policy with browser typos. The header is finicky about syntax. geolocation=() is correct; geolocation=none is wrong (silently ignored). Test before you trust it.
Adding headers in two places. If you set headers at Cloudflare AND at nginx, the duplicate values appear in the response and some browsers behave unpredictably. Pick one layer and stick with it.
How to test
Three free testers we use ourselves:
- securityheaders.com: paste your domain in. Get a letter grade A through F based on which headers are present and how strict.
- csp-evaluator.withgoogle.com: paste your CSP value in. Tells you exactly which directives are too permissive and which sources to lock down.
- Browser DevTools, Network tab, click any request, Headers panel. The "Response Headers" section shows every header your server actually sent for that page. The fastest sanity check.
After applying the headers, re-run our free scan. The Website Security card should flip from showing missing headers to a clean "all six present" result.
Run the free scan
Run our free scanner on your domain to see which security headers are missing right now. The technical report includes the exact Header always set and add_header lines to paste into your server config.