Gateway address deny-list
The gateway's live source-address deny-list: what it refuses and where, the operator command that changes it without a deploy, why it fails open (and what that costs), the two metrics it emits, and the coverage it does not claim — one address is not an account, and the LiteLLM upstream sits behind its own door.
What it is. A live list of source addresses the jls-gateway refuses outright, checked before
body parsing, authentication and every route — and on every WebSocket upgrade, which never enters the
Express pipeline. It exists because the project has no load balancer and no Cloud Armor, so an
app-level list is the only place a block can live.
Why it exists. An OVH-hosted server used a broker session key for five days and then fired 196 POSTs at the buy-credits endpoint. Authentication and the per-IP limiter both held, but there was no way to say "not you, ever again" to a source. Revoking the credential closed that incident; this closes the address as one more door.
Where the list lives. Firestore collection gateway_denied_ips in agentmc-shares. A document
id is the address, and it denies only when the document carries denied: true, so a half-written or
exploratory record cannot silently refuse a paying customer. The set is refreshed through the
gateway's existing TTL gate (30 seconds) and served from memory — a per-request Firestore read would
put a database round-trip on every call the company pays for.
Changing it. node scripts/ops/gateway-deny-list.mjs --add <ip> --reason "<why>", --list,
--remove <ip> --reason "<why>". No deploy; the change takes effect within the refresh window. The
script pins project and collection, refuses anything that is not an exact address, stores the
gateway's own canonical spelling, verifies a removal actually lifted the block, and appends every
write's reason, actor and time to the document's history.
Why it fails open. An unreadable list serves the caller, and the last successfully loaded set
stays in force. This is the deliberate opposite of the sign-in allowlist: refusing an unknown caller
protects the product, while refusing a legitimate paying customer costs one. The cost of that choice
is that a list that has stopped loading looks exactly like a list with nobody on it — which is why a
failed refresh emits its own metric and is alerted by
gateway-deny-list-refresh-failed-policy.json.
What it emits.
gateway_ip_denied— one per refusal, carrying a peppered pseudonym of the address, never the address itself. SetGATEWAY_IP_HASH_PEPPERwhen running--listto print the same pseudonym beside each entry, which is how you tell which one fired.gateway_ip_deny_list_refresh_failed— the list could not be refreshed; carries the error and how stale the served set is. Policy-backed.gateway_ip_deny_list_entry_unparseable— an entry whose id is not an address was skipped; carries a count, never the entry.
What it does not cover. Only jls-gateway. The LiteLLM upstream is public at its own ingress and
guarded by its own key. And an address is not an account: a rented host can rotate its IPv4 or move
inside its own IPv6 /64 at will, so this is a backstop — revoking the credential is what actually ends
an incident.
Last verified 2026-10-24