---
title: "Egress Proxy"
description: "Controlled HTTP gateway for all outbound agent requests, with domain allowlisting, SSRF protection, credential injection, and rate limiting"
canonical: "https://docs.ainative.studio/docs/agent-cloud/egress-proxy"
last-updated: "2026-10-13T21:42:35.442Z"
---

# Egress Proxy

Source: https://docs.ainative.studio/docs/agent-cloud/egress-proxy

> Controlled HTTP gateway for all outbound agent requests, with domain allowlisting, SSRF protection, credential injection, and rate limiting

The Egress Proxy is a controlled HTTP gateway that handles all outbound requests made by AI agents. Instead of giving agents direct access to external APIs (and the credentials required to authenticate), agents submit their outbound requests to the proxy, which:

1. Validates the target domain against a per-agent allowlist
2. Blocks requests to private/internal IP addresses (SSRF protection)
3. Strips any credential-like headers the agent attempted to set
4. Injects the correct authentication header from the [Credential Vault](credential-vault)
5. Enforces per-agent per-domain rate limits
6. Executes the request and returns the response to the agent
7. Logs everything (with credentials redacted)

This architecture ensures that agents can call external APIs without ever holding, seeing, or being able to leak raw credentials.

Refs #2287, #2300, #2301

## Base URL

```
https://api.ainative.studio/api/v1/cloud/egress
```

## Endpoints

### POST /request --- Proxy an outbound HTTP request

The primary endpoint. An agent submits the details of the HTTP request it wants to make, and the proxy executes it on the agent's behalf.

**Request**

```bash
curl -X POST https://api.ainative.studio/api/v1/cloud/egress/request \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agent-001",
    "method": "POST",
    "url": "https://api.openai.com/v1/chat/completions",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": "{\"model\":\"gpt-4\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}",
    "credential_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
    "timeout_seconds": 30
  }'
```

**Request Body**

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `agent_id` | string | Yes | -- | ID of the agent making the request |
| `method` | string | Yes | -- | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` |
| `url` | string | Yes | -- | Full target URL (max 2048 chars) |
| `headers` | object | No | `{}` | Request headers (credential-like headers are stripped) |
| `body` | string | No | `null` | Request body (max 1 MB) |
| `credential_id` | string | No | `null` | ID of a credential in the vault to inject |
| `timeout_seconds` | integer | No | `30` | Request timeout (1-120 seconds) |

**Response** `200 OK`

```json
{
  "status_code": 200,
  "headers": {
    "content-type": "application/json",
    "x-request-id": "req_abc123"
  },
  "body": "{\"id\":\"chatcmpl-xyz\",\"choices\":[{\"message\":{\"content\":\"Hello!\"}}]}",
  "request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "latency_ms": 842
}
```

The `request_id` is a unique identifier for audit trail purposes. Use it to look up this request in the egress logs.

**Error Responses**

The proxy returns structured error responses (not HTTP-level errors) to give the agent actionable information:

| `status_code` in response | Meaning |
|---------------------------|---------|
| 403 | Domain not in allowlist, SSRF blocked, or credential not found/unauthorized |
| 429 | Rate limit exceeded for this agent on this domain |
| 502 | Connection error to the target (DNS failure, connection refused, etc.) |
| 504 | Request timed out |

Example error response:

```json
{
  "status_code": 403,
  "headers": {},
  "body": "{\"error\":\"Domain api.evil.com not in allowlist (default deny)\"}",
  "request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "latency_ms": 0
}
```

---

### GET /logs --- Audit log of outbound requests

Retrieve a paginated list of all egress proxy requests, optionally filtered by agent.

**Request**

```bash
curl -X GET "https://api.ainative.studio/api/v1/cloud/egress/logs?agent_id=agent-001&limit=20&offset=0"
```

**Query Parameters**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `agent_id` | string | No | -- | Filter logs by agent ID |
| `limit` | integer | No | 50 | Results per page (1-200) |
| `offset` | integer | No | 0 | Pagination offset |

**Response** `200 OK`

```json
{
  "logs": [
    {
      "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "agent_id": "agent-001",
      "method": "POST",
      "url": "https://api.openai.com/v1/chat/completions",
      "status_code": 200,
      "credential_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
      "latency_ms": 842,
      "error": null,
      "created_at": "2026-06-01T12:00:00+00:00"
    },
    {
      "id": "e38bd20a-49bb-3361-b456-1f13a3b4c568",
      "agent_id": "agent-001",
      "method": "GET",
      "url": "https://api.github.com/repos/org/repo",
      "status_code": 403,
      "credential_id": null,
      "latency_ms": 0,
      "error": "Domain api.github.com not in allowlist (default deny)",
      "created_at": "2026-06-01T11:55:00+00:00"
    }
  ],
  "total": 2
}
```

---

## Credential Injection

When the agent includes a `credential_id` in its egress request, the proxy:

1. Retrieves the credential from the vault using `decrypt_for_egress(credential_id, agent_id)`.
2. Verifies that the requesting agent is in the credential's `agent_ids` list (or that the list is empty, meaning all agents are allowed).
3. Injects the appropriate HTTP header based on the credential type:

| Credential Type | Header Injected |
|----------------|-----------------|
| `bearer_token` | `Authorization: Bearer <value>` |
| `api_key` | `X-API-Key: <value>` |
| `basic_auth` | `Authorization: Basic <base64(value)>` |
| `oauth2_client_credentials` | `Authorization: Bearer <value>` |

If the credential is not found, has been deleted, or the agent is not authorized, the proxy returns a 403 without making the outbound request.

## Stripped Headers

The proxy removes the following headers from every agent request before forwarding, regardless of whether a `credential_id` is provided. This prevents agents from smuggling credentials in headers:

| Header | Why it is stripped |
|--------|--------------------|
| `authorization` | Prevents agents from injecting their own Bearer/Basic tokens |
| `x-api-key` | Prevents agents from injecting API keys directly |
| `api-key` | Alternative API key header used by some providers (e.g., Azure) |
| `x-auth-token` | Custom auth token header |
| `proxy-authorization` | Prevents proxy-level auth injection |
| `cookie` | Prevents session hijacking via cookies |
| `set-cookie` | Stripped from responses as well |

If an agent sets any of these headers, they are silently removed. The agent can still set non-credential headers like `Content-Type`, `Accept`, `User-Agent`, etc.

## Domain Allowlist

The egress proxy enforces a **default-deny** policy. Every outbound request must target a domain that appears on either:

1. **Platform-wide allowed domains** -- always permitted for all agents:
   - `api.openai.com`
   - `api.anthropic.com`
   - `api.cohere.ai`
   - `generativelanguage.googleapis.com`
   - `api.mistral.ai`
   - `api.groq.com`

2. **Per-agent allowlist** -- configured by the user for each agent

### Managing the Agent Allowlist

#### GET /api/v1/cloud/agents/\{agent_id\}/allowlist

Retrieve the domain allowlist for an agent.

```bash
curl -X GET https://api.ainative.studio/api/v1/cloud/agents/agent-001/allowlist \
  -H "Authorization: Bearer $TOKEN"
