Security Response Header Regression Gate
Pricing
from $10.00 / 1,000 run starteds
Security Response Header Regression Gate
Learn when a deploy, a CDN rule or a proxy change removes or weakens a security header on your production URLs. The Actor compares HSTS, CSP (each directive), X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy and the cross-origi
Pricing
from $10.00 / 1,000 run starteds
Rating
0.0
(0)
Developer
kingii98
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Your security headers are correct today. This Actor tells you when a deploy, a CDN rule, a WAF rule or a proxy change removes or weakens one of them.
The Actor does not give a grade from 0 to 100. It compares the headers of each
URL with the headers of a stored baseline. It reports each header that changed,
with a change class and a severity. It sets gate_pass to false when a
regression is at or above the severity that you select. Call it from your CI job
after each production deploy, and put it on a daily schedule to catch changes
that do not come from a deploy.
What the Actor reads
The Actor sends one GET request to each URL. It follows up to 5 redirects and reads the headers of the final response. It never reads the response body. It uses HTTP only: no browser and no proxy.
It reads these ten headers:
Strict-Transport-Security(HSTS)Content-Security-Policy(CSP) andContent-Security-Policy-Report-OnlyX-Frame-OptionsX-Content-Type-OptionsReferrer-PolicyPermissions-PolicyCross-Origin-Opener-Policy,Cross-Origin-Embedder-PolicyandCross-Origin-Resource-Policy
It does not read CORS headers or cache and compression headers.
What you get
- One summary record for each run: the URL count, the changed URL count,
the regression count for each severity, and the boolean
gate_pass. - One change record for each header that changed on one URL: the URL, the final status, the header name, the baseline value, the current value, the change class, the severity, and a list of details.
- For CSP, the details give the change for each directive, for example
'unsafe-inline' added to script-src,wildcard source * added to script-srcorframe-ancestors removed. - For HSTS, the details give
max-age lowered from 31536000 to 86400,includeSubDomains removedorpreload removed. - One policy violation record for each rule of your optional policy that a URL breaks.
- One URL error record for each URL that the Actor could not check.
A URL with no change gives no record. The summary counts it.
Change classes
| Change class | Meaning | Regression |
|---|---|---|
removed | The baseline had the header, and the response does not. | yes |
weakened | The header gives less protection than the baseline. | yes |
strengthened | The header gives more protection than the baseline. | no |
added | The response has a header that the baseline did not have. | no |
value-changed | The value changed, but the protection did not. For example a new CSP hash or a new report target. | no |
A reordered directive list, a new CSP nonce, other case or other white space is no change.
Severities
| Severity | Examples |
|---|---|
critical | HSTS max-age=0 (browsers delete their HSTS entry). |
high | HSTS or CSP removed. HSTS max-age lowered, or includeSubDomains removed. A CSP directive removed from default-src, script-src, object-src, base-uri or frame-ancestors. 'unsafe-inline', 'unsafe-eval', * or a scheme source (https:, data:) added to one of these directives. 'unsafe-inline' in effect again because the nonce or hash is gone. |
medium | X-Frame-Options, X-Content-Type-Options or COOP removed or weakened. HSTS preload removed. Another CSP directive removed. An unsafe keyword or a wildcard added to another CSP directive. 'strict-dynamic' removed. A Permissions-Policy feature opened to every origin. |
low | Referrer-Policy, Permissions-Policy, COEP, CORP or CSP-Report-Only removed or weakened. A new host source in CSP. X-Frame-Options removed while CSP frame-ancestors is present. |
info | Every change that is not a regression. |
A change in Content-Security-Policy-Report-Only is value-changed with the
severity info, because a report-only policy blocks nothing.
The baseline
The Actor keeps the last normalized header set of each URL in the named
key-value store security-header-baseline. The record key is baseline_name.
If you leave baseline_name empty, the Actor uses one record for each URL set.
- First run for a URL: the Actor writes the baseline and reports no regressions.
- Later run: the Actor compares the headers with the baseline.
- A regression at or above
fail_onkeeps its baseline value. The gate stays failed on each run until the header comes back, or until you reset the baseline. - Every other change becomes the new baseline value, so the Actor reports it one time only.
- Reset: set
reset_baselinetotrueafter you removed or changed a header on purpose. The Actor writes the current headers as the baseline and reports no changes. - A URL that the Actor could not check keeps its baseline.
The Actor writes the record one time, at the end of the run. An aborted run does not change the baseline. The record holds at most 1,000 URLs.
Protection against false regressions
A stability probe of 25 public sites on 2026-09-11 found stable headers on all
25 after normalization (docs/stability-probe.md). The Actor also does these
things:
- It masks CSP nonces, and it ignores the query of a
report-uriand the parameters of COOP and COEP. - When a URL shows a regression, the Actor requests the URL one more time. It
reports only the regressions that the second response repeats. The summary
counts the others in
unconfirmed_regressions. Sometimes the second request gets no usable answer: it times out, it cannot connect, it reaches the run deadline, or its status class is different. Then the Actor cannot confirm or reject the regressions. It reports each of them withconfirmed: false, and it charges each of them asregression-flagged. - When a URL with a 2xx baseline answers 4xx (for example a bot check), the
Actor does not compare the headers. The URL error record has the reason code
STATUS_CHANGED, and the baseline stays. When the URL answers 2xx after a baseline with another status, the Actor writes a new baseline. - A 5xx or 429 answer is retried one time. After the retry it is the reason
code
SERVER_ERROR, and the Actor does not compare it.
The gate
gate_pass is false when a regression, or a policy violation, has a severity
at or above fail_on. The default is high. A policy violation has the
severity high.
A failed gate is a result, not a fault. The Actor writes it to the dataset
and to the status message, and the run succeeds. The same applies to an
unreachable URL and to an unusable input entry. Your CI job reads gate_pass
from the summary record.
Input
| Field | Type | Default | Meaning |
|---|---|---|---|
urls | array of 1 to 200 URLs | three public demo URLs | The URLs to check. The Actor does not crawl. |
required_headers | object | {} | Optional policy. See below. |
fail_on | critical, high, medium or low | high | The lowest severity that sets gate_pass to false. |
baseline_name | string | empty | The record key of the baseline. Empty: one record for each URL set. |
reset_baseline | boolean | false | Write the current headers as the new baseline. |
alert_webhook_url | HTTPS URL | empty | One JSON POST when the run finds a regression. |
request_timeout_seconds | integer 2 to 60 | 15 | Timeout for each request and the webhook POST. |
concurrency | integer 1 to 20 | 10 | URLs requested at the same time. |
max_redirects | integer 0 to 10 | 5 | Redirect hops for each request. |
max_run_seconds | integer 30 to 3600 | 240 | Deadline for the URL list and the confirmation requests. |
Every field has a schema default. A run with the empty input {} checks the
three demo URLs and succeeds.
The required header policy
Each key is one of the ten headers. Each value is true (the header must be
present) or an object of rules:
{"strict-transport-security": {"min_max_age": 31536000, "include_subdomains": true},"content-security-policy": {"required_directives": ["frame-ancestors", "object-src"],"forbidden_sources": {"script-src": ["'unsafe-inline'", "'unsafe-eval'"]}},"x-frame-options": {"allowed_values": ["DENY", "SAMEORIGIN"]},"x-content-type-options": true}
allowed_values(all headers): the normalized value must be one of these.min_max_age,include_subdomains,preload(HSTS only).required_directives,forbidden_sources(CSP and CSP-Report-Only). A missing fetch directive falls back todefault-src. A source is forbidden only when every policy allows it.'unsafe-inline'next to a nonce, a hash or'strict-dynamic'is not a violation, because browsers ignore it.
Safety limits
- Only
httpandhttpsURLs. No credentials in a URL. localhost, and each hostname that resolves to a private or reserved address, is refused. The Actor resolves and checks each redirect hop again before it follows the hop. A redirect chain that repeats a URL stops.- An unusable input entry becomes one URL error record with the reason code
INVALID_URLorBLOCKED_TARGET. It does not stop the run, and it is not charged. - One URL gets one request and one retry. One URL takes at most
2 x request_timeout_seconds + 5seconds. - The webhook gets no redirect and at most 100 regressions in its body.
Output
The record_type field separates the records.
Summary (summary): url_count, urls_checked, urls_unavailable,
changed_url_count, unchanged_url_count, baselines_created, change_count,
changes_by_class, regression_count, regressions_by_severity,
unconfirmed_regressions, policy_violation_count, fail_on, gate_failures,
gate_pass, alert_status, baseline_store, baseline_name,
baseline_reset and observed_at.
Change (change): url, final_url, final_status,
baseline_final_status, header, baseline_value, current_value,
change_class, severity, regression, fails_gate, confirmed, details
(each with directive, source, change_class, severity and
description), baseline_updated_at and observed_at.
Policy violation (policy_violation): url, final_url, final_status,
header, current_value, severity, description, fails_gate and
observed_at.
URL error (url_error): url, final_url, final_status,
baseline_final_status, reason_code, error, attempts and observed_at.
Webhook body: event (regression-flagged), gate_pass, fail_on,
url_count, urls_checked, changed_url_count, regression_count,
regressions_by_severity, policy_violation_count, baseline_name,
observed_at, regressions and regressions_truncated.
Reason codes
OK, STATUS_CHANGED, SERVER_ERROR, TIMEOUT, DNS_FAIL, CONNECT_FAIL,
BLOCKED_TARGET, REDIRECT_LOOP, RUN_DEADLINE, CHARGE_LIMIT and
INVALID_URL.
Pricing: pay per event
| Event | Unit | Counted as |
|---|---|---|
run-started | one Actor run | Charged one time, before the Actor requests a URL. |
url-checked | one URL requested and its security headers compared with the baseline in one run | Charged one time for each URL that the Actor compared, or for which it wrote the first baseline. A URL that it could not check (an error, STATUS_CHANGED, CHARGE_LIMIT) and an unusable input entry are not charged. The confirmation request is not charged. |
regression-flagged | one header on one URL that was removed or weakened since the baseline | Charged one time for each change record with the change class removed or weakened. A regression at or above fail_on stays in the baseline, so it is charged on each run until it is fixed or reset. A change record with confirmed: false (the confirmation request got no usable answer) is charged. A regression that the confirmation request did not repeat (counted in unconfirmed_regressions, no change record) and a policy violation are not charged. |
Before the Actor requests a URL, it limits the URL list to the number of
url-checked events that the maximum total charge of the run allows. The URLs
outside that limit get the reason code CHARGE_LIMIT, are not requested, and
keep their baseline.
Local development
uv syncuv run pytestuv run ruff check .uv run python scripts/stability_probe.py 3 # the stability probe, not part of the image