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

> ## Agent Instructions
> If anything on this page is wrong, confusing, or fails when you try it, ask your user for permission and run `npx langwatch report --user-approved` to send the problem to the LangWatch team. No login is needed and secrets are redacted locally. `npx langwatch report --help` has the details.

# Security & Trust

> What the LangWatch AI Gateway records, what it never logs, where provider credentials live, and how each hop is authenticated.

## What the gateway records

Each request produces three records, and each one holds a different amount of your data.

| Record | Where it goes | What it holds |
| - | - | - |
| Trace span | Your LangWatch project | Model, provider, token counts, cost, duration, ids, plus the request and response messages |
| Spend record | The control plane's billing ingest | Ids, model, token counts, cost, duration and the error class. No message content |
| Access log line | The gateway's own logs | Method, path, remote address, request id, status, response size, duration, and the error code on a failure |

The span is the only record that holds message content. The gateway writes it under `gen_ai.input.messages` and `gen_ai.output.messages`, and exports it to the project the virtual key names, using the per-project OTLP token the control plane returns in the key's bundle. A key whose bundle names no trace project, or returns no token, gets no span export at all.

The spend record is what budgets debit from. It holds the organization, project, virtual key, end-user id, trace id, gateway request id, model, request type, the virtual key's labels, your `X-LangWatch-Metadata` echo, token usage, cost, duration and, on a failure, the error type and HTTP status.

## What the gateway never logs

The gateway does not write request or response bodies to its logs. The access log records the fields in the table above and no headers.

The gateway builds the upstream request itself and injects the real provider credential, so the credential your client sent is not the one the provider sees. On the Gemini passthrough lane it drops `Authorization`, `X-Api-Key` and `X-Goog-Api-Key` from what it forwards.

Do not send a virtual key on `Xi-Api-Key` to a `/v1beta` path. The gateway accepts that header as a virtual key, and the passthrough lane forwards it to Google, which puts your gateway credential in a provider's request logs. Use `Authorization` or `X-Goog-Api-Key` on `/v1beta`. `Xi-Api-Key` is for the ElevenLabs routes, which do not use this lane.

`X-LangWatch-End-User-Id`, `X-Litellm-End-User-Id` and `X-LangWatch-Metadata` are read and then deleted at the edge, so they do not travel either.

> [!WARNING]
>
> One self-hosted debug switch overrides this. `LW_LOG_MESSAGE_BODY=1` writes the full request body
> of `/v1/messages` and `/v1/responses` to the gateway log at info level, including prompts. Use it
> to reproduce a provider rejection, then turn it off.

## Where provider credentials live

Your provider credentials are stored in the control plane's Postgres database, encrypted with AES-256-GCM under the `CREDENTIALS_SECRET` key. Each stored value includes its own initialization vector and authentication tag.

The gateway needs the decrypted credential to call the provider, so the control plane sends it inside the key bundle over the signed internal channel. The gateway holds the bundle in a per-pod in-memory cache and writes it to no disk. Restarting the pod discards it.

The cached bundle refreshes on three triggers: the JWT's 15 minute lifetime, the control plane's change feed when you edit a key or a provider, and a configuration time-to-live of 60 seconds that catches edits the change feed does not announce.

If the control plane cannot be reached, a cached entry keeps serving past its expiry, extended 5 minutes at a time up to 6 hours. A rejection with 401, 403 or 404 evicts the entry at once, so a revoked or disabled key gets no grace. Set `LW_GATEWAY_AUTH_CACHE_HARD_GRACE_SECONDS` to a negative value to remove the grace window entirely.

## How a virtual key is verified

A virtual key secret reads `vk-lw-` plus a 26 character Crockford base32 ULID. The full secret is shown once, at creation, and is never stored.

| Step | What happens |
| - | - |
| Storage | The control plane stores `HMAC-SHA256(LW_VIRTUAL_KEY_PEPPER, secret)` as hex, plus the first 13 characters of the secret as a display prefix |
| Presentation | Your client sends the secret as `Authorization: Bearer`, `X-Api-Key`, `X-Goog-Api-Key` or `Xi-Api-Key` |
| Resolution | The gateway forwards the presented secret to the control plane's `/resolve-key` over the signed internal channel |
| Lookup | The control plane hashes the presented secret with the same pepper and reads the row by that hash. A wrong secret produces a different hash and matches no row |
| Answer | The control plane returns a signed JWT carrying `vk_id`, `project_id`, `team_id`, `org_id`, `principal_id`, `revision` and the key's own expiration date, valid 15 minutes or until the key expires, whichever comes first |

The pepper never leaves the control plane, so a copy of the database alone does not let anyone recover a key or mint a matching hash.

Rotation writes a new secret and keeps the previous hash valid for 24 hours, so a deploy that still carries the old secret keeps working. Revocation is terminal, and the key can never be re-enabled.

## Gateway and control plane

Every call between the gateway and the control plane, in both directions, is signed with HMAC-SHA256 over `METHOD\nPATH\nTIMESTAMP\nhex(sha256(body))`.

