# How OmniRoute Enforces IP-Based Security Policies with Allowlist and Denylist Filtering

> Learn how OmniRoute enforces IP-based security policies using allowlist and denylist filtering. Protect your APIs with granular IP control. Reject unauthorized access effectively.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-08-03

---

**OmniRoute enforces IP-based security policies through a layered middleware that checks client IP addresses against global environment-based lists and per-API-key database configurations, rejecting requests with 403 Forbidden when policies are violated.**

The **OmniRoute** routing service implements a comprehensive IP filter system that operates early in the request-handling pipeline. This mechanism protects HTTP endpoints by validating client IP addresses against configurable allowlists and denylists, applying a fail-closed security model that denies access by default unless explicitly permitted.

## How the IP Filter Middleware Works

The IP filter implementation in [`src/middleware/ipFilter.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/middleware/ipFilter.ts) processes every incoming request through a standardized validation flow. Understanding this flow helps operators configure policies correctly and debug access issues.

### IP Address Extraction and Normalization

The middleware first extracts the client IP from `req.headers["x-forwarded-for"]`, taking the first entry in the chain, and falls back to `req.socket.remoteAddress` when the header is absent.

**IPv6 normalization** occurs in [`src/lib/ipUtils.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/ipUtils.ts), which removes prefixes like `::ffff:` to ensure consistent string matching regardless of address format.

```typescript
// Simplified extraction pattern from src/lib/ipUtils.ts
function normalizeIP(ip: string): string {
  // Strip IPv4-mapped IPv6 prefix
  if (ip.startsWith('::ffff:')) {
    return ip.substring(7);
  }
  return ip;
}

```

### Two-Level Policy Architecture

OmniRoute applies IP policies at two distinct levels, with per-API-key settings overriding global defaults:

| Level | Configuration Source | Storage Format |
|-------|---------------------|----------------|
| **Global** | Environment variables `IP_ALLOWLIST`, `IP_DENYLIST` | Comma-separated string → `Set<string>` |
| **Per-API-Key** | `api_keys` table columns `ip_allowlist`, `ip_denylist` | JSON array → `Set<string>` |

The global lists are parsed once at startup by [`src/lib/ipUtils.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/ipUtils.ts), while per-key lists are fetched dynamically from [`src/lib/db/apiKeys.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/apiKeys.ts) after API key resolution.

## Allowlist and Denylist Evaluation Logic

The **IP filter enforces security policies** through a strict priority-based decision engine with three core rules:

1. **Allowlist wins**: If any allowlist (global or per-key) exists, the request IP must be present in it
2. **Denylist overrides**: If the IP appears in any denylist, the request is rejected regardless of allowlist membership
3. **Fail-closed default**: With no explicit allowlist, all IPs are rejected

This design ensures that accidental misconfiguration results in blocked access rather than exposed endpoints.

### Environment Variable Configuration

Set global policies before server startup:

```bash

# Allow only specific IPs

export IP_ALLOWLIST="203.0.113.10,198.51.100.22"

# Block specific IPs even if in allowlist

export IP_DENYLIST="198.51.100.22"

```

### Per-API-Key Policy Assignment

Configure granular access through database operations:

```sql
-- Create API key with restricted IP access
INSERT INTO api_keys (key_id, secret, ip_allowlist)
VALUES ('internal-service', 's3cr3t', '["10.0.0.0/8","172.16.0.0/12"]');

-- Key with both allow and deny restrictions
INSERT INTO api_keys (key_id, secret, ip_allowlist, ip_denylist)
VALUES (
  'partner-api',
  'p4rtn3r',
  '["203.0.113.0/24"]',
  '["203.0.113.15","203.0.113.16"]'
);

```

## Integration with the Authorization Pipeline

The IP filter runs **before** the main authentication and authorization pipeline defined in [`src/server/authz/pipeline.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/server/authz/pipeline.ts). This ordering prevents credential validation overhead for requests that would be rejected on network grounds alone.

```typescript
// Typical middleware stack configuration
import { ipFilter } from '@omniroute/middleware/ipFilter';
import { authPipeline } from '@omniroute/server/authz/pipeline';

app.use(ipFilter);      // ← Network-level security first
app.use(authPipeline);  // ← Credential validation second

```

When a request fails IP validation, the middleware immediately returns:

```http
HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error": "IP address not permitted"}

```

## Runtime Policy Modification

For dynamic scenarios, [`src/lib/ipUtils.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/ipUtils.ts) exports functions to update global policies without restart:

```typescript
import { setGlobalIpAllowlist, setGlobalIpDenylist } from '@omniroute/lib/ipUtils';

// Update allowlist programmatically
setGlobalIpAllowlist(['10.0.0.1', '10.0.0.2']);

// Add emergency block
setGlobalIpDenylist(['192.0.2.100']);

```

These functions modify the in-memory `Set<string>` structures used by active middleware instances.

## Parallel CORS Implementation

The IP filter pattern mirrors [`src/middleware/cors.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/middleware/cors.ts), which implements origin-based allowlist/denylist filtering. Both middlewares share:

- Normalized header extraction
- Set-based membership testing
- Identical precedence rules (allowlist required, denylist overrides)
- Fail-closed defaults

This consistency across network-level (`ipFilter`) and application-level (`cors`) security controls reduces operational complexity.

## Summary

- OmniRoute's **IP filter middleware** in [`src/middleware/ipFilter.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/middleware/ipFilter.ts) validates every request against configurable allowlists and denylists
- **Global policies** are defined via `IP_ALLOWLIST` and `IP_DENYLIST` environment variables, parsed at startup in [`src/lib/ipUtils.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/ipUtils.ts)
- **Per-API-key policies** stored in the `api_keys` table override global settings for granular access control
- The **denylist takes precedence** over the allowlist, and the **fail-closed design** rejects requests by default
- Failed validation returns **403 Forbidden** immediately, before authentication processing

## Frequently Asked Questions

### How does OmniRoute handle IP addresses behind load balancers?

OmniRoute prioritizes the `X-Forwarded-For` header over `req.socket.remoteAddress`, extracting the first (leftmost) entry in the proxy chain. Operators should ensure this header is sanitized by their edge infrastructure to prevent spoofing. The [`src/lib/ipUtils.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/ipUtils.ts) module handles IPv6 normalization and format standardization.

### Can I use CIDR notation in allowlists and denylists?

The base implementation performs exact string matching. For CIDR support, extend [`src/lib/ipUtils.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/ipUtils.ts) with a range-checking utility before the middleware invokes `Set.prototype.has()`. The database schema stores lists as JSON arrays, allowing arbitrary string formats.

### What happens if both global and per-key allowlists exist?

Per-key configurations **override** global settings entirely. When a valid API key is resolved, the middleware discards global lists and evaluates only the key-specific `ip_allowlist` and `ip_denylist`. To combine policies, replicate global entries in the per-key configuration.

### Why does my request fail with 403 even when the IP is in the allowlist?

Check for **denylist precedence**: an IP present in both lists is rejected. Also verify that `X-Forwarded-For` extraction isn't capturing an intermediary proxy address rather than the original client. Enable debug logging in [`src/middleware/ipFilter.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/middleware/ipFilter.ts) to trace the normalized IP value being evaluated.