Skip to content

IP Allowlist Administration

Arbitex supports per-organization IP allowlisting. When an allowlist is configured, only requests originating from permitted IP addresses reach the API — all other source addresses receive HTTP 403. The allowlist is opt-in: an organization with no configured entries permits all source IPs.

IP allowlist enforcement runs at the ASGI middleware layer, before authentication and rate limiting. Changes take effect immediately — CRUD operations explicitly invalidate the in-process cache.


Per-org allowlist entries (stored in the org_ip_allowlists table) support three rule formats:

Standard classless inter-domain routing notation. Supports IPv4 and IPv6. The 0.0.0.0/0 and ::/0 wildcards are explicitly rejected — leave the allowlist empty to allow all IPs.

CIDRMatches
203.0.113.42/32Single IPv4 host
203.0.113.0/24256-address /24 subnet
10.0.0.0/8RFC 1918 class A private range
172.16.0.0/12RFC 1918 class B private range
192.168.0.0/16RFC 1918 class C private range
2001:db8::1/128Single IPv6 host
2001:db8::/32IPv6 /32 block

CIDR strings are parsed with strict=False — host bits do not need to be zero. 203.0.113.42/24 is accepted and matched as 203.0.113.0/24.

An explicit address range within a single subnet. IPv4 only. Two forms are supported:

Short form — last-octet range notation:

203.0.113.10-20

Matches 203.0.113.10 through 203.0.113.20 inclusive. The right side is a last-octet integer (0–255). The base IP must have four octets.

Full form — both endpoints as complete IPv4 addresses:

203.0.113.10-203.0.113.20

Semantically identical to short form. The right side must contain at least one dot (to distinguish from short form). Start address must be ≤ end address.

An exact IPv4 or IPv6 address:

203.0.113.42
2001:db8::1

Equivalent to /32 (IPv4) or /128 (IPv6) CIDR, stored explicitly as rule type single.


The IPAllowlistMiddleware evaluates each incoming HTTP request in order:

  1. Emergency bypass — if _SUPPORT_IP_ALLOWLIST_BYPASS=true is set (support use only), all IP checks are skipped globally and a CRITICAL log is emitted on every request.
  2. Internal endpoint bypass — requests to /v1/internal/ (already protected by mTLS) bypass IP allowlist checks entirely.
  3. Exempt paths/health, /ready, /api/health, /api/auth/login, and /api/auth/register are always exempt.
  4. Platform bypass CIDRs — IPs matching IP_ALLOWLIST_BYPASS_CIDRS are always permitted, regardless of any org allowlist.
  5. Per-org allowlist check — for authenticated requests carrying org context (org_id or tenant_id in request state), the client IP is checked against the org’s entries via IPAllowlistCache.
  6. Legacy global path — for requests without org context, checked against the ip_allowlist_entries table (60s TTL cache).
  7. Empty allowlist = allow all — if an org has no enabled entries, all IPs are permitted.

When a request is blocked:

{
"error": "ip_not_allowed",
"detail": "Client IP not in organization allowlist"
}

An ip_allowlist_blocked warning log is emitted with client_ip, org_id, path, and timestamp.


All endpoints require Org Admin role. Authenticate with a Bearer token (arb_live_* API key or session JWT).

Base path: /api/admin/ip-allowlist/

GET /api/admin/ip-allowlist/
Authorization: Bearer arb_live_your-api-key-here

Returns all IP allowlist entries ordered by created_at descending (newest first).

Response:

[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"ip_range": "203.0.113.0/24",
"description": "Corporate HQ egress",
"tenant_id": "8d4b2c1e-0a3f-4e9d-b7c6-1a2b3c4d5e6f",
"is_active": true,
"created_by_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"created_at": "2026-03-01T12:00:00Z"
}
]
GET /api/admin/ip-allowlist/{entry_id}
Authorization: Bearer arb_live_your-api-key-here

Returns a single entry by UUID. Returns 404 if not found.

POST /api/admin/ip-allowlist/
Authorization: Bearer arb_live_your-api-key-here
Content-Type: application/json
{
"ip_range": "203.0.113.0/24",
"description": "Corporate HQ egress",
"tenant_id": "8d4b2c1e-0a3f-4e9d-b7c6-1a2b3c4d5e6f",
"is_active": true
}
FieldRequiredDescription
ip_rangeYesRule value. Accepts CIDR (203.0.113.0/24), range (203.0.113.10-20), or single IP (203.0.113.42).
descriptionNoHuman-readable label (e.g., "Corporate HQ egress").
tenant_idNoOrg UUID to scope this entry. If omitted, falls into the legacy global allowlist.
is_activeNoWhether this entry is enforced. Defaults to true.

Returns 201 with the created entry. Cache is cleared immediately.

PUT /api/admin/ip-allowlist/{entry_id}
Authorization: Bearer arb_live_your-api-key-here
Content-Type: application/json
{
"description": "Updated label",
"is_active": false
}

All fields are optional — only provided fields are updated. Returns 200. Cache is cleared immediately.

DELETE /api/admin/ip-allowlist/{entry_id}
Authorization: Bearer arb_live_your-api-key-here

