Skip to main content

Honeypots and canaries

A request for /.env is not ambiguous. No browser makes it, no API client of yours makes it, and there is no innocent reason to ask. That makes it the rarest thing in security: evidence rather than suspicion.

Threat Protection → Honeypots.

It runs before routing​

The plugin is an incoming request validator, not a route plugin, and that is not an implementation detail: /wp-login.php and /.git/config match no route, so a route-level plugin would never see the requests this exists to catch.

Like the other global validators it is configured in the global configuration JSON — see the note on global plugins:

{
"plugin": "cp:otoroshi_plugins.com.cloud.apim.otoroshi.extensions.waf.plugins.IncomingRequestValidatorCloudApimHoneypot",
"enabled": true,
"config": { "policy": "honeypot-policy_..." }
}

A new honeypot policy, pre-filled with the paths scanners ask for

The template arrives populated: /.env, /.git/config, /wp-login.php and friends. Nothing legitimate asks for these, which is what makes the signal so clean.

Paths​

The shipped list is deliberately short and unambiguous — /.env, /.git/config, /wp-login.php, /wp-admin*, /phpmyadmin*, /actuator/env and a handful more. A trailing * is a prefix match; everything else is exact.

Resist lengthening it. The property that makes honeypots worth having is a false-positive rate of essentially zero, and a broad list trades that away for coverage the WAF already provides.

FieldDefaultNote
weight100Near-certain evidence deserves the top of the scale
actiondenyban is opt-in — banning on a first hit is a decision, not a default
status404See below
Answer 404, not 403

A 403 tells the scanner that something is there and that you are protecting it. A 404 says nothing at all. The default is deliberate.

Canary tokens​

The same idea applied to values instead of paths. Plant a fake apikey in a public config, or a record id that exists in no database. Presenting it is proof: there is no way to hold that value except by having taken it.

{
"canaries": [
{ "value": "CANARY-a1b2c3", "description": "planted in the public sample config", "where": "any" },
{ "value": "ak_live_deadbeef", "description": "fake key in the docs repo", "where": "header" }
]
}

where narrows the search to header, query or path — or any, which checks the URI and the headers. An empty value never matches anything.

What it feeds​

A hit contributes a signal at the configured weight, records an incident, and charges the ledger — so a scanner sweeping your estate accumulates towards a ban even when each individual probe is merely denied.