```

**Response**

```json
{
  "agent_id": "agent-001",
  "domains": [
    {
      "domain": "api.github.com",
      "added_by": "user-uuid",
      "added_at": "2026-06-01T10:00:00+00:00"
    },
    {
      "domain": "*.stripe.com",
      "added_by": "user-uuid",
      "added_at": "2026-06-01T10:00:00+00:00"
    }
  ],
  "platform_domains": [
    "api.openai.com",
    "api.anthropic.com",
    "api.cohere.ai",
    "generativelanguage.googleapis.com",
    "api.mistral.ai",
    "api.groq.com"
  ]
}
```

#### PUT /api/v1/cloud/agents/\{agent_id\}/allowlist

Replace the agent's domain allowlist. This is a full replacement, not a merge.

```bash
curl -X PUT https://api.ainative.studio/api/v1/cloud/agents/agent-001/allowlist \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "domains": ["api.github.com", "*.stripe.com", "hooks.slack.com"]
  }'
```

#### POST /api/v1/cloud/agents/\{agent_id\}/allowlist/check

Test whether a specific domain is allowed for an agent without making an actual request.

```bash
curl -X POST https://api.ainative.studio/api/v1/cloud/agents/agent-001/allowlist/check \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain": "api.github.com"}'
```

**Response**

```json
{
  "domain": "api.github.com",
  "allowed": true,
  "matched_rule": "agent:api.github.com",
  "reason": "Allowed by agent domain allowlist"
}
```

### Wildcard Patterns

Domain patterns support wildcard prefixes:

| Pattern | Matches | Does Not Match |
|---------|---------|----------------|
| `api.github.com` | `api.github.com` | `github.com`, `raw.github.com` |
| `*.github.com` | `api.github.com`, `raw.github.com`, `github.com` | `github.io` |
| `*.stripe.com` | `api.stripe.com`, `dashboard.stripe.com` | `stripe.com.evil.net` |

## Rate Limiting

The egress proxy enforces rate limits per agent per destination domain using a sliding window:

- **Default limit**: 60 requests per minute per agent per domain
- **Window**: 60-second sliding window
- **Scope**: Each `(agent_id, domain)` pair has its own counter

When the limit is exceeded, the proxy returns:

```json
{
  "status_code": 429,
  "headers": {},
  "body": "{\"error\":\"Rate limit exceeded: 60 requests/min for api.openai.com\"}",
  "request_id": "...",
  "latency_ms": 0
}
```

The rate limiter is in-memory, so counters reset on service restart. This is intentional -- it protects external APIs from runaway agents without requiring persistent state.

## SSRF Protection

Before every outbound request, the proxy resolves the target hostname and checks whether it points to a private or internal IP address. The following network ranges are blocked:

| Network | Description |
|---------|-------------|
| `10.0.0.0/8` | RFC 1918 private |
| `172.16.0.0/12` | RFC 1918 private |
| `192.168.0.0/16` | RFC 1918 private |
| `127.0.0.0/8` | Loopback |
| `169.254.0.0/16` | Link-local |
| `::1/128` | IPv6 loopback |
| `fe80::/10` | IPv6 link-local |
| `fc00::/7` | IPv6 unique local |

This prevents agents from:

- Accessing internal services on the same network (`10.x.x.x`, `172.16.x.x`)
- Reaching the cloud metadata service (`169.254.169.254`)
- Probing localhost services (`127.0.0.1`)
- Using DNS rebinding attacks (hostnames are resolved and checked before the request is sent)

If a hostname resolves to a blocked IP, the request is rejected with:

```json
{
  "status_code": 403,
  "body": "{\"error\":\"Blocked: internal.service resolves to 10.0.1.5 in private network 10.0.0.0/8\"}",
  "request_id": "...",
  "latency_ms": 0
}
```

## Request Lifecycle

```
Agent                    Egress Proxy               Allowlist         Vault            Target API
  |                          |                         |                |                  |
  |-- POST /egress/request ->|                         |                |                  |
  |                          |-- is_request_allowed? -->|                |                  |
  |                          |   (SSRF + domain check) |                |                  |
  |                          |<-- allowed/denied -------|                |                  |
  |                          |                         |                |                  |
  |                          |-- check rate limit      |                |                  |
  |                          |                         |                |                  |
  |                          |-- strip cred headers    |                |                  |
  |                          |                         |                |                  |
  |                          |-- decrypt_for_egress -->|                |                  |
  |                          |   (credential_id,       |  (if cred_id) |                  |
  |                          |    agent_id)            |                |                  |
  |                          |<-- {type, value} -------|                |                  |
  |                          |                         |                |                  |
  |                          |-- inject auth header    |                |                  |
  |                          |-- forward request ----->|                |                  |
  |                          |                         |                |   (forwarded)    |
  |                          |<-- response ------------|----------------|------------------|
  |                          |                         |                |                  |
  |                          |-- log to audit table    |                |                  |
  |                          |                         |                |                  |
  |<-- EgressResponse -------|                         |                |                  |
