Ahosting Logo
Knowledge Base

How to Set Up Website Security Headers

Security headers in order of riskSafe on any sitenosniff, a referrerpolicy, SAMEORIGINframingHSTS, carefullyshort lifetime first;it cannot be withdrawnCSP in report-onlycollect what it wouldhave blocked, for weeksCSP enforcedonly once the reportsare quietA content security policy switched straight to enforcing breaks a real site almost every time, and the breakage isinvisible to you.

Response headers can close whole categories of attack, and most sites send none of them. They cost nothing to add and one of them can break your site if added carelessly, which is the reason to understand what each does rather than pasting a block.

Start with the two that cannot break anything

X-Content-Type-Options: nosniff stops browsers guessing a file's type from its contents. Without it, a file you serve as plain text can be interpreted as something executable.

Referrer-Policy: strict-origin-when-cross-origin stops full URLs leaking to other sites in the referrer header, which matters when your URLs contain anything identifying.

Both are safe on any site. Add them first.

Then control framing

X-Frame-Options: SAMEORIGIN stops other sites embedding your pages in an invisible frame over their own content. The technique that tricks people into clicking something they cannot see.

Use SAMEORIGIN rather than DENY unless you are certain nothing frames your own pages. Some payment flows and admin previews do.

The modern equivalent is a frame-ancestors directive in a content security policy, and where both are present browsers prefer that one.

HSTS, with a short value first

Strict-Transport-Security tells browsers never to use plain HTTP for your domain.

It is genuinely valuable and it is the header that can hurt you, because it is difficult to undo: a browser that received it refuses HTTP for the stated period regardless of what you do afterwards.

So start with a short max-age, confirm everything works over HTTPS including every subdomain, and only then raise it. Add includeSubDomains when you are certain every subdomain has a certificate: it applies to ones you forgot. There is more on it alongside the rest of the HTTPS configuration in TLS versions and cipher suites.

Content Security Policy is the powerful one

CSP says which sources may load scripts, styles, images and frames. Done properly it neutralises most cross-site scripting, because injected code has nowhere permitted to come from.

It is also the one that breaks sites, silently, by blocking something legitimate. A theme loading a font from elsewhere, an analytics script, a payment widget. Each needs to be allowed explicitly.

The way to do it without breaking anything:

Use Content-Security-Policy-Report-Only first. It reports what would be blocked and blocks nothing. Leave it for a week of real traffic, read the reports, and build the policy from what your site actually loads rather than from what you think it loads.

Then switch to the enforcing header. Skipping the report-only stage is how people end up with a checkout that fails for some customers.

Where to set them

At the web server, so they apply to everything including files the application never sees.

In .htaccess:

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"

The always keyword matters: without it the headers are omitted from error responses, which are exactly the responses an attacker is provoking.

A plugin can set them too, and it only covers requests that reach the application. Prefer the server.

Do not set them twice

A header set by both the server and a plugin can be sent twice, and browsers handle duplicates inconsistently: with CSP, two policies are intersected, so the result is more restrictive than either and usually breaks something.

Pick one place. Then check the actual response instead of the configuration:

curl -I https://example.com/

Read what is being sent. If a CDN or proxy is in front, check through it as well, some add or strip headers of their own. Setting up a CDN walks through that layer.

Permissions-Policy, briefly

Declares which browser features your pages may use: camera, microphone, geolocation.

Disabling what you do not use is free and mostly protects against a compromised third-party script asking for something on your behalf:

Permissions-Policy: camera=(), microphone=(), geolocation=()

What headers do not do

Worth stating, because a good grade on a header scanner is easy to mistake for security.

They do not patch a vulnerable plugin. They do not stop a weak password. They do not stop someone with your credentials.

They reduce the damage of a flaw that already exists, which is real and is a second line in place of a first. Securing WordPress explains the first.

A sensible starting set

nosniff, a referrer policy, and SAMEORIGIN framing: all safe, all immediate.

HSTS with a short max-age, raised once verified.

A permissions policy disabling features you do not use.

CSP last, built from report-only data over a week of real traffic.

That order gives you most of the benefit on the first afternoon and keeps the one risky header behind evidence rather than optimism.

One of these headers cannot be taken back once a browser has stored it, so it deserves its own sequence. How to Set Up HSTS Safely goes into it.

Read what you are already sending

Headers arrive from several places at once, and the first useful step is seeing the actual set rather than the intended one.

curl -sI https://example.com/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'
curl -sI https://example.com/some/page | grep -ic 'x-frame-options'

The second command checks for duplicates. A header sent twice with different values is worse than not sending it, because behaviour then depends on which the browser reads first, and that varies.

Duplication happens when a header is set in the server configuration and again by an application or a plugin. Check a deep page as well as the home page, since a rule scoped to one directory produces a site where the protection exists on some pages only.

Build the policy from what the site actually loads

Writing a content policy by hand and pasting it in is how sites break, and there is a way to derive it instead.

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

In report only mode nothing is blocked and every violation is reported, so a week of real traffic produces the list of everything the site genuinely loads, including the parts you had forgotten.

curl -s https://example.com/ | grep -oE 'src="https?://[^"]+"' | sed 's|.*//||; s|/.*||' | sort -u

That gives a starting list from one page. The report gathers the rest across every page and every visitor path, which is what makes the difference between a policy that holds and one that breaks a checkout at the worst moment.

Test on the paths that matter before enforcing

The pages that break under a strict policy are the ones with third party code, and those are the pages that carry revenue.

Before switching from reporting to enforcing, walk through a complete purchase, a form submission, a login and any page with an embedded map, video or chat widget. Each of those loads from somewhere else and each will be refused if the policy does not name it.

curl -s https://example.com/checkout/ | grep -oE '(src|href)="https?://[^"]+"' | sed 's|.*//||; s|/.*||' | sort -u

Enforce on a low traffic section first if the site allows it. A policy applied everywhere at once, on a Friday, is how a store spends a weekend refusing payments for a reason nobody thinks to look for.