Skip to content
PeekWellThe Casebook
Join now

Security headers: what they do and how to add them to your site

Cheeky, a mint-green founder in a yellow polo shirt and big glasses, stands on a stepladder fitting a bolt to a wooden front door that already has a peephole and a brass letterbox. A padlock, a hinge and a small shutter tumble through the air beside him, and a toolbox and a coffee cup wait on the pavement.

Security headers are short lines your server sends with every page, telling the visitor’s browser what it may do: only use HTTPS, which scripts may run, whether other sites may put your page in a frame. A handful of them cover most of what a small site needs. On most hosts you add them in a file or a dashboard in a few minutes. The one that can break things is the content security policy, so it goes in last, and in report-only mode first.

Below: each header with a safe starting value, where to put them on the common hosts, the order to add them in, a prompt for your assistant, and how to check what your site sends today. If a grader just gave you an F and you want it fixed tonight, skip to the build order and come back for the why.

What security headers are, and why a small site needs them

Every time a browser asks for a page, the server answers with the page and a list of headers: the content type, caching rules, cookies. HTTP security headers are the part of that list that gives the browser instructions. The browser can’t tell which scripts on your page are yours and which got there by accident. The headers tell it.

A small shop or marketing site still has forms, maybe a login, and a stack of third-party scripts. That’s enough. Article 32 of the GDPR, and the UK GDPR, which carries the same article, asks a business to put in place a level of security appropriate to the risk, taking into account the state of the art and the cost. The ICO’s guide to data security lists the security of your website and any other online service or application that you use under online security. It binds your processors too, the hosts and tools that handle data for you, which is where a data processing agreement comes in. Headers are cheap and well documented, about as close to “state of the art at low cost” as anything gets.

In the US the angle is different: the FTC acts on security claims a company can’t back up. In May 2025 it finalised an order with GoDaddy. The FTC alleged the hosting company advertised “award-winning security” while it failed to use multi-factor authentication, monitor for security threats, and secure connections to its consumer data. Those are allegations, settled by an order that bars GoDaddy from misrepresenting its security and requires a security programme with independent assessments. If your site promises “bank-level security”, the promise needs something behind it.

The security headers to add, one by one

Six of them. For each: what it does, a starting value from MDN or OWASP, and what it can break.

Strict-Transport-Security

What is HSTS? It’s this header. It informs browsers that the host should only be accessed using HTTPS, and the browser then upgrades any later plain-HTTP visit by itself. max-age is how long, in seconds, the browser remembers. includeSubDomains extends the rule to every subdomain. Browsers ignore the header if it arrives over plain HTTP.

OWASP’s recommended value is max-age=63072000; includeSubDomains; preload, which is two years. Don’t start there. Start with max-age=300, five minutes, which is the first step hstspreload.org suggests, then a week, then a month. Get it wrong and any subdomain still on plain HTTP becomes unreachable for anyone who has seen the header, until it expires.

preload asks browsers to build your domain into their HTTPS-only list. The list asks for a valid certificate, a redirect from HTTP to HTTPS on the same host, every subdomain on HTTPS, and a header with at least a year of max-age plus includeSubDomains and preload. And the warning: inclusion in the preload list cannot easily be undone. Removal takes months to reach users through Chrome updates.

Content-Security-Policy

The CSP header lets you control which resources the browser may load for a page. MDN’s guide says it’s mainly used as a defense against cross-site scripting (XSS) attacks. The directives you’ll touch first: default-src, the fallback for everything; script-src, where JavaScript may come from; frame-src, which sites may appear in your iframes; and frame-ancestors, which sites may put your page in theirs.

A starting policy for a simple site looks like default-src 'self'; frame-ancestors 'self', plus the domains your analytics, chat widget, payment page and embeds load from. That list is the hard part. Every tag manager, embed and font service needs its domain named, and a missing one breaks quietly. The YouTube nocookie guide covers what a video embed loads; those domains go in frame-src.

So it goes in as Content-Security-Policy-Report-Only first. In that mode, MDN says, The policy is not enforced, but any violations are sent to the reporting endpoint specified in the policy. You don’t need an endpoint to start. When I loaded a test page with a report-only policy in Chromium, the console logged each resource the policy would have blocked and loaded it anyway. OWASP is frank about the effort: Content Security Policy is complex to configure and maintain. MDN recommends nonces or hashes in the long run, what it calls a strict CSP. A domain list in report-only mode is a fine start.

X-Frame-Options

This header says whether a browser may show your page inside a frame, and MDN says sites use it to avoid clickjacking attacks: a page of yours, invisible, under someone else’s buttons. DENY blocks all framing. SAMEORIGIN allows only your own pages. ALLOW-FROM is obsolete, and a modern browser that sees it ignores the whole header. Setting it in a meta tag has no effect.

The newer way is CSP’s frame-ancestors. The W3C specification says the frame-ancestors directive overrides the X-Frame-Options header when the policy is enforced. 'none' matches DENY and 'self' matches SAMEORIGIN. OWASP’s advice is to use frame-ancestors if possible; older browsers only know X-Frame-Options, so send both, with matching values.