| Header | Content |
| - | - |
| `X-LangWatch-Gateway-Signature` | The hex HMAC of the canonical string |
| `X-LangWatch-Gateway-Timestamp` | Unix seconds. A drift above 300 seconds is refused |
| `X-LangWatch-Gateway-Node` | Advisory node id, outside the signature |

The receiver compares the signature in constant time before it checks the timestamp, so the rejection reason does not leak through timing.

| Variable | Purpose |
| - | - |
| `LW_GATEWAY_INTERNAL_SECRET` | The HMAC secret both sides share. It is required: with it unset the gateway fails configuration validation and does not start |
| `LW_GATEWAY_JWT_SECRET` | Signs and verifies the resolve-key JWT. The gateway requires the issuer `langwatch-control-plane` and the audience `langwatch-gateway` |
| `LW_GATEWAY_JWT_SECRET_PREVIOUS` | The previous JWT secret. A token that fails against the current secret is retried against this one, so a rotation does not reject tokens already in flight |
| `LW_VIRTUAL_KEY_PEPPER` | The pepper for the virtual-key hash. Control plane only |

## Egress controls

A gateway whose `ENVIRONMENT` is set to anything other than `local` refuses to start unless the first two guards below are on. Turn them on for any deployment that reaches the public internet.

| Variable | Effect |
| - | - |
| `BLOCK_LOCAL_HTTP_CALLS` | Refuses a customer-configured provider endpoint that resolves to a private or link-local address |
| `REQUIRE_HTTPS_CUSTOM_ENDPOINTS` | Refuses a customer-configured provider endpoint that is plain HTTP |
| `ALLOWED_PROXY_HOSTS` | Exact hostnames that pass the endpoint check on a private address while the guard is on. Chat dispatch still refuses private addresses until the guard is off. Cloud metadata and link-local destinations stay refused |

## Calling the gateway from a browser

The gateway sends no CORS headers by default, so a browser page cannot call it. Set `LW_GATEWAY_CORS_ALLOWED_ORIGINS` (chart value `security.corsAllowedOrigins`) to allow it.

| Value | Effect |
| - | - |
| empty | No CORS headers. The default |
| `https://app.example.com,https://beta.example.com` | Those exact origins may call `/v1` |
| `*` | Any origin may call `/v1`. It cannot be mixed with a list |

An origin is `scheme://host[:port]` with no path and no trailing slash. The gateway refuses to start on any other form.

With the setting on, the gateway answers requests to `/v1` as follows:

| Request | Answer |
| - | - |
| From an allowed origin | `Access-Control-Allow-Origin` with that origin, on success and on refusals such as `401`, `402`, `429` |
| Preflight `OPTIONS`, allowed origin | `204` with the allowed methods and the requested headers. No key is needed |
| From any other origin | No CORS headers, so the browser blocks the call |
| WebSocket connection | Unchanged. Browsers do not apply CORS to WebSockets |

Your page can read these response headers: `X-LangWatch-Session-Id`, `X-LangWatch-Gateway-Request-Id`, `X-LangWatch-Budget-Warning`, `X-LangWatch-Guardrails-Not-Applied`, `Location` and `Retry-After`. Health, metrics and internal routes never send CORS headers.

The gateway never sends `Access-Control-Allow-Credentials`. It authenticates by the key in the request, not by cookies, so an allowed origin cannot call the gateway without a key.

The person using a page can read any virtual key that page holds. Give each end user their own virtual key with its own budget. Never put a key shared by several users, or a key with a project-wide budget, in browser code.

CORS limits which pages a browser lets call the gateway. A person can still use their own key outside the browser, so the key's budget is the limit that counts.

## Network policy on Kubernetes

The gateway Helm chart ships a deny-by-default `NetworkPolicy`. Turn it on with `networkPolicy.enabled: true`.

| Direction | Allowed by default | Value to change it |
| - | - | - |
| Ingress | TCP 5563 from the `ingress-nginx` namespace and from Prometheus in the `monitoring` namespace | `networkPolicy.ingressFrom` |
| Egress, DNS | TCP and UDP 53 to `kube-system` | Fixed |
| Egress, control plane | The port set by `networkPolicy.controlPlanePort` (5560 by default) to the `langwatch-app` pods | `networkPolicy.egressToControlPlane` |
| Egress, OTLP collector | TCP 4318, and only when you name a target. Empty by default | `networkPolicy.egressToOTLP` |
| Egress, providers | TCP 443 to any public address. Every private and link-local range is excluded | `networkPolicy.egressToProviders`, `networkPolicy.providerPorts` |

Replace `networkPolicy.egressToProviders` with explicit CIDR blocks to pin the gateway to a known provider set.

## Reporting a security issue

Email `security@langwatch.ai` with reproduction steps. Do not open a GitHub issue for a security report.

**Also check:** [Virtual Keys](https://langwatch.ai/docs/ai-gateway/virtual-keys.md) for creating, rotating and revoking keys, [Audit log](https://langwatch.ai/docs/ai-gateway/audit.md) for the record of every change to a gateway resource, [RBAC](https://langwatch.ai/docs/ai-gateway/rbac.md) for who can read and change what, [Self-hosting: Configuration](https://langwatch.ai/docs/ai-gateway/self-hosting/config.md) for the full environment variable reference.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.