Returns 204 No Content. Cache is cleared immediately.


Per-org entries are stored in the org_ip_allowlists table (OrgIPAllowlistEntry):

ColumnTypeDescription
idUUIDEntry identifier
org_idUUIDOwning organization UUID
rule_typestringcidr, range, or single
valuestringRule value string
descriptionstringHuman-readable label
created_byUUIDAdmin user who created the entry
is_enabledboolWhether this entry is enforced
created_attimestampCreation time
updated_attimestampLast modification time

This table is separate from the legacy ip_allowlist_entries table (IPAllowlistEntry), which uses only CIDR-format ip_range strings with a tenant_id column. The legacy table is checked only when no org context is present on a request (the global fallback path).


The middleware uses an in-process per-org TTL cache (IPAllowlistCache) to avoid per-request database queries.

TTL: 60 seconds.

Invalidation: clear_cache() is called after every CRUD write so changes take effect immediately on the next request — the 60-second TTL only applies when no write has occurred.

Cache miss: Rules are loaded from org_ip_allowlists and parsed into ParsedRule objects (network, range, or address). Parsing happens once per cache population.

Load failure: If the database query fails during cache population, an empty list is returned and a warning is logged. Empty list = allow all — the system fails open. Apply monitoring to ip_allowlist_cache: DB query failed log events.

The legacy global path uses a separate 60-second TTL cache keyed to the ip_allowlist_entries table. This cache is only invalidated by CRUD operations on the global entries (not per-org operations).


Set this environment variable to a comma-separated list of CIDR ranges that are always permitted, regardless of any org allowlist. Parsed once at module load time.

IP_ALLOWLIST_BYPASS_CIDRS=10.0.0.0/8,172.16.0.0/12

Use for: platform support access ranges, monitoring infrastructure, internal load balancers.

Invalid CIDR strings emit a warning log but do not prevent startup.

Requests to paths beginning with /v1/internal/ bypass IP allowlist checks. These endpoints are protected by mutual TLS — the Outpost client certificate is the access control mechanism.

Always exempt to ensure health probes and authentication continue functioning:

  • /health
  • /ready
  • /api/health
  • /api/auth/login
  • /api/auth/register

_SUPPORT_IP_ALLOWLIST_BYPASS (emergency only)

Section titled “_SUPPORT_IP_ALLOWLIST_BYPASS (emergency only)”

Setting _SUPPORT_IP_ALLOWLIST_BYPASS=true disables all IP allowlist enforcement globally. A CRITICAL log entry is emitted on every request while active. For emergency lockout recovery only — remove immediately after restoring access.


IP allowlist enforcement runs before API key validation. A blocked IP receives 403 before the key is checked. A valid API key cannot bypass an IP allowlist restriction.

IP allowlist enforcement runs before rate limit middleware. Blocked IPs do not consume rate limit quota.

Requests to /v1/internal/ paths (Outpost heartbeat, policy sync) bypass the IP allowlist. These endpoints require mutual TLS.

SCIM endpoints (/scim/v2/) are subject to IP allowlist enforcement. If you use SCIM provisioning from a cloud identity provider (Okta, Entra ID), add the IdP’s outbound IP ranges before activating IP restrictions. Both providers publish their outbound IP ranges in their documentation.


  1. Identify all source IPs or CIDR ranges that should have access: corporate egress IPs, VPN exit nodes, CI/CD runner IPs, IdP SCIM outbound ranges.
  2. Create allowlist entries for each range via POST /api/admin/ip-allowlist/.
  3. Verify the entries with GET /api/admin/ip-allowlist/.
  4. Test from a permitted IP before any users are affected.

  1. Platform bypass CIDRs — if IP_ALLOWLIST_BYPASS_CIDRS covers your current IP range, access the API from there to add your locked-out IP.
  2. Emergency bypass — ask your infrastructure team to temporarily set _SUPPORT_IP_ALLOWLIST_BYPASS=true. Remove it immediately after adding the correct entry.
  3. Direct database access — insert a row into org_ip_allowlists (rule_type='cidr', value='<your-cidr>', is_enabled=true, correct org_id). The cache expires within 60 seconds; write operations also call clear_cache() directly if available.
  1. NAT or proxy translation — verify the actual egress IP using an IP echo service. Corporate networks may use multiple egress IPs or rotate them.
  2. Range too narrow — a single-IP entry may not cover all exit addresses. Widen to a /24 or use range notation.
  3. Entry disabled — check is_active or is_enabled on the relevant entries.
  4. Wrong org_id — entries scoped to a different org do not apply to your requests. Verify the org_id or tenant_id on the entry.
  5. Range format error — if rule_type=range, confirm 203.0.113.10-20 has a valid last-octet integer, or use the full form 203.0.113.10-203.0.113.20.
ErrorCause
Invalid CIDR valueCIDR string is malformed
0.0.0.0/0 (allow all) is not permittedWildcard CIDR rejected — leave allowlist empty to allow all
Invalid range valueRange format is missing -, has non-numeric end octet, or start > end
Invalid IP addressSingle-IP value is not a valid IPv4 or IPv6 address
422 Unprocessable EntitySchema validation failed — check the detail array