Network & IP allowlist
Restrict Pact access to trusted networks with tenant-scoped IP allowlist and denylist policies, scoped to admin/API/all traffic and enforced on every request.
Network & IP allowlist
Enterprise IT teams often need to restrict a SaaS tool to corporate egress IPs. Pact supports tenant-scoped allowlist and denylist policies stored in tenant_ip_policies, enforced by a middleware (api/middleware/ip_policy.py) on every authenticated route — admin, API, SCIM, and SAML callbacks alike. The admin CRUD is api/routes/admin_ip_policy.py; owner/admin only.
Manage policies
GET /v1/admin/network/policies — list
POST /v1/admin/network/policies — create
GET /v1/admin/network/policies/{id} — read one
PATCH /v1/admin/network/policies/{id} — partial update
DELETE /v1/admin/network/policies/{id} — delete
A create payload:
POST /v1/admin/network/policies
{
"name": "Corp egress only",
"mode": "allowlist", // allowlist | denylist
"cidrs": ["203.0.113.0/24", "198.51.100.7/32"],
"enforced_for": ["all"], // all | admin | api
"enabled": true
}
Scopes
The enforced_for list decides which traffic a policy governs, matched to the request path:
["admin"]— fires only on/v1/admin/...["api"]— fires on/v1/admin/...and other/v1/...API routes["all"]— fires on everything (the default, most restrictive)
Paths that are never gated
Health probes (/health*) and password login (/v1/auth/login) are always exempt — blocking liveness checks would break Azure Front Door and Container Apps, and IP-gating the login itself is a denial-of-service shape. The policy protects authenticated traffic, not the login endpoint.
Dry-run before you enforce
Test a candidate IP against your live policies without dispatching a real request — the admin UI uses this to answer "would this IP be blocked?":
POST /v1/admin/network/policies/test
{ "ip": "203.0.113.9", "scope": "api" } →
{ "allow": true, "reason": "...",
"matched_policy_id": 3, "matched_policy_name": "Corp egress only" }
There is also a CIDR validation endpoint (POST /v1/admin/network/policies/validate-cidrs) so the UI can reject malformed input before save.
Guardrails against self-lockout
An allowlist with empty cidrs would refuse every request, so the create route rejects it with 400. A denylist with empty cidrs is allowed (it denies no one) as a legitimate intermediate state during edits. Every create, update, and delete emits auth.ip_policy.{created,updated,deleted} to the audit stream so SOC 2 reviewers see network-policy changes.