```

## End-to-End Example

This example walks through storing a credential, configuring an allowlist, and making a proxied request.

### 1. Store a credential

```bash
curl -X POST https://api.ainative.studio/api/v1/cloud/credentials \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe Secret Key",
    "credential_type": "bearer_token",
    "credential_value": "sk_live_EXAMPLE_not_a_real_key",
    "target_domain": "api.stripe.com",
    "agent_ids": ["billing-agent"]
  }'
```

Save the returned `id` (e.g., `cred-uuid-here`).

### 2. Add the domain to the agent's allowlist

```bash
curl -X PUT https://api.ainative.studio/api/v1/cloud/agents/billing-agent/allowlist \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["api.stripe.com"]}'
```

### 3. Make a proxied request

```bash
curl -X POST https://api.ainative.studio/api/v1/cloud/egress/request \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "billing-agent",
    "method": "GET",
    "url": "https://api.stripe.com/v1/charges?limit=5",
    "headers": {"Content-Type": "application/json"},
    "credential_id": "cred-uuid-here"
  }'
```

The proxy will:
- Confirm `api.stripe.com` is in the agent's allowlist
- Verify `billing-agent` is in the credential's `agent_ids`
- Strip any auth headers the agent may have set
- Inject `Authorization: Bearer sk_live_EXAMPLE_not_a_real_key`
- Forward the GET to Stripe
- Return the response to the agent

### 4. Review the audit log

```bash
curl -X GET "https://api.ainative.studio/api/v1/cloud/egress/logs?agent_id=billing-agent&limit=5"
```

## Timeout and Redirect Behavior

- **Timeout**: Configurable per request (1-120 seconds, default 30). Timed-out requests return status code 504.
- **Redirects**: The proxy follows up to 5 redirects automatically. The final response is returned to the agent.
- **Response body logging**: The audit log stores up to 4096 bytes of response body for debugging.
