Skip to main content

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.

A policy with three tiers: log at 40, tarpit at 70, ban at 90

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.

ActionEffect
logRecord it. Nothing else
tarpitAnswer slowly, then serve normally. Costs the attacker a connection and you nothing
denyRefuse this request with status
banRefuse it and ban the caller for ban_for_seconds
challengeAsk the caller to prove something before proceeding. See challenges
throttleLet 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_weight defaults 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_weight defaults to 90, so a block reaches a default ban tier on its own.
Why a block is not simply "always blocked"

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 it
  • ip — always the address
  • apikey — 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_..."}'