Admin API
Entities
Standard Otoroshi entity endpoints, authenticated with an admin apikey.
GET /apis/waf.extensions.cloud-apim.com/v1/waf-configs
POST /apis/waf.extensions.cloud-apim.com/v1/waf-configs
GET /apis/waf.extensions.cloud-apim.com/v1/waf-configs/{id}
PUT /apis/waf.extensions.cloud-apim.com/v1/waf-configs/{id}
PATCH /apis/waf.extensions.cloud-apim.com/v1/waf-configs/{id}
DELETE /apis/waf.extensions.cloud-apim.com/v1/waf-configs/{id}
The same shape for every other collection: waf-rulesets, threat-policies, threat-feeds,
asn-databases, geo-databases, crowdsec-bouncers, bot-policies, challenge-providers and
honeypot-policies.
curl -u "$APIKEY_ID:$APIKEY_SECRET" \
https://otoroshi-api.example.com/apis/waf.extensions.cloud-apim.com/v1/threat-feeds
WAF endpoints
Backoffice-authenticated — they use the admin session, not an apikey.
Compile a ruleset
POST /extensions/cloud-apim/extensions/waf/utils/_compile
{ "rules": ["@import_preset crs", "SecRuleEngine On"], "rulesets": [], "crs": {} }
{ "done": true, "missing_rulesets": [], "crs_ignored": false, "warning": null }
It checks what a config would run, composed the way the gateway composes it: the referenced
rulesets in order, the CRS options, then rules. Each element is checked on its own, as the engine
reads it, then the engine is built. A preset name nobody provides, a rule that does not parse or
compile, and a chain that ends with its element are refused, each located:
{
"done": false,
"error": "ruleset 'baseline', rule 3: does not parse: Parse error at line 1:46 - …",
"errors": [{ "index": 2, "origin": "ruleset 'baseline', rule 3", "message": "does not parse: …" }]
}
A missing ruleset still compiles, and is listed in missing_rulesets.
Evaluate a request against a ruleset
POST /extensions/cloud-apim/extensions/waf/utils/_test
{
"rules": ["@import_preset crs", "SecRuleEngine On"],
"request": {
"method": "POST",
"uri": "/post",
"headers": { "content-type": "application/x-www-form-urlencoded" },
"body": "q=1' OR '1'='1"
}
}
Returns the full engine result — matched rules, phases, disposition.
Tuning endpoints
Backoffice-authenticated. These back the tuning assistant.
Recent candidates
GET /extensions/cloud-apim/extensions/waf/tuning/_matches
{
"store": { "retained": 42, "max": 500 },
"scope": "this node, within the retained window",
"matches": [
{
"key": "waf-config_x|942100|ARGS:comment|/api/posts",
"count": 7,
"id": "…",
"rule_id": 942100,
"target": { "collection": "ARGS", "member": "comment", "full": "ARGS:comment", "excludable": true },
"method": "POST",
"path": "/api/posts",
"blocked": true
}
]
}
Only matches naming an input an exclusion can be written against are retained.
Propose exclusions
POST /extensions/cloud-apim/extensions/waf/tuning/_propose
{ "sample_id": "…" }
Or without a stored sample:
{ "config_ref": "waf-config_x", "rule_id": 942100, "target": "ARGS:comment", "path": "/api/posts" }
Returns the candidates narrowest first, each carrying its generated seclang, its reach, where it
gets written (placement), and a preview — the measured result of running it.
Preview an arbitrary exclusion
POST /extensions/cloud-apim/extensions/waf/tuning/_preview
{ "sample_id": "…", "seclang": "SecRuleUpdateTargetById 942100 \"!ARGS:comment\"", "kind": "update_target" }
{
"compiles": true,
"reproduced": true,
"effective": true,
"safe": false,
"regressions": [{ "name": "sqli-union", "category": "sqli", "caught_before": true, "caught_after": false }],
"collateral_rules": [942190]
}
reproduced and effective are separate claims: the first says the match was rebuilt and observed,
the second that the exclusion removed it. Without the first, the second means nothing.
Apply
POST /extensions/cloud-apim/extensions/waf/tuning/_apply
{ "sample_id": "…", "seclang": "…", "kind": "update_target", "reason": "…", "force": false }
| Response | When |
|---|---|
200 | Written. The body carries the ruleset it went to and the preview it was verified against |
400 | It does not compile, or it does not stop the rule firing |
409 + needs_force | The match could not be reproduced, or known attacks stop being caught |
Learning endpoints
Backoffice-authenticated. These back learning mode.
Start, stop, discard a window
POST /extensions/cloud-apim/extensions/waf/learning/_start
POST /extensions/cloud-apim/extensions/waf/learning/_stop
POST /extensions/cloud-apim/extensions/waf/learning/_discard
{ "config_ref": "waf-config_x" }
_start answers with the engine-mode advice, so a window is never begun on a configuration whose
mode cannot measure what the operator is about to ask it for.
Read the report
POST /extensions/cloud-apim/extensions/waf/learning/_report
{ "config_ref": "waf-config_x" }
{
"run": { "requests": 41203, "matched": 380, "would_block": 96, "would_block_rate": 0.0023, "duration_millis": 604800000 },
"nodes": 3,
"mode": { "arming_estimate_reliable": true, "rule_counts_complete": false, "note": "…" },
"entries": [ { "rule_id": 942100, "target": { "full": "ARGS:comment" }, "path": "/api/posts", "count": 212, "would_block": 61, "paranoia": 1 } ],
"exclusions": [ { "entry": {}, "proposal": {}, "preview": {}, "accepted": true, "note": "verified: …" } ],
"paranoia": { "current": 1, "recommended": null, "rationale": "…" },
"threshold": { "current": 5, "curve": [ { "threshold": 5, "still_denied": 96 } ], "recommended": 15, "rationale": "…" },
"impact": { "sampled": 200, "resolved": 187, "residual": 13, "rationale": "…" },
"verdict": "…"
}
entries is everything counted; exclusions is what the report proposes acting on, each carrying
the preview that earned or denied it a place.
Apply the verified exclusions
POST /extensions/cloud-apim/extensions/waf/learning/_apply
{ "config_ref": "waf-config_x", "keys": ["942100|ARGS:comment|/api/posts"], "reason": "…" }
keys selects a subset; omit it to apply every verified one. Anything the report did not verify is
never applied, whether or not it is named.
Reputation endpoints
All under /extensions/cloud-apim/extensions/waf/reputation, backoffice-authenticated.
| Method | Path | Body | Purpose |
|---|---|---|---|
GET | /_catalog | — | The curated source catalog |
GET | /_status | — | Every feed, bouncer, ASN and geolocation database with its runtime state, and the DNS blocklist resolver with the health of each zone |
POST | /_template | {"entry":"tor-exit-nodes"} | Build a ThreatFeed from a catalog entry |
POST | /_refresh | {"feed":"<id>"}, {}, {"asn":true} or {"geo":true} | Refresh one feed, all of them, the ASN table, or the geolocation databases |
POST | /_rollback | {"feed":"<id>"} | Restore the previous snapshot |
POST | /_lookup | {"ip":"1.2.3.4"} | Evaluate an address against every enabled source |
POST | /_geo | {"ips":["1.2.3.4", …]} | Location (geolocation databases), network and AS number (ASN databases) of up to 500 addresses, with the attributions the data requires — what Threat Studio shows next to a caller. located: false means the country is only where the network is registered |
POST | /_crowdsec_sync | {"bouncer":"<id>"} or {} | Sync one bouncer, or all of them |
/_status is the one worth wiring into monitoring:
{
"feeds": [
{
"id": "threat-feed_1f3a...",
"name": "FireHOL level 1",
"enabled": true,
"snapshot": { "entries": 1284, "ranges": 1190, "rejected": 0, "error": null },
"previous": { "entries": 1279 }
}
],
"crowdsec": [
{
"id": "crowdsec-bouncer_9c2b...",
"pending_push": 0,
"store": { "decisions": 1042, "initialized": true, "last_error": null }
}
]
}
A non-null snapshot.error, or a store.last_error, means protection is degraded on that node.
Security fabric endpoints
All under /extensions/cloud-apim/extensions/waf/security, backoffice-authenticated.
| Method | Path | Body | Purpose |
|---|---|---|---|
GET | /_status | — | Node id, ban and allowlist counts, shared-state mode and why it is or is not distributed, ledger settings |
GET | /_bans | — | Every live ban with its evidence |
GET | /_incidents | — | Incidents in the current window, merged across every node, with their state |
GET | /_allowlist | — | The identities the fabric will never ban |
POST | /_ban | {"ref":"ip:1.2.3.4","duration_seconds":3600,"reason":"..."} | Ban by hand |
POST | /_extend | {"ref":"ip:1.2.3.4","duration_seconds":86400} | Push a live ban's end further out |
POST | /_unban | {"ref":"ip:1.2.3.4"} or {"all":true} | Lift one or all |
POST | /_allow | {"ref":"apikey:partner-ci","reason":"...","unban":true,"duration_seconds":null} | Allowlist, and lift what is held against them |
POST | /_disallow | {"ref":"apikey:partner-ci"} | Remove an allowlist entry |
POST | /_incident_state | {"key":"ip:1.2.3.4","state":"acknowledged","note":"..."} | Move an incident. open clears the state |
POST | /_ledger | {}, {"ref":"..."}, or {"ref":"...","forget":true} | Inspect or clear the cross-request memory |
POST | /_simulate | {"score":75,"policy":"..."} | What a policy would do at that score |
GET | /_challenge_presets | — | Ready-to-edit vendor challenge settings |
POST | /_challenge_from_preset | {"preset":"friendly-captcha"} | Build a provider from a preset |
GET | /_bot_catalog | — | Every known bot signature |
POST | /_robots_txt | {"policy":"<id>"} | The robots.txt a bot policy enforces |
POST | /_alert_test | {"rule":{…}} or {"id":"<id>"} | Send a test message to an alert rule's channel |
POST | /_scanner_test | {"scanner":{…}} or {"id":"<id>"} | EICAR must come back infected, a plain file clean |
POST | /_contract_check | {"spec":"…","base_path":""} or {"id":"<id>"} | Compile an API contract: its version, base path, operations and warnings |
POST | /_contract_fetch | {"url":"https://…"} | Fetch a contract's text once, for it to be stored. Nothing is saved |
GET | /_api_report?route_ids=a,b&zombie_days=90 | — | The API reports: operations, shadow and zombie endpoints, drift, credentials |
The API report is also served by the admin API, for an admin api key, at
GET /api/extensions/cloud-apim/waf/api/_report with the same parameters: what a CI asks.
Two of these answer done: false for outcomes that are decisions rather than failures, and say
which: /_ban returns {"done":false,"refused":"allowlisted","allowlist":{…}} when the identity is
on the allowlist, and /_extend refuses when there is no live
ban left to extend. Neither is an error — but neither did what was asked, so a caller that treats a
200 as success will be wrong.
Every mutating call here is recorded as a
CloudApimWafSecurityAudit with the operator on it.
_status is the one to monitor — shared_state.dedicated_redis: false means bans are only shared
between nodes if the Otoroshi storage backend is.