Skip to main content

Per-API-key IP allowlist

Bind an Orbit API key to one or more source IPs (or CIDR ranges) so the key only authenticates when the request originates from your infrastructure. A leaked key is then useless to anyone outside your network — the request fails with 401 INVALID_API_KEY before it touches your billing or tenant data. This is a per-key control. The org-wide IP allowlist (under Settings → Security) gates the dashboard and SAML login surfaces; the per-key list gates programmatic API authentication.

Pre-deployment checklist

Run through this list before the first PATCH, not after:
  1. Inventory your keys. List every active key (GET /api/v1/settings/api-keys) and record each key’s id, name, owner, and the services that use it. Adoption is per-key, so a skipped key stays unrestricted by default.
  2. Clone the source-IP inventory per key. For each key, collect the egress IPs of the systems that send with it — application servers, CI runners, scheduler hosts, and any outbound proxy that terminates TLS for you.
  3. Include the operator’s own IP. The person who will later test or debug with the key must allowlist the IP they connect from, or they lock themselves out of their first curl. There is no add-time safeguard on the per-key list (unlike the org-wide one, which refuses self-locking changes) — the guard here is your checklist.
  4. Tag owners for renamed or reassigned keys. Keys that changed hands (team renames, departed owners) need a named current owner before rollout; that owner confirms the source IPs are still accurate.
  5. Decide the rollout order. Start with one low-traffic key, verify the 401 path (below) from a disallowed source, then fan out.

Roll an allowlist out org-wide safely

A safe rollout is an announcement plus an audit trail, not a bulk edit:
  1. Announce to key owners first. Tell every key owner the date their keys become IP-restricted and where to send their source IPs. Owners who discover the change through a 401 treat it as an outage.
  2. Verify per owner before you PATCH. For each key, fetch the current list (GET .../allowed-ips), confirm the entries against what the owner reported, then PATCH the replacement.
  3. Audit after each batch. Every replacement writes an audit-log entry under api_key.allowed_ips_updated (see Audit log). Filter Settings → Audit Log by that action after each batch and reconcile the entries against your rollout sheet — the count of audit rows should equal the count of keys you intended to change.
  4. Watch for quiet stragglers. A service that was missed in inventory surfaces as 401 INVALID_API_KEY within minutes of its key being restricted — the rejection is generic, so the symptom is “valid key rejected.” Keep the inventory open for a few days after rollout and treat new rejections as inventory misses, not attacks.

Per-key vs org-wide: two different gates

The two allowlists are independent controls that never combine: Setting an org-wide entry does not restrict your API keys; setting a per-key list does not affect dashboard access. Roll them out separately and document them separately.

Edge behaviour you should know before scripting against this

These behaviours ship in the product but surprise operators on first contact:
  • IPv6 is first-class. Bare IPv6 addresses and IPv6 CIDRs are accepted: 2001:db8::41, 2001:db8::/32. A bare IPv6 address matches as a /128 host route, exactly like a bare IPv4 address matches as /32.
  • Mixed v4/v6 lists are normal. A single list can hold both families — matching is per-entry.
  • Duplicates are detected and dropped, not rejected. Duplicate entries (including a bare IP plus its own host route, e.g. 5.9.114.8 and 5.9.114.8/32) collapse to one stored entry — no error, no warning. If your rollout sheet produces duplicates, the PATCH succeeds; the response’s allowed_ips tells you what survived.
  • Blank entries are dropped, not rejected. Whitespace-only or empty strings inside the array are skipped by the write-time cleanup; the remaining valid entries are stored. Do not build validation on the assumption that a sloppy array fails — it does not.
  • Anything that is not a valid IP or CIDR fails the whole write. One malformed entry (203.0.113.999, 10.0.0.0/33, ::1/129, 203.0.113.5,203.0.113.6 as one string) rejects the entire request with 422 VALIDATION_ERROR and stores nothing.
  • null and [] are both “clear.” Either clears the list and makes the key unrestricted. This interacts with scheduled remediation scripts in one dangerous way: a remediation job that PATCHes a computed list which came back empty (an upstream source-IP lookup that returned nothing, a template that rendered zero entries) does not fail — it clears the allowlist, flipping the key to unrestricted exactly when you thought you were tightening it. Guard remediation scripts: refuse to PATCH when the computed list is empty, or send an explicit null and log that you meant to clear.
  • The cap counts final entries. The limit is 50 entries per list. Because duplicates and blanks are dropped, an input array of 50+ entries can succeed if it collapses to ≤50 after cleanup — but a 51-entry distinct list fails the whole write with 422.

Cloudflare-fronted clients see the client IP

The gate matches the true client IP resolved through the proxy chain, not the public edge address a “what’s my IP” site reports. If the system that calls Orbit sits behind an outbound proxy, a CDN (Cloudflare-fronted origins are the common case), or a NAT gateway, allowlist the egress IP your traffic exits from — the value the sender-side X-Forwarded-For chain resolves the client to. Allowlisting your recursive resolver’s or your CDN edge’s public address gives you a false positive at allow time and a 401 at request time. When a key owner reports an IP, ask “what does your egress report to the destination,” not “what is your machine’s IP.” For dashboard-side debugging, Settings → Security (GET /api/v1/settings/ip-allowlist/current-ip) reports the value the org-wide guard resolves you to — the same resolution logic applies here.

Validation and the 422 recovery workflow

When a PATCH returns 422 VALIDATION_ERROR, the error body identifies which entry broke the write:
Fix-and-retry workflow:
  1. Read the entry named in the message — the whole list is atomic, so one bad entry blocks all entries.
  2. Correct the entry (203.0.113.999 → a valid IP; a prefix over /32 or /128 → within range; a comma-joined list → separate array elements).
  3. Re-send the full replacement list. PATCH is replace-only, so send the entire corrected list, not a diff.

Read the current allowlist

Owner / admin / developer scope.
Response:
allowed_ips is null when the key is unrestricted.

Replace the allowlist

PATCH is replace-only — it never merges. To add an entry, fetch the list, append, and PATCH the full list back.

Clear the allowlist (make unrestricted)

What a blocked request looks like

When a key has allowed_ips set and a request arrives from a non-allowed source IP:
The error returns generic INVALID_API_KEY rather than IP_NOT_ALLOWED so a leaked key can’t enumerate the allowlist by trial-and-error. For the same reason the rejection is deliberately silent — blocked requests are not recorded under a dedicated audit-log action, so don’t build forensics on an api_key.allowed_ips_violation event (none is emitted). Use your edge/gateway access logs to investigate denied source IPs.

Audit log

Every replacement of the allowlist writes an audit-log entry under api_key.allowed_ips_updated with:
  • actor_user_id
  • resource_id — the API key id
  • details.count — the number of entries in the new list
  • ip_address — the operator’s source IP
Combine with the dashboard at Settings → Audit Log to track who changed which key when. Across teams, this action is the shared contract: security teams filter on api_key.allowed_ips_updated to prove a rollout completed, and platform teams reconcile the same rows against their rollout sheet. Because blocked requests are silent (above), the audit log is the only server-side trail this control leaves — treat its review as part of the control, not an optional extra.

Constraints

  • Up to 50 entries per key.
  • Each entry must be a valid IP (v4 or v6) or CIDR range. The auth package validates each entry before storage.
  • An empty list ([]) or null clears the allowlist — the key becomes unrestricted again.
  • Changes take effect on the next request — there is no propagation lag.

See also