HTTP security headers for NZ sites: CSP, HSTS, X-Frame-Options, Referrer-Policy and Permissions-Policy explained

Quick answer

Security headers are short instructions a web server sends with every page that tell the browser what it is allowed to do. The five worth setting on every New Zealand school or small-business site are Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. Each is one or two lines of server config; together they close the most common browser-side attacks without changing your code.

If you want to check your own site in ten seconds first, run the Free Website Security Check — it checks all of these headers plus TLS, cookies, exposed files, and mixed content.

What these security headers actually do

Every time your browser loads a page, your web server has a chance to send back a few extra instructions in the response — before any HTML, JavaScript, or image is delivered. These instructions are called HTTP response headers. Most of them are invisible to you; a few affect what the browser is allowed to do on your behalf.

The five that matter most for a public-facing website are:

| Header | What it stops | One-line description | |---|---|---| | Content-Security-Policy (CSP) | Cross-site scripting (XSS), malicious iframes | Tells the browser which sources of script, style, and images are allowed | | Strict-Transport-Security (HSTS) | Downgrade attacks, cookie theft over plain HTTP | Forces the browser to use HTTPS for a set period | | X-Frame-Options | Clickjacking (your page being loaded inside a malicious frame) | Stops your site being embedded on someone else's page | | X-Content-Type-Options | MIME-type sniffing attacks | Tells the browser to honour the declared content type, not guess | | Referrer-Policy | Leaking student or staff names in the Referer header | Controls how much of the URL gets sent when users click a link | | Permissions-Policy | Third-party scripts hijacking camera, mic, or location | Lets you turn off powerful browser features your site does not use |

None of these headers fix a vulnerability in your code. They limit what an attacker can do if something else goes wrong. Think of them as seatbelts — they are cheap, they do not stop the crash, but they change the outcome.

For dedicated reading on the headers themselves, the Mozilla Web Security guidelines remain the most accurate plain-English reference.

Why this matters for New Zealand schools and small businesses

Most New Zealand schools and small businesses are not targeted by sophisticated attackers. They are targeted by opportunistic scanning — automated bots that crawl the public web looking for sites with missing or weak security headers (1). The bots do not care how big your school is. They care that you forgot to set a header in 2019.

Three reasons this matters more than it might first appear:

  • The school website is a parent-facing surface. A missing header is not a private technical detail — it is a public signal that the site, and therefore the data and people connected to it, may not be looked after carefully.
  • Student data is in scope of the Privacy Act 2020. If a breach can be traced back to a misconfiguration that a single header would have prevented, that is a defensibility problem for the school, even though the legislation does not name headers specifically (2).
  • CERT NZ and the NCSC both name header hygiene as a baseline control. "Baseline controls" are the things the agencies expect every New Zealand organisation to have done already before an incident is investigated (1)(3).

Setting the five recommended headers is a one-day job for whoever runs your hosting. After that, the bots stop flagging your site as a soft target, and you have a stronger starting position if something else goes wrong.

If you are responsible for a school's broader digital footprint, the Building Cyber-Safe Habits as an NZ Educator guide covers the wider context.

Content-Security-Policy — the most important one

Content-Security-Policy (CSP) is the most powerful header on this list, and the most confusing to write. It tells the browser which sources of script, style, images, and connections your site is allowed to load. Anything not explicitly allowed gets blocked.

A reasonable starting CSP for a school or small-business site looks like this:

Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'

In plain language:

  • default-src 'self' — by default, only load things from your own domain
  • img-src 'self' data: — images from your domain, plus inline data URIs (small icons)
  • script-src 'self' — JavaScript only from your domain (no third-party trackers, no inline scripts)
  • style-src 'self' 'unsafe-inline' — CSS from your domain, with inline styles allowed (common for school sites using inline style blocks)
  • frame-ancestors 'none' — nobody can embed your site in an iframe (this replaces the older X-Frame-Options header)
  • form-action 'self' — forms can only post back to your own domain

You will likely need to relax one or two directives to match what your site actually loads. A school site that embeds a YouTube video, for example, needs frame-src https://www.youtube.com. The right way to write a CSP is to start strict, then loosen it one directive at a time, testing as you go.

A CSP that breaks the site is worse than no CSP. If you are not sure, deploy the policy in report-only mode first — change Content-Security-Policy to Content-Security-Policy-Report-Only and the browser will send violation reports to a URL you specify instead of actually blocking anything.

For a deeper walkthrough of CSP unsafe-inline, unsafe-eval, and wildcard directives, see the CSP Deep Dive guide.

Strict-Transport-Security — the quick win

Strict-Transport-Security (HSTS) is the easiest header on this list to get right. One line tells the browser: for the next year, only ever load this site over HTTPS, even if a user types http:// or follows an http:// link (4).

Strict-Transport-Security: max-age=31536000; includeSubDomains