If your page is meant to sit in a frame somewhere, say a booking widget on a partner’s site, that frame now shows the browser’s error page. It works the other way round too. If an embed on your site shows an error page instead of the video or map, the other site’s headers may be saying no, or your own frame-src may be.

X-Content-Type-Options

One value: nosniff. It tells the browser that the MIME types in the Content-Type header should be respected and not guessed. A file served as text can’t then run as a script. It only breaks a stylesheet or script sent with the wrong content type. The fix, OWASP agrees, is the content type.

Referrer-Policy

When someone clicks a link or your page loads a third-party file, the browser sends the address it came from. Referrer-Policy controls how much of that address goes along. Browsers default to strict-origin-when-cross-origin: the full address within your site, only the domain to other sites. OWASP says to Set the header explicitly rather than relying on browser defaults. That default is a sensible value to set. no-referrer sends nothing at all.

MDN’s example of the risk is a reset-password page with a social media link in the footer, which can hand the reset URL to the social network. The same happens to a thank-you page whose URL carries the visitor’s email address in the query string. MDN’s fixes: the header, POST rather than GET for forms, and no third-party content on login, payment and reset pages.

Permissions-Policy

This header allows and denies browser features for your page and the iframes in it. An empty list, (), turns a feature off everywhere. A brochure site that never asks for the camera, microphone or location can send camera=(), microphone=(), geolocation=(), and then no embedded script can ask for them either. OWASP’s advice is to disable all the features your site does not need. MDN marks it limited availability, since some major browsers ignore it. Send it anyway. A store finder that uses location, or a video-call widget, will stop working; allow those with self or the widget’s domain.

Where to add them on your host

