Plate № 27 · Ops · security / audit

A pattern from the gf.cx specimen book

Security-header audit · count the live response, own the headers once

Source · Dan, 2026-07-25

"Auditing and assessment is a vital skill for sites."

Why it's an audit, not a glance

Security response headers fail in two quiet ways, and a glance catches neither. A header can be missing — no Content-Security-Policy, so a scanner marks the site down and nobody notices until the grade drops. Or a header can be duplicated — the same X-Content-Type-Options: nosniff emitted twice because two layers each set it. A browser tolerates the duplicate, so the page looks fine; but it's a signal of drift — two sources both think they own the header, and the day their values disagree you get undefined behaviour and a confusing debug.

Neither shows up if you read the headers with your eyes — a duplicated header reads as "yes, it's there." The audit that catches both is mechanical: don't read the headers, count them.

The method — count, don't eyeball

curl -sSI the live URL and count each security header. Absent (0) means missing; more than one means two emitters. And the pattern of which headers doubled fingerprints the culprit before you've opened a single config panel:

# count every security header in the live response
for h in content-security-policy strict-transport-security x-content-type-options \
        x-frame-options referrer-policy permissions-policy; do
  printf '%-30s %s\n' "$h" "$(curl -sSI https://dare.co.uk/ | grep -ci "^$h:")"
done


content-security-policy       1
strict-transport-security     1
x-content-type-options        2  ← two emitters
x-frame-options               1
referrer-policy               1
permissions-policy           1

The read: only X-Content-Type-Options is doubled; the other three appear exactly once. So the second emitter produces only that one header — which is the exact signature of Cloudflare's Managed Transform → "Add security headers" (it adds precisely nosniff and nothing else). Before touching any dashboard, the counts have already told you there are two sources and named one of them.

The reading — what a doubled header tells you

Count patternWhat it means
a header at 0no emitter — missing; a scanner will dock you
every header at 1one clean owner — the goal state
one header doubled, rest singlea second source that emits only that header — often a managed/default layer bolted on beside your own rule
several doubled togethertwo full header-setting layers overlapping (e.g. a Transform Rule and a _headers file both firing)

The fix — one owner, set not add

A duplicate is almost always an append where an overwrite was meant. Cloudflare's response-header transform has two operations: add appends a header (HTTP permits repeats), set replaces it. When a custom Transform Rule uses add and a managed layer already emitted the same header, you get two. Switch the rule to set and it collapses to one — set overwrites whatever the earlier layer left, so the rule becomes the single effective owner regardless of order.

Single-owner principle: exactly one source of security headers per zone — a custom Transform Rule or Managed Transforms or a _headers/_worker layer — never two. And for a single-value header, set, never add.

On dare.co.uk the fix was one API call. Read the rule (a per-rule GET is refused for this token scheme — read the whole ruleset and filter), then PATCH every header to set:

ZID=$(curl -s -H "Authorization: Bearer $CF_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones?name=dare.co.uk" \
  | jq -r '.result[0].id')

# the custom rules live in the response-header transform phase entrypoint
curl -s -H "Authorization: Bearer $CF_TOKEN" \
  "$API/zones/$ZID/rulesets/phases/http_response_headers_transform/entrypoint"

# switch add → set (overwrite) on the culprit rule
curl -X PATCH -H "Authorization: Bearer $CF_TOKEN" -H "Content-Type: application/json" \
  "$API/zones/$ZID/rulesets/$RULESET/rules/$RULE" --data '{
    "action": "rewrite",
    "expression": "true",
    "action_parameters": { "headers": {
      "X-Content-Type-Options": { "operation": "set", "value": "nosniff" },
      "X-Frame-Options":        { "operation": "set", "value": "SAMEORIGIN" },
      "Referrer-Policy":        { "operation": "set", "value": "strict-origin-when-cross-origin" },
      "Permissions-Policy":     { "operation": "set", "value": "geolocation=(), microphone=(), camera=()" }
    }}
  }'