In plain language:

  • max-age=31536000 — one year, in seconds. Any future visit within that year will refuse to fall back to HTTP
  • includeSubDomains — applies to every subdomain (e.g. portal.yourschool.nz). Only add this if you are certain every subdomain is HTTPS-ready

Once a browser has received HSTS, it will silently rewrite any http:// requests to https:// for that period. This blocks a category of attack where an attacker briefly forces a visitor over plain HTTP on a coffee-shop network to capture credentials.

HSTS only fires after the first HTTPS visit. To get into the browser's HSTS preload list (so even the first visit is protected) you need to submit your domain to the hstspreload.org list. That is optional — the basic header covers almost everyone.

Once you have HSTS in place, the remaining headers are mostly set-and-forget — they do not change between visits the way HSTS does.

Content-Security-Policy, HSTS and X-Frame-Options explained for NZ sites — browser security headers in plain language

X-Frame-Options and X-Content-Type-Options — the classic guards

These two headers are older than CSP but are still worth setting for older browsers and as defence-in-depth.

X-Frame-Options: DENY
X-Content-Type-Options: nosniff

X-Frame-Options: DENY stops your page being loaded inside an iframe on another site. The attack it prevents is clickjacking — an attacker overlays your site invisibly inside a frame, then tricks the user into clicking something they did not intend to click. If you have already set CSP's frame-ancestors 'none' (recommended), this header is technically redundant, but browsers older than CSP support will still respect it (5).

X-Content-Type-Options: nosniff tells the browser: do not try to guess what kind of file this is. Without it, a clever attacker can sometimes persuade the browser to interpret a .txt file as a script. With nosniff set, the browser will only execute a file if its declared content type matches what is being executed (5).

Both headers cost nothing to set and never break anything. Set them on every site you manage.

Referrer-Policy and Permissions-Policy — the modern pair

These two are newer and often missed. Both default to nothing, which means most browsers fall back to "share as much as possible" or "everything allowed" behaviour.

Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()

Referrer-Policy: strict-origin-when-cross-origin means: when a user clicks a link from your site to another site, only send the origin (e.g. https://yourschool.nz) in the Referer header, not the full URL. This matters if your URLs include anything sensitive — student IDs, assessment names, search query strings (6).

Permissions-Policy lets you turn off browser features your site does not need. The example above disables camera, microphone, geolocation, payment, and USB on every page. If you do not embed a video-conference widget, you should not be handing camera permission to any script that loads.

A more permissive version for a school that uses Google Meet might be:

Permissions-Policy: camera=(self "https://meet.google.com"), microphone=(self "https://meet.google.com"), geolocation=(), payment=(), usb=()

That allows Meet to access camera and microphone, but only when embedded on your domain; everything else is disabled.

Copy-paste fixes for the four most common setups

Below are minimal, tested snippets for the four stacks most New Zealand schools and small businesses run on. Paste them, restart the relevant service, and the headers will be live.

nginx

Add inside your server { ... } block:

add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" 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=(), payment=(), usb=()" always;

Reload with nginx -s reload (or sudo systemctl reload nginx on systemd hosts).

Apache

Add inside your <VirtualHost>, or in an .htaccess in the document root:

<IfModule mod_headers.c>
  Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
  Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
  Header always set X-Frame-Options "DENY"
  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=(), payment=(), usb=()"
</IfModule>

If the headers disappear on Apache, the most common cause is a mod_headers mismatch with how the site is hosted — confirm mod_headers is loaded (apache2ctl -M | grep headers).

Express (Node.js)

Use the widely-trusted helmet package:

npm install helmet

Then in your app:

import helmet from 'helmet';
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      imgSrc: ["'self'", 'data:'],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      connectSrc: ["'self'"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"],
    },
  },
  strictTransportSecurity: { maxAge: 31536000, includeSubDomains: true },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

helmet covers all six headers plus several others automatically.

Cloudflare

If you front your site with Cloudflare, you can set most of these from the dashboard:

  1. Go to SSL → Edge Certificates → HSTS → enable with max-age=31536000, includeSubDomains, preload (read the warning before submitting to the preload list)
  2. Go to Rules → Transform Rules → Modify Response Headers → add a rule matching all requests and setting the other five headers
  3. Alternatively, set them at origin and Cloudflare will pass them through

If you only set headers at Cloudflare and not at origin, your site loses those headers the moment Cloudflare is bypassed (e.g. for a direct API call). Set them at both layers where you can.

Verify your headers before saying you're done

Setting the headers is half the job. Verifying them is the other half. Two ways to check:

1. Run the free NZAI scanner

The Free Website Security Check at nzscan.nzaisecurity.com checks all five headers in about ten seconds, alongside TLS, exposed files, cookies, and mixed content. If any header is missing, the report tells you which snippet to paste for nginx, Apache, Express, or Cloudflare.

2. curl from the command line

For a quick sanity check without opening a browser:

curl -sI https://yoursite.nz | grep -iE "content-security-policy|strict-transport-security|x-frame-options|x-content-type-options|referrer-policy|permissions-policy"

