The [Pomerium Kubernetes Ingress Controller](https://github.com/pomerium/ingress-controller) is Pomerium's official, open-source controller for Kubernetes environments. Pomerium's Ingress Controller builds secure access to Kubernetes Services by enforcing access control policies based on user identity; Enterprise users can build access control policies that include criteria like device, location, and other contextual factors."

## How Pomerium Ingress Controller works

Pomerium's Ingress Controller for Kubernetes enables you to dynamically provision routes from Ingress resources and set authorization policy on those routes with Ingress annotations. By defining routes as Ingress resources in the Kubernetes API, you can easily create and remove those routes from your Pomerium configuration.

If you've tested Pomerium using the [all-in-one binary](https://www.pomerium.com/docs/deploy/core.md), you're probably familiar with configuring routes in Pomerium's [`config.yaml`](https://www.pomerium.com/docs/internals/configuration.md) file. When using the Pomerium Ingress Controller, each route is defined as an Ingress resource in the Kubernetes API.

This document shows you how to configure an Ingress resource that's compatible with the Pomerium Ingress Controller.

**Before you start:**

This document assumes you've installed the Pomerium Ingress Controller and added global configuration settings with the [Pomerium CRD](https://www.pomerium.com/docs/deploy/k8s/configure.md).

If you haven't completed these steps, see the following docs:

- [Install Pomerium Ingress Controller](https://www.pomerium.com/docs/deploy/k8s/install.md)
- [Global Configuration](https://www.pomerium.com/docs/deploy/k8s/configure.md)

## Configure an Ingress resource

The Pomerium Ingress Controller monitors Ingress resources in the cluster.

Keep the following items in mind:

- By default, Ingress resources in all namespaces are watched.
- Only resources with a matching `spec.ingressClassName` would be served.
- TLS (HTTPS) is required.

### Set the Ingress class

The default installation adds `pomerium` [IngressClass](https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class) to your cluster. In order for Pomerium to service your Ingress objects, please set `spec.ingressClassName` to `pomerium`.

It is also possible to [set Pomerium to be a default ingress controller](https://www.pomerium.com/docs/deploy/k8s/install.md#set-pomerium-as-default-ingressclass) cluster-wide.

### Set Ingress annotations

Most configuration keys in non-Kubernetes deployments can be specified as annotation in an Ingress Resource definition. The format is `ingress.pomerium.io/${OPTION_NAME}`.

Kubernetes object annotations are a **string map** and `Ingress` annotation value has to be a YAML string in quote marks. Pomerium would parse the value of the annotation string to decode the desired format.

The expandable list below contains the annotations available, which behave as described in our reference documentation (with links to the appropriate reference documentation).

The remaining annotations are specific to or behave differently than they do when using Pomerium without the Ingress Controller:

| Annotation | Description |
| --- | --- |
| `ingress.pomerium.io/kubernetes_service_account_token_secret` | Name of a Kubernetes Secret containing a [Kubernetes Service Account Token](https://www.pomerium.com/docs/reference/routes/kubernetes-service-account-token.md) in a `token` key. |
| `ingress.pomerium.io/name` | Sets a human-readable name for the route. See [Route Naming](#route-naming) below. |
| `ingress.pomerium.io/mcp_client` | When set to `"true"`, configures the route as an MCP (Model Context Protocol) client. The URL is defined by the service backend. |
| `ingress.pomerium.io/mcp_server` | When set to `"true"`, configures the route as an MCP (Model Context Protocol) server. The URL is defined by the service backend. Optional if other MCP server annotations are present. |
| `ingress.pomerium.io/mcp_server_max_request_bytes` | Sets the maximum request body size for MCP server routes. |
| `ingress.pomerium.io/mcp_server_path` | Sets the path property for MCP server routes, used when returning the server URL in the .mcp/routes endpoint. Defaults to "/" if not specified. |
| `ingress.pomerium.io/mcp_server_upstream_oauth2_secret` | Name of a Kubernetes Secret containing OAuth2 credentials for MCP server upstream authentication. |
| `ingress.pomerium.io/mcp_server_upstream_oauth2_token_url` | OAuth2 token URL for MCP server upstream authentication. |
| `ingress.pomerium.io/mcp_server_upstream_oauth2_scopes` | Comma-separated list of OAuth2 scopes for MCP server upstream authentication. |
| `ingress.pomerium.io/path_regex` | When set to `"true"` enables path regex matching. See the [Regular Expressions Path Matching](#regular-expressions-path-matching) section for more information. |
| `ingress.pomerium.io/secure_upstream` | When set to `"true"`, use `https` when connecting to the upstream endpoint. |
| `ingress.pomerium.io/set_request_headers_secret` | Name of Kubernetes Secret containing the contents of the request header to send upstream. When used, `ingress.pomerium.io/set_request_headers` should not contain overlapping keys. |
| `ingress.pomerium.io/set_response_headers_secret` | Name of Kubernetes Secret containing the contents of the response header to send downstream. When used, `ingress.pomerium.io/set_response_headers` should not contain overlapping keys. |
| `ingress.pomerium.io/service_proxy_upstream` | When set to `"true"` forces Pomerium to connect to upstream servers through the k8s service proxy, and not individual endpoints. <br /> This is useful when deploying Pomerium inside a service mesh. |
| `ingress.pomerium.io/ssh_upstream` | When set to `"true"`, enables native SSH proxying for the route. See the [example below](#native-ssh) for more information. |
| `ingress.pomerium.io/tcp_upstream` | When set to `"true"`, defines the route as supporting a TCP tunnel. See the [example below](#tcpudp-services) for more information. |
| `ingress.pomerium.io/udp_upstream` | When set to `"true"`, defines the route as supporting a UDP tunnel. See the [example below](#tcpudp-services) for more information. |
| `ingress.pomerium.io/tls_client_secret` | Name of Kubernetes `tls` Secret containing a [client certificate][tls_client_certificate] for connecting to the upstream. |
| `ingress.pomerium.io/tls_custom_ca_secret` | Name of Kubernetes `tls` Secret containing a custom [CA certificate][`tls_custom_ca_secret`] for the upstream. |
| `ingress.pomerium.io/tls_downstream_client_ca_secret` | Name of Kubernetes `tls` Secret containing a [Client CA][client-certificate-authority] for validating downstream clients. |
| `ingress.pomerium.io/identity_provider_secret` | Name of Kubernetes `opaque` Secret containing `client_id` and `client_secret` to use for the route. |

### Set authorization policy

The `ingress.pomerium.io/policy` annotation allows you to build an authorization policy and apply it to a route. To build your authorization policy, apply [Pomerium Policy Language (PPL)](https://www.pomerium.com/docs/internals/ppl.md) inside a YAML or JSON block (as strings).

#### Ingress authorization policy examples

### Deterministic Matching

If you have Ingresses with potentially overlapping host/path combinations, Pomerium maintains the following order that would match an incoming request:

1. Ascending by `host`.
2. Descending by `path`.
3. Descending by `regex`.
4. Descending by `prefix`.

This sorting order helps ensure that more restrictive routes for specific paths and regular expressions are applied correctly.

### Route Naming

By default, Pomerium generates route names based on the Ingress namespace, name, and host. You can customize the human-readable route name using the `name` annotation:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: production
  annotations:
    ingress.pomerium.io/name: 'Production App'
    ingress.pomerium.io/policy: |
      allow:
        and:
          - domain:
              is: company.com
spec:
  ingressClassName: pomerium
  rules:
    - host: app.company.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app-service
                port:
                  number: 80
```

In this example:

- The route will display as "Production App" in Pomerium's UI and logs
- The unique identifier for monitoring and stats remains based on the Ingress resource details

### Regular Expressions Path Matching

You can use a [re2 regular expression] to create an Ingress that matches multiple paths.

1. Set the `path_regex` annotation to `"true"`
2. Set `pathType` to `ImplementationSpecific`
3. Set `path` to an re2 expression matching the full path. It must include the `^/` prefix and `$` suffix. Any query strings should be removed.

Check out [this example expression](https://regex101.com/r/IBVUKT/1/) at [regex101.com] for a more detailed explanation and example paths, both matching and not.

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/issuer: example-issuer
    ingress.pomerium.io/policy: |
      allow:
        and:
        - domain:
            is: exampledomain.com
    ingress.pomerium.io/path_regex: 'true'
  name: example
spec:
  ingressClassName: pomerium
  rules:
    - host: example.localhost.pomerium.io
      http:
        paths:
          - backend:
              service:
                name: example
                port:
                  name: http
            path: ^/(admin|superuser)/.*$
            pathType: ImplementationSpecific
  tls:
    - hosts:
        - example.localhost.pomerium.io
      secretName: example-tls
```

### Unsupported features

The following Ingress features are not supported:

- [Default Backend](https://kubernetes.io/docs/concepts/services-networking/ingress/#default-backend)
- [Resource Backend](https://kubernetes.io/docs/concepts/services-networking/ingress/#resource-backend)

## Services

Each Ingress should be backed by a Service. Pomerium supports certain extensions while communicating to Kubernetes services, beyond plaintext HTTP interaction via Service Load Balancer.

### Native SSH

Pomerium can directly proxy SSH connections. See [Native SSH Access](https://www.pomerium.com/docs/capabilities/native-ssh-access.md) for information about this capability.

To configure a native SSH route, you will need to:

- Set the `ingress.pomerium.io/ssh_upstream` annotation to `'true'`.
- Set the [`pathType`](https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types) to `ImplementationSpecific`.
- Do not set a `path`.

For example:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ssh-example
  annotations:
    ingress.pomerium.io/ssh_upstream: 'true'
spec:
  ingressClassName: pomerium
  rules:
    - host: 'my-ssh-host'
      http:
        paths:
          - pathType: ImplementationSpecific
            backend:
              service:
                name: ssh-service
                port:
                  name: ssh
```

This corresponds to a route with a From URL of `ssh://my-ssh-host` in Pomerium Core.

### TCP/UDP Services

Pomerium is capable of creating secure connections to services like SSH, Databases, and more by creating a TCP or UDP tunnel to the service with a local client.

The example route below defines a route providing a tunneled TCP connection to an upstream service listening for non-web traffic. Pomerium provides [command line and GUI](https://www.pomerium.com/docs/deploy/clients.md) clients to interact with the TCP services.

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tcp-example
  annotations:
    ingress.pomerium.io/tcp_upstream: 'true'
spec:
  ingressClassName: pomerium
  rules:
    - host: 'tcp.localhost.pomerium.io'
      http:
        paths:
          - pathType: ImplementationSpecific
            backend:
              service:
                name: tcp-service
                port:
                  name: app
```

The important points to note in this example:

- The annotation `ingress.pomerium.io/tcp_upstream` is set to `'true'`,
- `spec.rules.[].http.paths.[].path` is omitted,
- `spec.rules.[].http.paths.[].pathType` is set to `ImplementationSpecific`,
- `spec.rules.[].host` and `spec.rules.[].paths.[].backend.service.port.name/number` together define the address used when connecting to the route using the [Pomerium Desktop or CLI clients](https://www.pomerium.com/docs/deploy/clients.md),
- You may apply standard access control annotations to define access restrictions to the service.

Unlike a standalone Pomerium configuration, you may not create multiple TCP or UDP routes using the same hostname with different ports. This limitation was made to avoid confusion, and because additional configuration parameters, such as the Ingress resource, do not allow passing port numbers in the `spec.rules.host` parameter.

### Service Proxy

For performance reasons, by default, Pomerium routes traffic directly to the referenced Service's Endpoints, bypassing Kubernetes service proxy. If you would like to disable this behavior, i.e. you are deploying Pomerium inside a service mesh such as Istio, set the following annotation to the Ingress:

```yaml
ingress.pomerium.io/service_proxy_upstream: 'true'
```

### Load Balancing

Unless you disabled direct traffic to Endpoints, Pomerium would load balance the requests to the upstream endpoints. See the [Load Balancing](https://www.pomerium.com/docs/capabilities/routing.md) guide for details, and use relevant Ingress annotations to fine tune load balancing and health checks.

```yaml
ingress.pomerium.io/lb_policy: 'lb_policy_option'
```

The `lb_policy` has the following options:

| **Load Balancer Policy options** |
| :-- |
| [`ROUND_ROBIN`](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#weighted-round-robin) (each pod has an equal weight that cannot be customized) |
| [`RING_HASH`](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#ring-hash) (may be further configured using [`ring_hash_lb_config`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-ringhashlbconfig) option) |
| [`LEAST_REQUEST`](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#weighted-least-request) (may be further configured using [`least_request_lb_config`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-msg-config-cluster-v3-cluster-leastrequestlbconfig)) |
| [`RANDOM`](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#random) |
| [`MAGLEV`](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#maglev) (may be further configured using [`maglev_lb_config`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-msg-config-cluster-v3-cluster-maglevlbconfig) option) |

You can further configure `lb_policy` with the following OPTIONAL annotations:

| **Annotation name** | **Type** | **Usage** |
| :-- | :-- | :-- |
| [`least_request_lb_config`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-msg-config-cluster-v3-cluster-leastrequestlbconfig) | `object` | **optional** |
| [`ring_hash_lb_config`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-ringhashlbconfig) | `object` | **optional** |
| [`maglev_lb_config`](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-msg-config-cluster-v3-cluster-maglevlbconfig) | `object` | **optional** |

For example:

```yaml
ingress.pomerium.io/lb_policy: LEAST_REQUEST
ingress.pomerium.io/least_request_lb_config: '{"choice_count": 2}'
```

See [Envoy documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-enum-config-cluster-v3-cluster-lbpolicy) for more details.

Kubernetes has its own concept of [readiness](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes) for the endpoints. If a pod fails Readiness probe, its Endpoint is removed. Pomerium would detect this change and stop serving traffic to that upstream endpoint. You may use both Pomerium and Kubernetes health checks together.

### Upstream mutual TLS

The Ingress spec assumes that all communications to the upstream service are sent in plaintext. However, you can configure both Pomerium and the upstream server to mutually authenticate each other with TLS certificates. This configuration secures communication between Pomerium and the upstream endpoint over TLS.

Annotate your Ingress with

```yaml
ingress.pomerium.io/secure_upstream: 'true'
```

Additional TLS certificates may be supplied by creating a Kubernetes secret(s) in the same namespaces as the Ingress resource. Please note that we do not support file paths or embedded secret references.

- [`ingress.pomerium.io/tls_client_secret`](https://www.pomerium.com/docs/reference/routes/tls.md#tls-client-certificate)
- [`ingress.pomerium.io/tls_custom_ca_secret`](https://www.pomerium.com/docs/reference/routes/tls.md#tls-custom-certificate-authority)
- [`ingress.pomerium.io/tls_downstream_client_ca_secret`](#set-ingress-annotations)

Please note that the referenced `tls_client_secret` must be a [TLS Kubernetes secret](https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets). `tls_custom_ca_secret` and `tls_downstream_client_ca_secret` referenced Secrets must contain `ca.crt` key containing a .PEM encoded (base64-encoded DER format) public certificate.

### External Services

You may refer to external services by defining a [Service](https://kubernetes.io/docs/concepts/services-networking/service/) with `externalName`.

I.e. if you have `https://my-existing-service.corp.com`:

```yaml
apiVersion: v1
kind: Service
metadata:
  name: external
spec:
  type: ExternalName
  externalName: 'my-existing-service.corp.com'
  ports:
    - protocol: TCP
      name: https
      port: 443
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: external
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod-http
    ingress.pomerium.io/secure_upstream: 'true'
    ingress.pomerium.io/policy: |
      - allow:
          and:
            - domain:
                is: pomerium.com
spec:
  ingressClassName: pomerium
  tls:
    - hosts:
        - 'external.localhost.pomerium.io'
      secretName: external-localhost-pomerium.io
  rules:
    - host: 'external.localhost.pomerium.io'
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: external
                port:
                  name: https
```

## TLS Certificates

Pomerium expects TLS (HTTPS) for all routes created from the `Ingress` objects.

HTTP requests would be automatically redirected to the HTTPS port.

Pomerium certificates may be supplied individually per-Ingress via `spec.tls`, defined globally in the CRD via [`certificates`](https://www.pomerium.com/docs/reference/certificates.md), or both.

### `spec.tls`

Pomerium load use certificates referenced by the `spec.tls` section of the `Ingress`. For more information, see the [TLS](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls) section of the Ingress API documentation.

### Global Certs

You may provide [`certificates`](https://www.pomerium.com/docs/reference/certificates.md) as part of the global Pomerium configuration. This may be useful if you i.e. have a wildcard certificate.

### cert-manager Integration

Pomerium Ingress Controller can use [cert-manager](https://cert-manager.io/) to automatically provision certificates. These may come from the [ingress-shim](https://cert-manager.io/docs/usage/ingress/) or explicitly configured [`Certificate` resources](https://cert-manager.io/docs/usage/certificate/).

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/issuer: example-issuer
    ingress.pomerium.io/policy: '[{"allow":{"and":[{"email":{"is":"user@exampledomain.com"}}]}}]'
  name: example
spec:
  ingressClassName: pomerium
  rules:
    - host: example.localhost.pomerium.io
      http:
        paths:
          - backend:
              service:
                name: example
                port:
                  name: http
            path: /
            pathType: Prefix
  tls:
    - hosts:
        - example.localhost.pomerium.io
      secretName: example-tls
```

To use [HTTP01 Challenges](https://cert-manager.io/docs/configuration/acme/http01/) with your [Issuer](https://cert-manager.io/docs/concepts/issuer/), configure the solver class to match the Ingress Controller. The Pomerium Ingress Controller will automatically configure policy to facilitate the HTTP01 challenge:

```yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: example-issuer
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: example-issuer-account-key
    solvers:
      - http01:
          ingress:
            class: pomerium
```

## Metrics

Pomerium [exposes](https://www.pomerium.com/docs/deploy/k8s/install.md#metrics) a number of Prometheus style metrics that you may use to monitor your Ingress.

In order to filter out metrics for a particular Ingress, use `envoy_cluster_name` metric label, that has a `ingressnamespace-ingressname-host-domain-com` format.

See [Envoy Cluster Stats](https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats) for details on individual metrics.

## Troubleshooting

### View Event History

Pomerium Ingress Controller will add **events** to the Ingress objects as it processes them, and updates the status section of [Pomerium CRD](https://www.pomerium.com/docs/deploy/k8s/reference.md).

```bash
kubectl describe pomerium/global
```

```log
Name:         global
Namespace:
Labels:       <none>
Annotations:  <none>
API Version:  ingress.pomerium.io/v1
Kind:         Pomerium
Metadata:
  Creation Timestamp:  2022-07-14T21:43:08Z
  Generation:          5
  Resource Version:  1507973
  UID:               9c7e56ab-e74c-492c-945d-5db1cd6582b0
Spec:
  Authenticate:
    URL:  https://login.localhost.pomerium.io
  Certificates:
    pomerium/wildcard-localhost
  Identity Provider:
    Provider:  google
    Secret:    pomerium/idp
  Secrets:     pomerium/bootstrap
  Storage:
    Postgres:
      Secret:  pomerium/postgres
Status:
  Ingress:
    pomerium/httpbin:
      Observed At:          2022-07-29T13:01:37Z
      Observed Generation:  1
      Reconciled:           true
  Settings Status:
    Observed At:          2022-07-27T18:44:43Z
    Observed Generation:  5
    Reconciled:           true
Events:
  Type    Reason   Age   From              Message
  ----    ------   ----  ----              -------
  Normal  Updated  43m   pomerium-ingress  pomerium/httpbin: config updated
```

Additionally, events are posted to the individual `Ingress` objects.

```bash
kubectl describe ingress/my-ingress
```

```log
Events:
  Type    Reason   Age   From              Message
  ----    ------   ----  ----              -------
  Normal  Updated  18s   pomerium-ingress  updated pomerium configuration
```

If an error occurs, it may be reflected in the events:

```log
Events:
  Type     Reason       Age                 From              Message
  ----     ------       ----                ----              -------
  Normal   Updated      5m53s               pomerium-ingress  updated pomerium configuration
  Warning  UpdateError  3s                  pomerium-ingress  upsert routes: parsing ingress: annotations: applying policy annotations: parsing policy: invalid rules in policy: unsupported conditional "maybe", only and, or, not, nor and action are allowed
```

### Logs

```console
kubectl logs deployment/pomerium
```

Pomerium assigns a unique `request-id` that is also set in the response headers. Filter by `request-id` to see access and authorization logs for a particular request.

Pomerium produces an access log entry for each request. Filter by `"service":"envoy"`.

Pomerium performs authorization check for each and every request. Filter by `"service":"authorize"`. See [Authorization Log Keys](https://www.pomerium.com/docs/capabilities/audit-logs.md#authorization-log-keys).

### HSTS

If your domain has [HSTS] enabled and you visit an endpoint while Pomerium is using the self-signed bootstrap certificate or a LetsEncrypt staging certificate (before cert-manager has provisioned a production certificate), the untrusted certificate may be pinned in your browser and would need to be reset. See [this article](https://www.ssl2buy.com/wiki/how-to-clear-hsts-settings-on-chrome-firefox-and-ie-browsers) for more information.

## More Information

For more information on the Pomerium Ingress Controller or the Kubernetes concepts discussed, see:

- [Ingress (Kubernetes Docs)](https://kubernetes.io/docs/concepts/services-networking/ingress/)
- [Pomerium Kubernetes Ingress Controller (code repository)](https://github.com/pomerium/ingress-controller)

## MCP (Model Context Protocol) Configuration

The Pomerium Ingress Controller supports Model Context Protocol (MCP) through specific annotations. MCP routes can be configured as either servers or clients, with additional configuration options for OAuth2 upstream authentication.

### MCP Annotations

All MCP annotations use the `ingress.pomerium.io/` prefix.

#### Basic MCP Configuration

**Server Mode:**

- `ingress.pomerium.io/mcp_server: "true"` - Configures the route as an MCP server. Optional if other MCP server annotations are present.
- `ingress.pomerium.io/mcp_server_max_request_bytes` - Sets the maximum request body size for MCP server routes (optional)
- `ingress.pomerium.io/mcp_server_path` - Sets the path property for MCP server routes, used when returning the server URL in the .mcp/routes endpoint. Defaults to "/" if not specified (optional)

**Client Mode:**

- `ingress.pomerium.io/mcp_client: "true"` - Configures the route as an MCP client

A route cannot be configured as both MCP server and client.

#### OAuth2 Configuration (Server Mode Only)

For secure upstream authentication in MCP server mode, OAuth2 credentials must be stored in Kubernetes secrets and referenced via annotations:

- `ingress.pomerium.io/mcp_server_upstream_oauth2_secret` - References a Kubernetes secret containing OAuth2 credentials
- `ingress.pomerium.io/mcp_server_upstream_oauth2_token_url` - OAuth2 token URL for upstream authentication (optional)
- `ingress.pomerium.io/mcp_server_upstream_oauth2_scopes` - Comma-separated list of OAuth2 scopes (optional)

The secret referenced by `mcp_server_upstream_oauth2_secret` must be of type `Opaque` and can contain:

- `client_id` - OAuth2 client ID (optional if `client_secret` is provided)
- `client_secret` - OAuth2 client secret (optional if `client_id` is provided)

At least one of `client_id` or `client_secret` must be present in the secret.

### MCP Examples

The `mcp_server` annotation can be omitted when other MCP server-specific annotations (like `mcp_server_max_request_bytes`) are present. The presence of server-specific annotations automatically enables MCP server mode.

#### Basic MCP Server

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mcp-server
  annotations:
    ingress.pomerium.io/mcp_server: 'true'
spec:
  ingressClassName: pomerium
  rules:
    - host: mcp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: mcp-server-service
                port:
                  number: 8080
  tls:
    - hosts:
        - mcp.example.com
      secretName: mcp-tls
```

#### MCP Server with Custom Path

If your MCP client application makes use of [MCP Routes Enumeration](https://www.pomerium.com/docs/capabilities/mcp/reference.md#listing-available-mcp-servers) to determine the exact URL the MCP Streaming HTTP is exposed at, you may specify it with this annotation.

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mcp-server-with-path
  annotations:
    ingress.pomerium.io/mcp_server: 'true'
    ingress.pomerium.io/mcp_server_path: '/api/v1/mcp'
spec:
  ingressClassName: pomerium
  rules:
    - host: mcp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: mcp-server-service
                port:
                  number: 8080
  tls:
    - hosts:
        - mcp.example.com
      secretName: mcp-tls
```

#### MCP Server with OAuth2 Authentication

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: mcp-oauth2-credentials
type: Opaque
data:
  client_id: Y2xpZW50LWlk # base64 encoded "client-id"
  client_secret: Y2xpZW50LXNlY3JldA== # base64 encoded "client-secret"
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mcp-server-oauth2
  annotations:
    ingress.pomerium.io/mcp_server: 'true'
    ingress.pomerium.io/mcp_server_max_request_bytes: '1048576'
    ingress.pomerium.io/mcp_server_upstream_oauth2_secret: 'mcp-oauth2-credentials'
    ingress.pomerium.io/mcp_server_upstream_oauth2_token_url: 'https://auth.example.com/token'
    ingress.pomerium.io/mcp_server_upstream_oauth2_scopes: 'read,write,admin'
spec:
  ingressClassName: pomerium
  rules:
    - host: mcp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: mcp-server-service
                port:
                  number: 8080
  tls:
    - hosts:
        - mcp.example.com
      secretName: mcp-tls
```

#### MCP Server without explicit annotation

When MCP server-specific annotations are used, the `mcp_server` annotation can be omitted:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mcp-server-implicit
  annotations:
    # mcp_server annotation omitted - implicitly enabled by server-specific annotation
    ingress.pomerium.io/mcp_server_max_request_bytes: '2097152'
spec:
  ingressClassName: pomerium
  rules:
    - host: mcp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: mcp-server-service
                port:
                  number: 8080
```

#### MCP Client

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mcp-client
  annotations:
    ingress.pomerium.io/mcp_client: 'https://mcp-client.example.com'
spec:
  ingressClassName: pomerium
  rules:
    - host: mcp-client.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: mcp-client-service
                port:
                  number: 8080
  tls:
    - hosts:
        - mcp-client.example.com
      secretName: mcp-client-tls
```

### MCP Security Considerations

- OAuth2 credentials are handled securely through Kubernetes secrets, preventing exposure in annotations
- The secret must be in the same namespace as the ingress resource
- Use appropriate RBAC controls to limit access to OAuth2 credential secrets

[`tls_custom_ca_secret`]: https://www.pomerium.com/docs/reference/routes/tls.md#tls-custom-certificate-authority

[client-certificate-authority]: https://www.pomerium.com/docs/reference/certificates.md

[hsts]: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security

[re2 regular expression]: https://github.com/google/re2/wiki/Syntax

[regex101.com]: https://regex101.com

[tls_client_certificate]: https://www.pomerium.com/docs/reference/routes/tls.md#tls-client-certificate
