> For the complete documentation index, see [llms.txt](https://docs.p0.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.p0.dev/getting-started/deploying-the-p0-mcp-gateway.md).

# Deploying the P0 MCP Gateway

The P0 MCP Gateway is the self-hosted runtime enforcement layer for [agentic authorization](/readme/agentic-control-plane.md). You deploy it into your own Kubernetes cluster, where it sits in the data path between your AI agents and your MCP servers. Because it runs in your environment, your sensitive data and upstream credentials never leave your perimeter.

This guide covers the architecture of the self-hosted components and how to install them.

## Architecture

Agentic authorization spans three runtime planes — two you self-host, and the P0 Controller, which P0 delivers as SaaS.

```mermaid
flowchart LR
  A["AI agent<br/>(MCP client)"]

  subgraph self["Your environment (self-hosted)"]
    direction TB
    GW["P0 MCP Gateway"]
    OS["P0 OAuth server"]
    UP["Upstream MCP servers"]
  end

  subgraph saas["P0 SaaS"]
    CTRL["P0 Controller"]
  end

  IDP["Your IdP<br/>(Okta, Entra ID, Google)"]
  RES["Apps & cloud resources"]
  SIEM["Logging sink / SIEM<br/>(Splunk, Elastic, Grafana, …)"]

  A -->|"OAuth token (user + agent)"| GW
  A -.->|"authenticate"| OS
  OS <-->|"federate"| IDP
  GW <-->|"policy sync, approvals"| CTRL
  GW -->|"token exchange, WIF credentials"| OS
  GW -->|"proxied tool calls"| UP
  GW -->|"audit"| SIEM
  UP -->|"access"| RES
  CTRL -->|"session identity and access provisioning"| RES
```

**P0 MCP Gateway** (self-hosted). The front door for agent traffic. It proxies MCP tool calls to your upstream MCP servers, verifies the identity token on every call, evaluates policy, and ships audit activity to your logging sink or SIEM. For upstream servers that require just-in-time credentials, it launches an isolated, per-session container for each grant and injects short-lived credentials into it. The gateway periodically syncs MCP server definitions and tool catalogs from the Controller, and calls the Controller to drive the access request → approval → grant → revoke lifecycle.

**P0 OAuth server** (self-hosted). The authorization server and credential broker. It federates with your existing IdP to authenticate the user, issues signed tokens that bind the user and agent identities together, and brokers downstream cloud credentials. When the gateway needs to reach a cloud resource, the OAuth server mints short-lived Workload Identity Federation (WIF) credentials for that session (for example, exchanging a signed OIDC token with GCP STS or AWS `AssumeRoleWithWebIdentity`) — so cloud credentials are minted per session rather than stored.

**P0 Controller** (P0 SaaS). The policy layer. It holds your MCP roles and policies, pushes server definitions to the gateway, decides whether each tool call is allowed, coordinates approvals, and provisions session identity and access in your upstream apps and cloud resources for deeper enforcement. The Controller is not deployed by these charts — the self-hosted components reach it over HTTPS at your tenant URL.

## Prerequisites

* A Kubernetes cluster (EKS, GKE, or AKS).
* A block-storage `StorageClass` for the bundled PostgreSQL. For example, `gp2` on EKS (requires the EBS CSI driver add-on), `standard-rwo` on GKE, or `managed-premium` on AKS.
* A Google OAuth 2.0 client (client ID and client secret) for user sign-in. The self-hosted OAuth server authenticates end users through Google today.
* A P0 SaaS tenant — you need your tenant's P0 URL and audience.
* Control over a public DNS record for the gateway's hostname (required for TLS certificate issuance).
* `helm` (v3, with OCI support) and `kubectl` configured against your cluster.

## Deployment options

P0 publishes three artifacts. Most deployments should use the **batteries-included Helm chart**.

| Option                                                                                                                          | What it is                                                                                                                                                                          | When to use it                                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Batteries-included Helm chart** — [`p0security/p0-helm-oauthed-mcp`](https://hub.docker.com/r/p0security/p0-helm-oauthed-mcp) | An umbrella chart that deploys the gateway and OAuth server **plus** everything they depend on: Envoy Gateway, cert-manager (with Let's Encrypt), PostgreSQL, and Valkey.           | Standard installs, and any cluster that doesn't already provide these dependencies. **Recommended.**          |
| **Basic Helm chart** — [`p0security/oauthed-mcp`](https://hub.docker.com/r/p0security/oauthed-mcp)                              | Deploys only the gateway and OAuth server and their wiring. It does **not** include the Gateway API controller, cert-manager/TLS, PostgreSQL, or Valkey — you bring those yourself. | Clusters that already manage those dependencies (for example, a platform team's shared Postgres and ingress). |
| **Terraform module**                                                                                                            | A thin wrapper that installs the batteries-included chart via a `helm_release`.                                                                                                     | Teams that manage infrastructure as code with Terraform.                                                      |

The rest of this guide uses the batteries-included chart.

## Install with the batteries-included Helm chart

### 1. Create the `app-secrets` secret

The chart doesn't create secrets for you — the `app-secrets` Secret must exist in the target namespace before you install. It holds the signing keys and credentials the services need:

| Key                                  | Description                                               |
| ------------------------------------ | --------------------------------------------------------- |
| `JWT_PRIVATE_KEY` / `JWT_PUBLIC_KEY` | Key pair the OAuth server uses to sign and verify tokens. |
| `KMS_LOCAL_ENCRYPTION_KEY`           | 32-byte (64 hex character) key for local encryption.      |
| `REFRESH_TOKEN_SECRET`               | Base64-encoded secret (≥32 bytes) for refresh tokens.     |
| `OIDC_CLIENT_SECRET`                 | The client secret for your Google OAuth client.           |
| `POSTGRESQL_PASSWORD`                | Password for the bundled PostgreSQL.                      |

Generate the signing key pair and random secrets, then create the Secret in your namespace:

```bash
# Create the namespace (the Secret must exist before you install)
kubectl create namespace oauthed-mcp

# RSA signing key pair for the OAuth server
openssl genrsa -out jwt-private.pem 2048
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem

# Create app-secrets with the generated keys, random secrets, and your OIDC client secret
kubectl -n oauthed-mcp create secret generic app-secrets \
  --from-file=JWT_PRIVATE_KEY=jwt-private.pem \
  --from-file=JWT_PUBLIC_KEY=jwt-public.pem \
  --from-literal=KMS_LOCAL_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
  --from-literal=REFRESH_TOKEN_SECRET="$(openssl rand -base64 32)" \
  --from-literal=OIDC_CLIENT_SECRET="<your-oidc-client-secret>" \
  --from-literal=POSTGRESQL_PASSWORD="$(openssl rand -base64 24)"
```

### 2. Create your values file

{% hint style="info" %}
Google Workspace is currently the only supported identity provider for user sign-in. The `oidcClientId` in the following example is a Google OAuth 2.0 client ID.
{% endhint %}

Create a `my-values.yaml` with the values required for your environment:

```yaml
letsEncrypt:
  email: platform@example.com   # for TLS certificate registration
  env: staging                  # switch to "prod" once staging certs succeed

postgresql:
  storageClass: gp2             # your cluster's block-storage class

oauthed-mcp:
  imageRegistry: p0security     # override only if self-building images

  gateway:
    className: oauthed-mcp      # unique per release; use the release name from the `helm install` command in step 3
    host: gateway.example.com   # public hostname for the gateway
    tlsSecretName: gateway-tls

  oauthServer:
    oidcClientId: <your-client-id>.apps.googleusercontent.com   # Google OAuth 2.0 client ID; user sign-in goes through Google
    p0Url: https://api.p0.app/o/<tenant>
    p0Audience: https://api.p0.app/o/<tenant>
    gatewayIss: https://gateway.example.com

  mcpServer:
    oauthServer: http://oauth-server:52701                 # in-cluster URL
    p0Url: https://api.p0.app/o/<tenant>
    p0Audience: https://api.p0.app/o/<tenant>              # equal to p0Url
    gatewayIss: https://gateway.example.com
    managementAudience: https://gateway.example.com/manage # gateway address + "/manage" path
    manageGcpIssuers: https://accounts.google.com          # accept Google-issued identity tokens on /manage; required for the P0 Controller to reach the gateway
    manageAllowedEmails: customer-p0-gcp-sa@p0-prod.iam.gserviceaccount.com # P0 service account allowed to call the gateway's /manage endpoint
```

If your cluster already provides any of the bundled dependencies, disable the corresponding subchart (`cert-manager.enabled`, `envoy-gateway.enabled`, `postgresql.enabled`, `valkey.enabled`) and point the services at your managed services instead.

### 3. Install the chart

```bash
helm install oauthed-mcp \
  oci://registry-1.docker.io/p0security/p0-helm-oauthed-mcp \
  --version <chart-version> \
  --namespace oauthed-mcp \
  -f my-values.yaml
```

{% hint style="warning" %}
Don't pass `--wait`. cert-manager can't issue the TLS certificate until you add the DNS record in the next step, so `--wait` would block and eventually time out.
{% endhint %}

### 4. Add the DNS record

Find the load balancer address assigned to the gateway, then create a public DNS record for your gateway host that points at it:

```bash
kubectl -n oauthed-mcp get gateway oauthed-mcp \
  -o jsonpath='{.status.addresses[0].value}'
```

cert-manager retries certificate issuance automatically; the certificate becomes `Ready` once DNS resolves.

### 5. Verify

Confirm the pods are running:

```bash
kubectl -n oauthed-mcp get pods
```

Confirm DNS resolves and the certificate is `Ready` (this may take several minutes):

```bash
kubectl -n oauthed-mcp get certificate
```

Desired output:

```
NAME          READY   SECRET        AGE
gateway-tls   True    gateway-tls   15m
```

Once staging certificates issue successfully, promote to production certificates:

```bash
helm upgrade oauthed-mcp \
  oci://registry-1.docker.io/p0security/p0-helm-oauthed-mcp \
  --namespace oauthed-mcp --reuse-values \
  --set letsEncrypt.env=prod
```

The `prod` certificate is required for testing the backends. The `gateway-tls` certificate is reissued by cert-manager. Wait for it to be `Ready` again.

Confirm both backends respond through the gateway host over public DNS. Replace `<gateway-host>` with your gateway's public hostname:

```bash
# OAuth server (exact-match route) — expect 200
curl -sS -o /dev/null -w '%{http_code}\n' https://<gateway-host>/.well-known/oauth-authorization-server

# Gateway (catch-all route) — expect 200
curl -sS -o /dev/null -w '%{http_code}\n' https://<gateway-host>/health

# HTTP is redirected to HTTPS — expect 301
curl -sS -o /dev/null -w '%{http_code}\n' http://<gateway-host>/health
```

## Install with the Terraform module

The Terraform module wraps the batteries-included chart in a single `helm_release`. It requires Terraform ≥ 1.7 and the `hashicorp/helm` provider ≥ 3.0.

```hcl
module "oauthed_mcp" {
  source  = "p0-security/p0-oauthed-mcp/kubernetes"
  version = "<module-version>"

  release_name     = "oauthed-mcp"
  namespace        = "oauthed-mcp"
  create_namespace = true

  # Same YAML as my-values.yaml above, passed through to the chart.
  values = [file("${path.module}/my-values.yaml")]
}
```

The module passes your values straight through to the chart, so the prerequisites are the same: you still create the `app-secrets` Secret and add the DNS record yourself. Each module version pins a specific chart version — check the module's compatibility matrix.

## Next steps

Once you deploy the gateway, register it with P0 and configure the MCP servers it fronts:

* [Agentic Gateway integration](/integrations/resource-integrations/agentic-gateway.md) — register the gateway and configure upstream MCP servers.
