Skip to main content
Browse docs

Tutorial: Setting Up CORS & IP Allowlists

This tutorial shows you how to configure Cross-Origin Resource Sharing (CORS) headers and IP allowlists in the Keeptrusts gateway to restrict access to authorized browser origins and network ranges in production.

Use this page when

  • You need to restrict which browser origins can call the gateway (CORS).
  • You are limiting gateway access to specific IP ranges or CIDR blocks.
  • You want to secure a production gateway against unauthorised network access.
  • You are debugging CORS preflight failures or IP deny responses.

Primary audience

  • Primary: Platform and security engineers hardening gateway network access
  • Secondary: Frontend developers troubleshooting CORS errors; compliance teams requiring network-level access controls

Prerequisites

  • kt CLI installed (first-run tutorial)
  • An OpenAI-compatible API key exported as OPENAI_API_KEY
  • curl and jq installed

Step 1: Create the CORS Configuration

Create policy-config.yaml with CORS settings:

version: '1'
providers:
targets:
- id: openai
provider: openai
secret_key_ref:
env: OPENAI_API_KEY
cors:
allowed_origins:
- https://app.example.com
- https://staging.example.com
- https://console.keeptrusts.com
allowed_methods:
- GET
- POST
- OPTIONS
allowed_headers:
- Content-Type
- Authorization
- X-Consumer-Group
- X-Request-Id
expose_headers:
- X-RateLimit-Limit-Requests
- X-RateLimit-Remaining-Requests
- X-Request-Id
max_age_seconds: 3600
allow_credentials: true
policies:
- name: content-filter
type: content_filter
action: flag

CORS configuration breakdown

FieldPurpose
allowed_originsOrigins the browser permits for cross-origin requests
allowed_methodsHTTP methods allowed in cross-origin requests
allowed_headersRequest headers the browser can send cross-origin
expose_headersResponse headers the browser can read cross-origin
max_age_secondsHow long the browser caches preflight responses
allow_credentialsWhether cookies and auth headers are sent cross-origin

Step 2: Add IP Allowlist Restrictions

Add an ip_allowlist section to restrict access by source IP:

# policy-config.yaml (continued)
ip_allowlist:
enabled: true
ranges:
- cidr: "10.0.0.0/8"
label: "internal-network"
- cidr: "172.16.0.0/12"
label: "docker-network"
- cidr: "203.0.113.0/24"
label: "office-network"
- cidr: "198.51.100.50/32"
label: "ci-runner"
deny_action: reject
deny_status_code: 403
log_denied: true

IP allowlist configuration breakdown

FieldPurpose
ranges[].cidrCIDR notation for allowed IP ranges
ranges[].labelHuman-readable label shown in logs and events
deny_actionWhat to do with non-allowed IPs — reject or log_only
deny_status_codeHTTP status code returned for rejected requests
log_deniedWhether denied requests generate decision events

Step 3: Validate and Start the Gateway

kt policy lint --file policy-config.yaml
kt gateway run --policy-config policy-config.yaml --port 41002

Expected output:

INFO keeptrusts::gateway CORS: 3 allowed origin(s), credentials=true, max_age=3600s
INFO keeptrusts::gateway IP allowlist: 4 range(s), deny_action=reject
INFO keeptrusts::gateway Gateway ready

Step 4: Test CORS Preflight from an Allowed Origin

Simulate a browser preflight request from an allowed origin:

curl -s -D- -X OPTIONS http://localhost:41002/v1/chat/completions \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
2>&1 | grep -E "^(HTTP|Access-Control)"

Expected headers:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600

Step 5: Test CORS from a Blocked Origin

Simulate a request from an unauthorized origin:

curl -s -D- -X OPTIONS http://localhost:41002/v1/chat/completions \
-H "Origin: https://malicious-site.example.com" \
-H "Access-Control-Request-Method: POST" \
2>&1 | grep -E "^(HTTP|Access-Control)"

Expected output — no CORS headers returned:

HTTP/1.1 403 Forbidden

Step 6: Test an Actual Request with CORS Headers

Send a POST request including the Origin header:

curl -s -D- http://localhost:41002/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Origin: https://app.example.com" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello"}]
}' 2>&1 | grep -E "^(HTTP|Access-Control)"

Expected:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-RateLimit-Limit-Requests, X-RateLimit-Remaining-Requests, X-Request-Id

Step 7: Test IP Allowlist Enforcement

From a machine outside the allowed ranges, a request will be rejected:

# This will succeed from a machine within the allowed CIDR ranges
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:41002/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}'

From an IP outside the allowed ranges:

{
"error": {
"message": "Request denied by IP allowlist",
"type": "access_denied",
"code": "ip_not_allowed",
"details": {
"source_ip": "192.0.2.100",
"deny_action": "reject"
}
}
}

Step 8: Use Log-Only Mode for Rollout

Before enforcing the allowlist, run in log_only mode to audit traffic:

ip_allowlist:
enabled: true
ranges:
- cidr: "10.0.0.0/8"
label: "internal-network"
deny_action: log_only
log_denied: true

In this mode, requests from non-allowed IPs are permitted but logged as events:

kt events tail --last 5 --format json \
| jq '[.[] | select(.details.ip_allowlist_denied == true) | {source_ip: .details.source_ip, label: .details.matched_label}]'

Review the output to confirm your allowlist covers all legitimate sources before switching to reject mode.

Step 9: Verify Response Headers

Confirm that exposed rate-limit headers are visible to the browser:

curl -s -D- http://localhost:41002/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Origin: https://app.example.com" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}' \
2>&1 | grep -iE "^(X-RateLimit|X-Request-Id)"
X-RateLimit-Limit-Requests: 60
X-RateLimit-Remaining-Requests: 59
X-Request-Id: req_abc123def456

These headers are readable by browser JavaScript because they are listed in expose_headers.

For AI systems

  • Canonical terms: Keeptrusts gateway, CORS, IP allowlist, CIDR, preflight, allowed origins, deny action.
  • Config fields: cors.allowed_origins, cors.allowed_methods, cors.allowed_headers, cors.max_age_seconds, cors.allow_credentials, ip_allowlist.enabled, ip_allowlist.ranges[].cidr, ip_allowlist.deny_action.
  • CLI commands: kt gateway run, kt policy lint.
  • Best next pages: Rate Limiting, Consumer Group Isolation, Gateway Docker Compose.

For engineers

  • Prerequisites: kt CLI, OPENAI_API_KEY exported, curl and jq.
  • Test CORS: curl -I -X OPTIONS -H "Origin: https://app.example.com" http://localhost:41002/v1/chat/completions should return Access-Control-Allow-Origin.
  • Test IP deny: request from an IP outside the allowlist returns HTTP 403.
  • Use ip_allowlist.ranges[].label to identify which CIDR matched in event logs.
  • Combine CORS + IP allowlist for defence in depth on production gateways.

For leaders

  • CORS prevents unauthorised web applications from calling the gateway — critical for browser-exposed gateways.
  • IP allowlists restrict access to known corporate networks, VPNs, or CI runners.
  • Both controls operate before policy evaluation — rejected requests incur zero LLM cost.
  • Review allowlists quarterly as team networks and office IPs change.

Next steps