Host Where the headers go Documentation
Cloudflare Rules, Overview, Create rule, Response Header Transform Rule, “Set static” for each header, then Deploy. Up to 30 headers per rule Cloudflare docs
Netlify A _headers file in the publish directory, or a [[headers]] table in netlify.toml; /* for every page Netlify docs
Vercel A headers array in vercel.json, with source: "/(.*)" and key and value pairs Vercel docs
Apache Header always set lines in the site config or .htaccess mod_headers
nginx add_header ... always; in the server block ngx_http_headers_module
WordPress on managed hosting The host’s dashboard if it has a headers setting, .htaccess if the host runs Apache, or a rule at your CDN Your host’s help pages
Squarespace An “HSTS secure” option in the SSL panel Squarespace help
Shopify, Wix HTTPS is automatic; the help pages don’t describe adding your own headers Shopify, Wix

For Netlify, a _headers file with steps 1 to 4 of the build order below looks like this:

/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  X-Frame-Options: SAMEORIGIN
  Content-Security-Policy: frame-ancestors 'self'
  Permissions-Policy: camera=(), microphone=(), geolocation=()

On Apache, the always matters. Without it, Apache leaves the headers off error pages and redirects:

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 Content-Security-Policy "frame-ancestors 'self'"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"

nginx has the same trap and one more. Without always, add_header only applies to a list of normal response codes. And a block inherits headers from the level above if and only if there are no add_header directives defined on the current level, so one add_header inside a location block quietly drops all the others there.

add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

For WordPress, plugins that set headers exist. Whatever sets them, check the result in the browser, on a cached page as well as a fresh one. On Shopify and Wix, ask the platform’s support before putting another service in front of the store to add headers.

A safe build order, and a prompt for your assistant

Check each step in the browser before the next one.

  1. X-Content-Type-Options: nosniff.
  2. Referrer-Policy: strict-origin-when-cross-origin.
  3. X-Frame-Options: SAMEORIGIN with Content-Security-Policy: frame-ancestors 'self'.
  4. Permissions-Policy, turning off what you don’t use.
  5. Content-Security-Policy-Report-Only with your full domain list. Browse every page type for a week: home, product, form, checkout, login. Read the console and add what’s missing.
  6. Swap Report-Only for the enforced Content-Security-Policy, and fold frame-ancestors into it, since an enforced policy can carry it.
  7. Strict-Transport-Security: max-age=300. Check every page and subdomain still loads.
  8. Raise max-age to a week, a month, then a year. Add includeSubDomains once every subdomain is on HTTPS.
  9. Preload only if every subdomain will be on HTTPS for good.

To get the config written for your host, copy the headers from the browser (the cookie checking guide shows where the Network tab is) and paste them into this:

Here are the response headers my homepage sends today:

[paste the response headers from the browser's Network tab]

And here are the third-party domains my pages load, from the same tab:

[paste the list of domains]

My site runs on [host: Cloudflare, Netlify, Vercel, Apache, nginx, WordPress on ...].

Write the configuration for this host to add:
- Strict-Transport-Security with max-age=300 to start, no preload
- X-Content-Type-Options: nosniff
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy turning off camera, microphone and geolocation
- frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN
- a Content-Security-Policy-Report-Only header that allows only my own domain and the domains I listed

Explain each line in one sentence. Tell me what each one could break, and how to undo it.
Flag any header I already send that conflicts with these, or that I should remove.

Read the answer before you paste it anywhere. The domain list in the CSP is where assistants guess, and a guess there either breaks a widget or lets everything through.

How to check your security headers

In Chrome or Firefox on a laptop, open developer tools (F12, or right-click and Inspect), go to the Network tab and reload. Click the first row, which is the page itself, and open Headers. The response headers section lists everything your server sent. On a phone, the browsers don’t show headers. What a phone can show is HTTPS: type your address with http:// and see whether it lands on https:// with the padlock.

For a grade, Mozilla’s HTTP Observatory performs an in-depth assessment of a site’s HTTP headers and other key security configurations, and it’s a web page, so a phone browser can run it too. Other graders exist, securityheaders.com among them. Read a grade for what it is. Mozilla’s own FAQ says: We’d love to say that any site that gets an A+ Observatory grade is perfectly secure. It then lists what it doesn’t test: outdated software, plugins with known holes, how passwords are stored. A grade reads what the server announces, nothing more.

A security.txt file

A security.txt file tells someone who found a problem on your site how to reach you. RFC 9116, published in April 2022, defines it. It goes at /.well-known/security.txt. Two fields are required: Contact, an email address or URL, and Expires, a date the RFC recommends keeping less than a year ahead.

Contact: mailto:security@example.com
Expires: 2027-10-01T00:00:00.000Z

Without one, a report lands at whatever address is on your contact page, if the finder bothers. Put the Expires date in your calendar.

Common mistakes on real sites

  • Headers on the homepage only. The marketing site sits on one host, the shop on another, the login on a vendor’s subdomain. Each sends its own set, and the one with the form is often the bare one.
  • Preload too early. A forgotten subdomain on plain HTTP goes dark, and getting off the list takes months.
  • A CSP that does nothing. With 'unsafe-inline' and * in script-src it’s decoration. MDN: Developers should avoid ‘unsafe-inline’, because it defeats much of the purpose of having a CSP.
  • Still sending X-XSS-Protection. MDN marks the x-xss-protection header non-standard and says to avoid it in new projects, warning it can create XSS vulnerabilities in otherwise safe websites. OWASP: Do not set this header or explicitly turn it off. Take it out and let CSP do that job. OWASP says to drop X-Powered-By too.
  • The same header twice. The host sets one value, the CDN another, and you’re guessing which one the browser obeys. Set each header in one place.

Check your own site’s headers

  1. On your phone, type your address with http:// in front: the bare domain, www, and every subdomain you use, such as the shop or the login. Each should end on https:// with the padlock.
  2. On a laptop, open your homepage, open the Network tab, reload and read the response headers of the first row. Write down which of the six are there.
  3. Repeat on a contact form page, and on your checkout or login. Compare the lists.
  4. Look for X-XSS-Protection, X-Powered-By, and any header listed twice.
  5. Read the Strict-Transport-Security line. Note the max-age, and whether includeSubDomains and preload are there.
  6. Submit a form and look at the address bar on the thank-you page. If your email address is in it, check the Referrer-Policy and the form’s method.
  7. Open /.well-known/security.txt on your domain. Check the Expires date.

The rules behind this are the security checks in the Handbook: EU-15 and UK-15 for Article 32, and US-15 for the US. For each, Peeky reads what your server tells every visitor’s browser on your public pages: whether the connection is encrypted, whether the site’s TLS identity is in date, and whether the usual browser protections are switched on. For US-15 he also notes whether well-known sensitive folders and files answer from outside.

Questions people ask

What are the most important security headers?

For an ordinary site: Strict-Transport-Security, Content-Security-Policy (or at least its frame-ancestors directive), X-Content-Type-Options, Referrer-Policy and Permissions-Policy. X-Frame-Options still helps older browsers. The OWASP HTTP Headers Cheat Sheet lists a recommended value for each, and tells you to stop sending X-XSS-Protection and X-Powered-By.

How can I check the security headers for my website?

Open your page in Chrome or Firefox on a laptop, open developer tools, go to the Network tab and reload. Click the first row, which is the page itself, and read the response headers. For a graded report, Mozilla's HTTP Observatory assesses a site's headers and other security settings from its address.

How do I find missing security headers?

Compare what your page sends with a short list: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, Permissions-Policy and X-Frame-Options. Whatever's absent from the response headers is missing. Check more than the homepage. Login, checkout and form pages often sit on another server or subdomain and send a different set, and nginx drops inherited headers in some blocks.

Is X-Frame-Options still needed if I use CSP frame-ancestors?

Not in a current browser. The W3C CSP specification says an enforced frame-ancestors directive overrides X-Frame-Options, which is then ignored. MDN notes that X-Frame-Options is also supported in older browsers, so sending both, with matching values, covers old browsers and new ones alike.

Do security headers help SEO?

Google has said HTTPS is a ranking signal. In 2014 it called it very lightweight, affecting fewer than 1% of global queries. Google's page experience documentation mentions HTTPS and doesn't mention security headers. So the honest answer: HTTPS counts a little, and there's no Google statement that the other headers count at all.

Filed with