Re-run the count. It flaps 2 / 1 for a minute as the ruleset version rolls out edge-node by edge-node, then settles at 1:

# sample repeatedly — the change propagates POP-by-POP
for i in $(seq 1 20); do curl -sSI "https://dare.co.uk/?n=$i$RANDOM" \
  | grep -ci '^x-content-type-options:'; done | sort | uniq -c


 19 1  ← converged (single, fixed)
  1 2  ← one stale edge, clears within ~1-3 min

The result — a measured climb to A+

The dedup isn't only cosmetic. The duplicated, trailing-whitespace header was itself failing a test on Mozilla's HTTP Observatory (v5). The only change between the first two live scans was add → set plus trimming the value — one test flipped and the grade moved. Two later, separate passes — hash-pinning the CSP's inline scripts to drop 'unsafe-inline', then a TLS-floor + HSTS-subdomains hardening — carried it the rest of the way to A+:

Before · duplicated header
B 75/100
8 / 10 tests · x-content-type-options ✗
emitted twice, value "nosniff    "
Then · single owner (set)
B+ 80/100
9 / 10 tests · x-content-type-options ✓
one clean nosniff
Now · CSP + TLS locked
A+ 115 score
10 / 10 tests · content-security-policy ✓
inline scripts hash-pinned, 'unsafe-inline' dropped

What was the last deduction — the CSP's 'unsafe-inline' — has since been closed: every executable inline script is hash-pinned (a per-script sha256 in script-src), so 'unsafe-inline' could be dropped, and a min-TLS-1.2 + HSTS-includeSubDomains pass cleared the last transport-layer flags. dare.co.uk now scores A+ · 115 · 10/10. See the live Observatory scan ↗. The lesson the numbers make concrete: a duplicate header isn't cosmetic — a scanner docks you for it — and the climb from B to A+ is a ladder of single-owner, structural fixes, each worth points.

The trap — the invisible second owner

The reason this needs an audit and not a code review: one of the two owners is often nowhere in your repo. On dare.co.uk the custom Transform Rule was findable via the API — but the second nosniff came from a Managed Transform, a dashboard toggle in a different part of Cloudflare, invisible to grep across _headers, _worker.ts, and wrangler.jsonc (all three were clean). You cannot reason your way to "there are two emitters" from the source tree; only the live response shows the truth. That's the whole case for counting the wire over reading the config.

And the same token that edits your custom rule may not reach the managed layer — here the managed_headers endpoint returned "request is not authorized." So don't plan the fix as "turn off the duplicate at its source"; plan it as "make my one owner authoritative with set," which you can do, and which is robust even if the managed layer is later toggled off.

Gotchas

Where it applies

Reusable elements

The named, copyable pieces — lift any one without the others:

Reference

Source
Dan, 2026-07-25 — "auditing and assessment is a vital skill for sites", after a security sweep flagged dare.co.uk's headers.
In use / example
dare.co.ukX-Content-Type-Options was emitted twice (a custom Transform Rule with add plus the "Add security headers" Managed Transform); fixed to a single nosniff by switching the rule to set. That flipped the Observatory x-content-type-options test and lifted the grade from B / 75 to B+ / 80 (CSP had shipped the day prior); a later CSP hash-pin (dropping 'unsafe-inline') plus a min-TLS-1.2 + HSTS-subdomains pass carried it to A+ · 115 · 10/10.
Reusable elements
the count loop · the fingerprint read · the single-owner rule · set-not-add · the sample-until-settled check · the Observatory grade (listed above).
Kin
Visual verification · external probe and Diagnostic probes panel — the same "assess from outside, on the live thing" instinct, applied to render and to health.
Origin
A duplicate header a code review would never have found — because the second owner lived in a dashboard toggle, not the repo. The audit's rule: count the wire, not the config; own each header once.