You should see all six header names in the output. If any are missing, you have a deploy problem — either the server did not reload, or a caching layer (Cloudflare, Varnish, a CDN) is stripping them. To bypass cache for the test, add a ?nocache=1 query string or run against a fresh URL.

3. Browser DevTools

Open your site in Chrome or Firefox, press F12 to open DevTools, switch to the Network tab, click any page load, and inspect Response Headers in the right pane. All six should be visible. This is what an attacker sees — and what a real visitor's browser sees.

Common header mistakes to avoid

From running the free scanner across New Zealand school and small-business sites, the most frequent missteps are:

  • Setting a header but a CDN strips it. Cloudflare, Varnish, and some reverse proxies overwrite or drop headers set at the origin. Always verify through the CDN edge, not just from the origin server.
  • CSP that uses unsafe-inline and unsafe-eval everywhere. Defeats most of what CSP is for. If you cannot remove them, scope them to specific directives (style-src 'unsafe-inline' is more acceptable than script-src 'unsafe-inline').
  • HSTS sent over plain HTTP. HSTS only works over HTTPS. If your site is still reachable over HTTP for any reason, HSTS will not fire.
  • CSP report-only mode left on forever. Report-only mode does not protect the site. Once you have triaged the violation reports, switch back to enforcement mode (Content-Security-Policy, not Content-Security-Policy-Report-Only).
  • Wildcard * in script-src or frame-ancestors. script-src * is almost the same as no CSP. frame-ancestors * allows any site to frame yours. Always be explicit.
  • Setting the headers in local dev but not in production config. The build environment that runs on your laptop is rarely the same as the one that ships. Verify on the live URL.

Knowledge check

Test your understanding of HTTP security headers with these questions. Click or tap each card to flip it and see the answer.

Q1Your school website passes a free scan but the report flags Content-Security-Policy, Strict-Transport-Security, Referrer-Policy, and Permissions-Policy as missing. X-Frame-Options and X-Content-Type-Options are set. Which header is the most dangerous to leave unset, and why?tap to flip
Answer: Content-Security-Policy is the most consequential of the four missing headers. Without CSP, a single cross-site scripting bug anywhere on the site can be used to load attacker-controlled scripts and exfiltrate form data. Referrer-Policy and Permissions-Policy are useful but lower-stakes. HSTS is high-value but its absence is mostly felt on first-visit downgrade attempts, not normal browsing. Set CSP first, then HSTS, then the others (1)(3).
Q2You paste a recommended nginx CSP into your config and reload. The headers show up correctly, but the site breaks — fonts will not load, the contact form will not submit, and embedded YouTube videos show errors. What is the most likely cause?tap to flip
Answer: The CSP is correct in principle but too restrictive for what the site actually loads. A standard CSP allows only same-origin by default (default-src 'self'), so any third-party font CDN, form action endpoint, or embedded video domain needs its own directive (for example font-src https://fonts.gstatic.com, form-action 'self' https://forms.example.com, frame-src https://www.youtube.com). Read the browser console errors to see which directive blocked which request, then loosen only the directives that need it (5).
Q3You set HSTS with max-age=31536000 and includeSubDomains on Friday. On Monday morning the IT team spins up a new staging.yourschool.nz subdomain on plain HTTP for testing — and it stops responding entirely. What happened, and is it fixable without consequences?tap to flip
Answer: This is exactly what HSTS was designed to do. Every visitor who has visited any subdomain of your school in the last year has been told that all subdomains must use HTTPS for the next year. Their browser now refuses plain HTTP on staging.yourschool.nz and silently rewrites it to HTTPS. If the subdomain does not serve HTTPS, the user's browser shows an error. Fixing it requires serving HTTPS on the subdomain — there is no way to "undo" HSTS for affected visitors during the max-age period, which is why includeSubDomains should only be added once every subdomain is HTTPS-ready (4).

Sources and references

[1] CERT NZ. (2025). Top 11 cyber security tips for your business. https://www.cert.govt.nz/business/guides/top-tips-for-cyber-security/

[2] New Zealand. Parliament. (2020). Privacy Act 2020. https://www.legislation.govt.nz/act/public/2020/0031/latest/whole.html

[3] New Zealand. National Cyber Security Centre. (2025). Cyber security baseline controls for organisations. https://www.ncsc.govt.nz/

[4] Internet Engineering Task Force. (2012). RFC 6797 — HTTP Strict Transport Security (HSTS). https://datatracker.ietf.org/doc/html/rfc6797

[5] Mozilla. (2025). MDN Web Docs — Content Security Policy (CSP). https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

[6] Mozilla. (2025). MDN Web Docs — Referrer-Policy header. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy

[7] Mozilla. (2025). Web security guidelines. https://infosec.mozilla.org/guidelines/web_security

[8] Chromium Project. (2025). HSTS preload list submission. https://hstspreload.org

What to do next