> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portkey.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom hosts

> Route requests to privately hosted or local models using custom host URLs, and understand Portkey's host validation and security rules.

<Info>
  Custom hosts with private network routing — including the `TRUSTED_CUSTOM_HOSTS` allowlist — applies only to **hybrid and air-gapped enterprise deployments** of the Portkey AI Gateway. On Portkey SaaS, custom host URLs must be publicly reachable; routing to private or internal network IPs is not supported.
</Info>

The `custom_host` parameter routes Portkey Gateway requests to your own model endpoints — whether running locally, in a private cloud, or on custom infrastructure. Portkey validates all custom host URLs to prevent [SSRF](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery) attacks.

## Setting a custom host

Specify a custom host using one of these methods:

| Method         | Parameter                            |
| -------------- | ------------------------------------ |
| HTTP header    | `x-portkey-custom-host`              |
| Python SDK     | `custom_host`                        |
| Node.js SDK    | `customHost`                         |
| Gateway config | `custom_host` (in the target object) |

<CodeGroup>
  ```python Python theme={"system"}
  from portkey_ai import Portkey

  portkey = Portkey(
      api_key="PORTKEY_API_KEY",
      provider="openai",
      custom_host="https://your-llm-server.com/v1/"
  )

  response = portkey.chat.completions.create(
      model="your-model",
      messages=[{"role": "user", "content": "Hello"}]
  )
  ```

  ```javascript Node.js theme={"system"}
  import Portkey from 'portkey-ai'

  const portkey = new Portkey({
      apiKey: "PORTKEY_API_KEY",
      provider: "openai",
      customHost: "https://your-llm-server.com/v1/"
  })

  const response = await portkey.chat.completions.create({
      model: "your-model",
      messages: [{ role: "user", content: "Hello" }]
  })
  ```

  ```sh cURL theme={"system"}
  curl https://api.portkey.ai/v1/chat/completions \
    -H "Content-Type: application/json" \
    -H "x-portkey-api-key: $PORTKEY_API_KEY" \
    -H "x-portkey-provider: openai" \
    -H "x-portkey-custom-host: https://your-llm-server.com/v1/" \
    -d '{
      "model": "your-model",
      "messages": [{"role": "user", "content": "Hello"}]
    }'
  ```
</CodeGroup>

Also set `custom_host` in a [Gateway config](/product/ai-gateway/configs) target:

```json theme={"system"}
{
  "strategy": { "mode": "fallback" },
  "targets": [
    {
      "provider": "openai",
      "custom_host": "https://your-private-llm.com/v1",
      "forward_headers": ["Authorization"]
    },
    {
      "provider": "openai",
      "api_key": "sk-xxxxx"
    }
  ]
}
```

<Note>
  Include the version path (e.g., `/v1`) in the `custom_host` URL. Portkey appends the endpoint path (`/chat/completions`, `/responses`, etc.) automatically.
</Note>

<Note>
  `forward_headers` cannot include cloud metadata headers such as `metadata-flavor`, `x-aws-ec2-metadata-token`, or `x-google-metadata-request`, or the `x-portkey-forward-headers` header itself.
</Note>

For a full guide on integrating private LLMs, see [Bring Your Own LLM](/integrations/llms/byollm).

***

## How validation works

Portkey applies SSRF checks at two layers on **Node.js deployments**:

1. **Request time** — When a `custom_host` URL is accepted, the gateway validates the hostname, port, scheme, and URL shape before the request proceeds.
2. **Connection time** — On outbound requests, DNS resolution validates every returned IP against the same rules and pins the connection to safe addresses. This blocks DNS rebinding attacks where a hostname initially resolves to a public IP but later resolves to a private or metadata address.

On Cloudflare Workers, request-time validation still applies; connection-time DNS pinning is a Node.js-only hardening layer.

URLs must use `http://` or `https://`, must not embed credentials, and are capped at 2048 characters.

***

## Blocked host patterns

