Threat policies
A ThreatPolicy says what happens at what score. Threat Protection → Threat policies.
Dry run is the default
A newly created policy has dry_run: true. It computes the score, picks the tier, records the
decision it would have taken, and enforces nothing.
That is deliberate. Arming a scoring system before measuring it is the fastest way to break production, so the entity refuses to do it unless you say so explicitly.
The sequence is the same as tuning the WAF: run in dry run, read the events, check that what would have been denied deserves it, then turn it off.

The graded response, as one screen. Dry run on means every tier is recorded and none of it is
enforced — which is where a policy should start.
Tiers
{
"dry_run": false,
"tiers": [
{ "min_score": 40, "action": "log" },
{ "min_score": 70, "action": "tarpit", "tarpit_millis": 3000 },
{ "min_score": 90, "action": "ban", "ban_for_seconds": 3600, "status": 403 }
],
"exemptions": ["10.0.0.0/8"],
"ban_identity": "auto",
"waf_match_weight": 45,
"waf_block_weight": 90,
"waf_block_decisive": false,
"slow_refusal_millis": 0
}
The highest matching tier wins, so they can be listed in any order. A score below every tier is still recorded — that record is what tuning reads.
| Action | Effect |
|---|---|
log | Record it. Nothing else |
tarpit | Answer slowly, then serve normally. Costs the attacker a connection and you nothing |
deny | Refuse this request with status |
ban | Refuse it and ban the caller for ban_for_seconds |
challenge | Ask the caller to prove something before proceeding. See challenges |
throttle | Let the caller make throttle_quota requests per throttle_window_seconds, and refuse the rest with 429 and Retry-After |
Throttling
A throttle tier is for a caller suspicious enough to slow down, not enough to refuse: it goes on at
throttle_quota requests every throttle_window_seconds (20 per 10 seconds by default), counted
across the cluster, and past that gets a 429 with a Retry-After saying when the window ends.
It is counted only while the caller's score holds it at the tier, so once whatever raised the
score stops, the throttle stops with it: nothing to lift.
{ "min_score": 40, "action": "throttle", "throttle_quota": 20, "throttle_window_seconds": 10 }
A throttled request is reported with decision.throttled and counts as enforced, and is not
charged to the ledger: the traffic guard scores a whole route's surge on every
caller, which a throttle tier answers well and a ban never should. If the shared store cannot be
reached, the request goes through.
Refusing slowly
A fast 403 tells the caller at once that the probe failed, and frees it to try the next one.
slow_refusal_millis holds every refusal of the policy that long before sending it: a deny or
ban tier, and a caller already banned that the threat gate stops. The status
line arrives only after the hold, so a scanner pays a connection and that much time for each
attempt. 0, the default, refuses at once.
A held request costs no thread, but it costs a connection on this node as well as on the
caller's. Each node holds at most security.tarpit.max-held-connections requests at once (1000 by
default), counting tarpit tiers too. Past that, a refusal is sent at once and a tarpit lets the
request through without waiting, so the defence never becomes the denial of service. The
security event says how long a request was really held, in decision.held_ms, and
/extensions/cloud-apim/extensions/waf/security/_status how many are held right now, under tarpit.
What a WAF verdict is worth
The score is additive and capped at 100. Most detectors publish their own weight, but the WAF is special: it publishes two verdicts, and the policy decides what each one is worth to the score.
- a match (
waf:match) — rules fired, but the engine stayed below its own anomaly threshold. The weaker, false-positive-prone verdict.waf_match_weightdefaults to 45: a lone match is recorded, not punished, and reaches a tier only when another detector corroborates it. - a block (
waf:blocked) — the engine reached a deny.waf_block_weightdefaults to 90, so a block reaches a default ban tier on its own.
Putting a WAF config in monitoring is a deliberate "observe, do not act on it alone" — its verdict
becomes evidence for the score rather than a decision. That is why a block feeds a weight instead of
refusing outright. When you do want a block to stop the request whatever else the score says, set
waf_block_decisive: true: a block is then taken at the ceiling and reaches the top tier
regardless of the arithmetic. It is off by default so it does not override the monitoring you asked
for. (A WAF config that is itself armed refuses at the WAF, before any of this — see
the WAF.)
The studio's Threat policy page reads this same arithmetic back and shows, before any traffic,
what a lone match and a lone block actually reach under the current weights and tiers — and warns
when a block cannot refuse a request at all (for instance a block weight below your ban tier, with
waf_block_decisive off).
Who gets banned
ban_identity decides which identity a ban attaches to:
auto(default) — the most specific one known: an apikey rather than the shared address behind itip— always the addressapikey— the apikey, falling back to the most specific available
This matters more than it looks. Banning an address because one apikey behind a corporate NAT misbehaved takes out everyone at that company.
Exemptions
{ "exemptions": ["10.0.0.0/8", "192.168.0.0/16", "203.0.113.9"] }
Exempt callers bypass the fabric entirely — the score is never computed for them, so no accumulation of weak signals can ever reach a tier. Put your own probes and monitoring here.
Simulating
The policy page has a What would happen? box: give it a score, and it tells you which tier matches, which action follows, and whether it would actually be enforced. No traffic involved.
The same thing over the API:
curl -X POST .../security/_simulate -d '{"score": 75, "policy": "threat-policy_..."}'