Guides Create account 🇬🇧 🇩🇪
  • Guides
  • Create account
  • Sign in
  • 🇬🇧 🇩🇪
  • Security guide

    How to monitor your security headers

    Setting security headers is a one-off job. Keeping them is not: the next deploy, a CDN rule or a plugin update can remove or loosen them, and nothing on the page looks different when it happens. This guide shows three ways to notice, with scripts you can use as they are and an honest note on what each one misses.

    Scan my headers All security headers

    01 Why it matters

    Headers change without anyone deciding to

    Security headers live in configuration that many hands touch: the web server, the application, a CDN or reverse proxy in front of it, and the plugins of a CMS. A server migration that forgets one include, an nginx location block with its own add_header line, which silently drops every header inherited from the server block, or a CDN rule that rewrites responses are enough. The site keeps working, which is precisely why nobody notices.

    A one-off check only tells you the state on the day you ran it. Monitoring means comparing the live headers against a known good state again and again, and being told when they differ.

    02 What to watch

    Write down what correct looks like

    Every approach below compares against a baseline: the headers and values your site should send. Keep it as a plain text file with one header per line, names in lower case, sorted. That makes a change show up as a readable diff instead of a wall of text.

    content-security-policy: default-src 'self'; script-src 'self' 'nonce-X'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none' permissions-policy: camera=(), microphone=(), geolocation=(), payment=() referrer-policy: strict-origin-when-cross-origin strict-transport-security: max-age=31536000; includeSubDomains x-content-type-options: nosniff x-frame-options: DENY

    Two things vary on every request and have to be neutralised before comparing: a CSP nonce, which is new per response, and headers such as date or set-cookie, which say nothing about your security. The scripts below only keep the security headers and replace every nonce with a fixed placeholder.

    03 Option 1

    A cron job with curl

    The simplest monitor is a shell script on any server you already run. It fetches the headers, reduces them to the security headers, compares them with the result of the previous run and sends a mail when they differ. The first run only records the baseline.

    #!/bin/sh set -eu url="https://www.example.com/" dir="/var/lib/header-watch" mkdir -p "$dir" curl -fsS -o /dev/null -D "$dir/raw.txt" "$url" tr -d '\r' < "$dir/raw.txt" \ | sed -E "s/'nonce-[^']+'/'nonce-X'/g" \ | awk -F': ' 'tolower($1) ~ /^(content-security-policy(-report-only)?|permissions-policy|referrer-policy|strict-transport-security|x-content-type-options|x-frame-options)$/ { print tolower($1) ": " substr($0, length($1) + 3) }' \ | sort > "$dir/current.txt" if [ -f "$dir/baseline.txt" ] && ! diff -u "$dir/baseline.txt" "$dir/current.txt" > "$dir/diff.txt"; then mail -s "Security headers changed: $url" ops@example.com < "$dir/diff.txt" fi mv "$dir/current.txt" "$dir/baseline.txt"
    MAILTO=ops@example.com */30 * * * * /usr/local/bin/header-watch.sh

    curl -f makes the script fail on an error status, and set -e stops it right there, so cron mails the curl error to MAILTO instead of the script reporting that every header vanished. Each change is mailed once, because the current state becomes the new baseline. Run it every 30 minutes or once an hour; header changes come with deploys, not by the second.

    04 Option 2

    A check after every deploy

    A cron job notices a change within the hour. A check in your deploy pipeline notices it in the same minute, and it can name the deploy that caused it. The script below fails when a required header is missing or the policy allows unsafe-inline or unsafe-eval or has no form-action.

    #!/bin/sh set -eu curl -fsS -o /dev/null -D headers.txt "$1" tr -d '\r' < headers.txt > headers.clean fail=0 for header in strict-transport-security content-security-policy x-content-type-options referrer-policy; do if ! grep -qi "^$header:" headers.clean; then echo "missing header: $header" fail=1 fi done csp=$(awk 'tolower($0) ~ /^content-security-policy:/' headers.clean) case "$csp" in *"'unsafe-inline'"*) echo "policy allows 'unsafe-inline'"; fail=1 ;; esac case "$csp" in *"'unsafe-eval'"*) echo "policy allows 'unsafe-eval'"; fail=1 ;; esac case "$csp" in *form-action*) ;; *) echo "policy has no form-action"; fail=1 ;; esac exit "$fail"
    security-headers: stage: .post image: alpine:3.20 before_script: - apk add --no-cache curl script: - sh ci/check-headers.sh "$PRODUCTION_URL"

    The .post stage always runs last in GitLab CI, after the deploy job. In GitHub Actions the same script runs as a step after the deploy step. The check is deliberately strict: it rejects unsafe-inline even next to a nonce, where browsers ignore it. Relax that line if your policy relies on the fallback for old browsers.

    05 Option 3

    The Erseni Scan monitor

    If you would rather not run scripts yourself, the monitor rescans your site at a fixed interval, every 1, 3, 7, 14 or 30 days, 7 by default. Each run is a full scan, so it covers not only the headers but TLS, cookies, DNS and mail records, and the other dimensions of the report.

    After each run it compares the new scan with the previous one and sends a mail when the overall score drops or a finding appears that was not there before, with the titles of the new findings. It does not report every changed header value: a new host in an already strong policy, or a further weakness in a policy that is already reported as weak, leaves the score and the findings unchanged and so causes no mail. For that, the diff in option 1 or 2 is the right tool.

    Monitoring is part of the Agency plan and covers up to 25 websites. Without it, you can run a free scan now, scan again after your next deploy and open the comparison from the report, which shows the findings that were resolved, the ones that appeared and how the scores moved.

    See the Agency plan

    06 Which one

    What each approach catches and misses

    Cron job with curl

    Catches. Every change to a header value you watch, including one extra host in a policy, within one interval.

    Misses. Anything outside the headers you listed, and pages other than the one URL. It runs on your server, so it stops when that server does.

    CI check after deploy

    Catches. Missing headers and the policy weaknesses you test for, immediately and tied to the deploy that caused them.

    Misses. Changes that do not come through your pipeline: CDN rules, hosting panels, CMS plugins updated in the admin area.

    Scan monitor

    Catches. Any new finding and any drop in the score, across headers, TLS, cookies, DNS and the rest of the report, whatever caused it.

    Misses. Value changes that do not create a new finding or lower the score, and anything between two scheduled runs.

    They complement each other: the CI check stops your own mistakes, the diff catches every value change, and the scan notices what gets worse from sources you do not control. For the content security policy in particular, detecting changes has a guide of its own.

    Detecting CSP changes

    07 Questions

    What people ask about monitoring headers

    How often should I check my security headers?

    After every deploy, and on a schedule in addition for changes that do not go through your pipeline. Hourly is plenty for a curl check, and weekly is a sensible default for a full scan, because headers change with configuration, not with traffic.

    Why does my header check report a change on every run?

    Almost always because of a CSP nonce, which is new in every response, or because date, set-cookie or a request ID ended up in the comparison. Compare only the security headers and replace the nonce with a fixed placeholder before diffing, as the scripts above do.

    Does the Erseni Scan monitor report every header change?

    No. It reports when the overall score drops or a new finding appears. A header that disappears, a policy that switches to report-only, or a previously clean policy that gains unsafe-inline is caught that way. A changed value that creates no new finding, such as an extra script host in a strong policy, is not. For those, compare the header values directly.

    Should I check only the homepage?

    Start with it, then add the pages that are served differently: login, checkout, the API and anything behind a separate location block or application. Headers are often set per path, and a missing header on the login page matters more than one on the homepage.

    08 In depth

    Related security topics

    Monitoring tells you when something changed. These guides explain what each header should look like in the first place.

    HTTP security headers

    What each response header does, which ones your site should send and how to configure them correctly.

    Read guide

    Content Security Policy

    The strongest defence against cross-site scripting: which directives to set, how to roll a policy out in report-only mode and how to reach an enforcing policy without breaking your site.

    Read guide

    HSTS

    Strict-Transport-Security explained: what max-age, includeSubDomains and preload actually do, and why the preload list is a decision you cannot quickly undo.

    Read guide

    Cookie security

    Secure, HttpOnly and SameSite: the cookie attributes that keep a session out of reach of scripts and cross-site requests, with the settings for a typical stack.

    Read guide

    Clickjacking and X-Frame-Options

    How an invisible overlay turns a visitor click into an action on your site, and the two headers that stop it: X-Frame-Options for older clients, frame-ancestors for everything else.

    Read guide

    Referrer-Policy

    Which part of your URLs travels to other sites when a visitor clicks away: the five policy values that matter, what each one gives up and the one to set by default.

    Read guide

    Permissions-Policy

    Switch off camera, microphone, geolocation and payment for your site and everything it embeds: the allow-list syntax, and why a feature you did not name is not a feature you turned off.

    Read guide

    SPF, DMARC and DNSSEC

    The records that stop someone sending mail in your name, and the one that keeps your DNS answers honest: what each does and the order to introduce them in.

    Read guide

    security.txt

    The file that tells a researcher where to report a vulnerability. Two mandatory fields, ten minutes of work, and the difference between a private report and a public one.

    Read guide

    Detecting CSP changes

    How a policy changes quietly through deploys, plugins and CDN rules, and how to catch it: a header diff, violation reports through report-to, and what a scan comparison can and cannot show.

    Read guide

    Keep reading

    Every guide stands on its own, and together they cover what our scanner looks at. Pick the one closest to your next question.

    Is your website affected? BFSG check Accessibility statement BFSG for online shops BFSG for medical practices BFSG for hotels BFSG for tradespeople HTTP security headers Privacy signals Content Security Policy HSTS Cookie security Clickjacking and X-Frame-Options Referrer-Policy Permissions-Policy SPF, DMARC and DNSSEC security.txt Detecting CSP changes Security headers in nginx Security headers in Apache Security headers in WordPress Security headers in IIS Security headers in TYPO3 Security headers in Shopware 6 Security headers in Plesk All guides

    Start with the state of today

    Run a free scan to get your baseline: every security header assessed and explained in plain language. Scan again after your next deploy and compare, or let the Agency monitor do that on a schedule.

    Scan a website Monitoring in the Agency plan
    Recent scans Guides Local leagues Pricing Methodology For hosters API Data protection Imprint Accessibility Terms Cancel contracts here © 2026 Erseni