Security headers explained
Every time someone opens a page on your site, your server sends a few lines of instructions along with it. Visitors never see them, but their browser does. These response headers tell the browser things like "only ever load this site over a secure connection" or "do not let other sites show this page inside theirs".
They cost nothing, they take a developer minutes to add, and a missing one is one of the most common findings in any website check. This guide explains each one in plain language, says what to ask for, and adds a short note for whoever makes the change.
The headers that matter most
Strict-Transport-Security (HSTS)
What it does: tells the browser to use the secure https:// version of your site every time, even if someone types or clicks an http:// link. Without it, the very first request can go over an insecure connection, where someone on the same Wi-Fi could tamper with it.
What to ask for: Strict-Transport-Security: max-age=31536000; includeSubDomains
max-age is in seconds, so 31536000 is one year. Values much shorter than that protect for a short time only. includeSubDomains applies the rule to www. and every other address under your domain, so only add it when all of them work over https://.
Going further: with ; preload added, you can submit your domain at hstspreload.org so browsers know the rule before the first visit. Preloading is hard to undo, so treat it as a later step, not a first one.
Content-Security-Policy (CSP)
What it does: lists where scripts, styles and other parts of your pages may come from. If someone manages to slip a script onto your site, for example through a comment form or a compromised plugin, the browser refuses to run it because it is not on the list.
What to ask for: a policy that fits your site. There is no single value that works everywhere, because it depends on which outside services (analytics, payment forms, chat widgets) your pages use. Ask your developer to start with Content-Security-Policy-Report-Only, which reports problems without blocking anything, and to switch to the enforcing header once nothing on the site breaks.
A policy can exist and still not help. A policy that allows 'unsafe-inline' scripts or any source (*, https:) blocks very little. A Scanylo report calls that a weak policy.
X-Frame-Options, or frame-ancestors
What it does: stops other websites from showing your pages inside a frame on their own page. That trick, called clickjacking, lets an attacker put an invisible copy of your page under their own buttons, so a visitor clicks on your site without knowing it.
What to ask for: X-Frame-Options: SAMEORIGIN (or DENY if your pages never need to be framed at all). The modern way to say the same thing is frame-ancestors 'self' inside your Content-Security-Policy. Either one is enough.
X-Content-Type-Options
What it does: tells the browser not to guess what kind of file it received. Without it, a browser may treat an uploaded text file as a script and run it.
What to ask for: X-Content-Type-Options: nosniff. This header has exactly one value.
Referrer-Policy
What it does: controls how much of your page address is passed on when a visitor clicks a link to another site. Full addresses can contain search terms, order numbers or reset tokens.
What to ask for: Referrer-Policy: strict-origin-when-cross-origin. Current browsers already behave this way when the header is missing, which is why a missing Referrer-Policy is only a minor finding. Setting it explicitly also covers older browsers.
Permissions-Policy
What it does: switches off browser features your site does not use, such as the camera, microphone and location. If a script is ever injected into your page, it cannot ask for them.
What to ask for: something like Permissions-Policy: camera=(), microphone=(), geolocation=(), adjusted to the features your site really does use. This is a tidy-up, not an urgent fix.
Headers that say too much
Some headers do not protect anything; they just announce your software and its version to every visitor:
Server: Apache/2.4.41 (Ubuntu)orServer: nginx/1.18.0X-Powered-By: PHP/8.1.2X-AspNet-VersionandX-AspNetMvc-Version
A version number is not a weakness in itself, but it saves an attacker the work of finding out which known flaws to try. Ask for the version to be removed (the server name alone is fine) and for X-Powered-By to be dropped.
A header you can skip
X-XSS-Protection switched on a filter that modern browsers have removed. The OWASP Secure Headers Project recommends either leaving it out or sending X-XSS-Protection: 0. A Scanylo report does not ask for it.
Notes for the developer: the fix on common stacks
Headers can be set in the web server, in the application, or at a CDN. Set each one in one place only, so they do not end up duplicated or contradicting each other. The examples below set the safe, site-independent headers; add a Content-Security-Policy separately once you have tested one in report-only mode.
nginx (in the server block; always also covers error pages):
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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=()" always;
server_tokens off;
nginx only inherits add_header lines into a location block that has none of its own, so check the headers on a few different pages.
Apache (in the virtual host or .htaccess, with mod_headers enabled). This is also the usual route for WordPress on Apache hosting:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
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=()"
Header unset X-Powered-By
ServerTokens Prod (in the main server configuration, not .htaccess) removes the version from the Server header. For PHP's X-Powered-By, expose_php = Off in php.ini is the cleaner fix.
Cloudflare: HSTS has its own setting under SSL/TLS, Edge Certificates. The other headers can be added with a response header transform rule (under Rules in the dashboard), which applies them to every response without touching the server.
Netlify: a _headers file in the published folder:
/*
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Vercel: a headers entry in vercel.json with "source": "/(.*)" and one key/value pair per header.
IIS and ASP.NET: <customHeaders> under <system.webServer><httpProtocol> in web.config; <httpRuntime enableVersionHeader="false" /> removes X-AspNet-Version.
Managed platforms such as Shopify, Wix or Squarespace set most headers themselves and do not let you change them. If a finding is about a header you cannot set, ask the platform's support.
Check the result in your browser's developer tools (Network tab, click the page, look at Response Headers) or run the free check above.
How Scanylo checks headers
A Scanylo check reads the headers on your home page and up to two pages linked from it, and checks HSTS on the other addresses under your domain that your site itself names, such as www.. It never sends anything unusual: these are the same headers every visitor's browser receives. The details are on how the headers check works.
Sources
More guides
- SPF and DMARC for small sites · Three DNS records that stop strangers sending email that looks like it comes from you, and the order to set them up in.
- What an external website scan can and cannot see · What any outside-in check, ours included, can learn about your site, where it goes blind, and what a clean report does and does not prove.