Portkey validates all custom host URLs to prevent requests to internal network resources. The following IP ranges and patterns are **blocked by default** (unless the hostname is explicitly trusted — see [Trusted custom hosts (allowlist)](#trusted-custom-hosts-allowlist)).

### Private IPv4 ranges

| Range            | Description               |
| ---------------- | ------------------------- |
| `10.0.0.0/8`     | Private network (Class A) |
| `172.16.0.0/12`  | Private network (Class B) |
| `192.168.0.0/16` | Private network (Class C) |

### Reserved IPv4 ranges

| Range                                               | Description                                          |
| --------------------------------------------------- | ---------------------------------------------------- |
| `127.0.0.0/8`                                       | Loopback addresses (except `127.0.0.1` when trusted) |
| `169.254.0.0/16`                                    | Link-local addresses                                 |
| `100.64.0.0/10`                                     | Carrier-grade NAT (CGNAT)                            |
| `0.0.0.0/8`                                         | Non-routable (includes `0.0.0.0`)                    |
| `224.0.0.0/4`                                       | Multicast, reserved, and broadcast                   |
| `198.18.0.0/15`                                     | Benchmarking                                         |
| `192.0.0.0/24`                                      | IETF protocol assignments                            |
| `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24` | TEST-NET ranges                                      |
| `192.88.99.0/24`                                    | Former 6to4 relay anycast                            |

### Cloud metadata endpoints

These addresses are **always blocked** and cannot be allowlisted:

| Address            | Description                                                       |
| ------------------ | ----------------------------------------------------------------- |
| `169.254.169.254`  | AWS / Azure / GCP / DO / Oracle / Hetzner / OpenStack IMDS (IPv4) |
| `169.254.169.0/24` | Full AWS IMDS IPv4 subnet                                         |
| `169.254.170.2`    | AWS ECS task metadata / credentials endpoint                      |
| `169.254.170.23`   | AWS ECS task metadata v4                                          |
| `100.100.100.200`  | Alibaba Cloud instance metadata                                   |
| `168.63.129.16`    | Azure platform (wireserver / DHCP / health)                       |
| `192.0.0.192`      | Oracle Cloud Classic metadata                                     |
| `fd00:ec2::/96`    | AWS IMDSv2 IPv6 endpoint                                          |

### Single-label blocked hostnames

These bare hostnames are always blocked (they often resolve to internal services via DNS search domains):

| Hostname        | Description                              |
| --------------- | ---------------------------------------- |
| `metadata`      | Kubernetes / generic metadata short name |
| `instance-data` | AWS instance metadata alternate name     |
| `kubernetes`    | Kubernetes API short name                |

### Blocked hostname suffixes

Subdomains of known cloud metadata and internal service FQDNs are blocked, including (but not limited to):

* `metadata.google.internal`, `metadata.gce.internal`, `metadata.goog`
* `metadata.azure.com`, `internal.cloudapp.net`
* `metadata.internal`, `metadata.do.internal`, `metadata.packet.net`
* `instance-data.ec2.internal`, `ec2.internal`, `compute.internal`
* `metadata.cloud.ibm.com`
* `kubernetes.default`, `kubernetes.default.svc`, `kubernetes.default.svc.cluster.local`
* `cluster.local`
* `consul` (and `*.service.consul`, `*.node.consul`)

These suffix checks run **before** the trusted-host allowlist and cannot be overridden.

### Wildcard DNS rebinding services

Hostnames that encode arbitrary IPs in the DNS label are blocked:

| Domain                                            | Example                    |
| ------------------------------------------------- | -------------------------- |
| `nip.io`                                          | `127.0.0.1.nip.io`         |
| `sslip.io`                                        | `169-254-169-254.sslip.io` |
| `xip.io`                                          | Arbitrary IP encoding      |
| `localtest.me`, `lvh.me`, `vcap.me`, `traefik.me` | Resolve to loopback        |
| `localhost.run`                                   | Public tunnel service      |

### Blocked internal TLDs

Hostnames ending in these TLDs are blocked unless explicitly trusted:

`.local`, `.localdomain`, `.internal`, `.intranet`, `.lan`, `.home`, `.corp`, `.test`, `.invalid`, `.onion`, `.localhost`

### IPv6 local and private ranges

| Range            | Description                            |
| ---------------- | -------------------------------------- |
| `::1`            | IPv6 loopback                          |
| `::`             | IPv6 unspecified                       |
| `fc00::/7`       | Unique local addresses (`fc*` / `fd*`) |
| `fe80::/10`      | Link-local addresses                   |
| `fec0::/10`      | Site-local addresses (deprecated)      |
| `ff00::/8`       | Multicast                              |
| `2001::/32`      | Teredo                                 |
| `100:0:0:1::/64` | RFC 9780 dummy prefix                  |

### IPv6 addresses with embedded IPv4

IPv6 literals that embed a blocked IPv4 address are rejected, including:

* IPv4-mapped (`::ffff:127.0.0.1`)
* IPv4-compatible (`::127.0.0.1`, `::7f00:1`)
* 6to4 (`2002:7f00:1::`)
* NAT64 (`64:ff9b::7f00:1`)

Public IPv6 addresses such as `2001:4860:4860::8888` remain allowed.

### IP obfuscation tricks

The following alternate IP representations are also blocked to prevent bypass attempts:

* **Decimal form** — e.g., `2130706433` (resolves to `127.0.0.1`)
* **Hexadecimal form** — e.g., `0x7f000001` (resolves to `127.0.0.1`)
* **Shortened IPv4** — e.g., `127.1` (resolves to `127.0.0.1`)
* **Octal notation** — e.g., `0177.0.0.1` (resolves to `127.0.0.1`)
* **Percent-encoded hostnames** — e.g., `%31%32%37...`
* **Punycode labels** — hostnames containing `xn--` labels

***

## Blocked ports

Even for trusted hostnames, Portkey blocks ports commonly used by non-HTTP internal services (SSH, databases, caches, admin APIs, etc.). Examples include `22`, `25`, `445`, `3306`, `5432`, `6379`, `6443`, `9200`, and `27017`.

HTTP and HTTPS service ports (such as `80`, `443`, `8000`, `8080`, `8443`) are allowed. Ports must be between `1` and `65535`.

***

## Trusted custom hosts (allowlist)

Portkey maintains a trusted hosts allowlist via the `TRUSTED_CUSTOM_HOSTS` environment variable. Trusted hostnames bypass private/reserved IP range checks at request time, and their DNS-resolved addresses bypass private/reserved IP checks at connection time.

<Note>
  `TRUSTED_CUSTOM_HOSTS` is available only on [self-hosted](/self-hosting/hybrid-deployments/architecture) hybrid and air-gapped enterprise deployments of the Portkey AI Gateway.
</Note>

### Default behavior

| Environment                                    | `TRUSTED_CUSTOM_HOSTS` unset | Behavior                                                                     |
| ---------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------- |
| **Non-production** (`NODE_ENV` ≠ `production`) | Yes                          | Defaults to `localhost`, `127.0.0.1`, `::1`, and `host.docker.internal`      |
| **Production** (`NODE_ENV` = `production`)     | Yes                          | **Empty allowlist** — localhost and private IPs are blocked until you opt in |
| Any                                            | Explicitly set               | Uses **only** the hosts you specify (replaces defaults; does not merge)      |

<Warning>
  When setting `TRUSTED_CUSTOM_HOSTS` in non-production, include the default values (`localhost`, `127.0.0.1`, `::1`, `host.docker.internal`) along with your additions if you still need local development access.
</Warning>

### Adding hosts to the allowlist

To route to a private network IP (e.g., `172.31.2.45`), add it to the trusted hosts allowlist using the `TRUSTED_CUSTOM_HOSTS` environment variable.

Set the environment variable as a comma-separated list of hosts:

```bash theme={"system"}
TRUSTED_CUSTOM_HOSTS="localhost,127.0.0.1,::1,host.docker.internal,172.31.2.45,*.svc.internal"
```

Requests with `custom_host` set to `http://172.31.2.45:8008/v1/` will then pass validation.

### Entry format

Each entry can be:

| Format                          | Matches                                                                                      |
| ------------------------------- | -------------------------------------------------------------------------------------------- |
| `example.com`                   | Exact hostname only (`example.com`)                                                          |
| `*.example.com`                 | Apex domain and any subdomain (`example.com`, `api.example.com`, `v1.api.example.com`, etc.) |
| `172.31.2.45`                   | Exact IP literal only                                                                        |
| `https://example.com:8080/path` | Normalized to `example.com`                                                                  |

Wildcard entries (`*.domain`) are supported in `TRUSTED_CUSTOM_HOSTS`. Subdomains are **not** inherited from a bare domain entry — `example.com` does not allow `api.example.com`. To trust subdomains, add an explicit wildcard entry such as `*.example.com`.

Entries with schemes, ports, or paths are normalized automatically. Wildcard entries (`*.`) cannot target IP literals.

When `localhost` is trusted, subdomains ending in `.localhost` (e.g., `api.localhost`) are also allowed.

<Warning>
  Cloud metadata endpoints, metadata FQDN suffixes, and wildcard DNS rebinding domains **cannot** be allowlisted. Entries that overlap always-blocked ranges are silently dropped from `TRUSTED_CUSTOM_HOSTS`.
</Warning>

### Validation rules for trusted hosts

Even for trusted hosts, Portkey enforces:

* **Port range** — The port must be between `1` and `65535`, and must not be on the blocked-ports list
* **Host-only matching** — Only the hostname or IP is checked against the allowlist, not the full URL
* **IP literals are exact-match only** — Trusting `127.0.0.1` does not unlock `x.127.0.0.1`
* **Subdomains require a wildcard** — Trusting `example.com` does not unlock `api.example.com`; add `*.example.com` to cover subdomains
* **Always-blocked overrides** — Cloud metadata, IMDS subnets, blocked hostname suffixes, wildcard rebinding domains, and dangerous IPv6-embedded-IPv4 forms remain blocked even when trusted

***

## Common scenarios

| Scenario                                            | Default behavior                                                               | Action needed                                                                                                             |
| --------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| **Local development** (e.g., Ollama on `localhost`) | Allowed in non-production — `localhost` and `127.0.0.1` are trusted by default | None in dev. In production, add to `TRUSTED_CUSTOM_HOSTS`. See the [Ollama integration guide](/integrations/llms/ollama). |
| **Docker containers** (`host.docker.internal`)      | Allowed in non-production — trusted by default                                 | In production, add to `TRUSTED_CUSTOM_HOSTS`                                                                              |
| **Private network IP** (e.g., `172.31.2.45:8008`)   | Blocked — falls within `172.16.0.0/12`                                         | Add the IP to `TRUSTED_CUSTOM_HOSTS` (hybrid/air-gapped only)                                                             |
| **Internal DNS name** (e.g., `llm.svc.internal`)    | Blocked — `.internal` TLD                                                      | Add the hostname (or `*.svc.internal`) to `TRUSTED_CUSTOM_HOSTS`                                                          |
| **Cloud metadata** (`169.254.169.254`)              | Always blocked                                                                 | Cannot be allowlisted for security reasons                                                                                |
| **DNS rebinding** (`127.0.0.1.nip.io`)              | Always blocked                                                                 | Cannot be allowlisted for security reasons                                                                                |
