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

# Migrate from ingress-nginx to the ngrok Kubernetes Operator

> Move your Kubernetes Ingress traffic from the retired ingress-nginx controller to the ngrok Kubernetes Operator without downtime.

The Kubernetes project [retired ingress-nginx](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/) on March 24, 2026.
Existing installations keep running, but they no longer receive releases, bug fixes, or security patches.

This guide moves an existing `Ingress` to the [ngrok Kubernetes Operator](https://ngrok.com/docs/gateway/k8s.md) without downtime.
You run both controllers side by side, point your DNS at ngrok, and then remove ingress-nginx.

> [!NOTE]
>
> ingress-nginx (`kubernetes/ingress-nginx`) is a different project from F5's [NGINX Ingress Controller](https://github.com/nginx/kubernetes-ingress), which F5 still maintains.
> ingress-nginx annotations start with `nginx.ingress.kubernetes.io/`, while F5's start with `nginx.org/` or `nginx.com/`.

To check whether your cluster runs ingress-nginx, list its pods:

```bash theme={null}
kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx
```

## What you'll need

* An ngrok account on the [Pay-as-you-go plan](https://ngrok.com/pricing), which you need to bring your own domain.
* A cluster where ingress-nginx serves at least one `Ingress` for a domain you control.
* Access to your domain's DNS records.
* [`kubectl`](https://kubernetes.io/docs/reference/kubectl/) and [Helm](https://helm.sh/docs/intro/install/) 3.0.0 or later.

## 1. Install the ngrok Kubernetes Operator

### Add the ngrok Helm chart

```bash theme={null}
helm repo add ngrok https://charts.ngrok.com
helm repo update
```

> [!NOTE]
>
> Whenever you want to update the Operator or install a new version, you must run `helm repo update` to fetch the latest charts.

### Get your ngrok API key and authtoken

You can get both these from the ngrok dashboard:

* Your [authtoken](https://dashboard.ngrok.com/get-started/your-authtoken)
* An [API key](https://dashboard.ngrok.com/api-keys)

The ngrok Kubernetes Operator provisions these as a Kubernetes secret, then uses the authtoken to create tunnels.
The Operator uses your API key to manage resources via the [ngrok API](https://ngrok.com/docs/api.md).

> [tab] Linux
>
> ```bash theme={null}
> export NGROK_AUTHTOKEN=your copied auth token goes here
> export NGROK_API_KEY=your copied api key goes here
> ```

> [tab] PowerShell
>
> ```powershell theme={null}
> $env:NGROK_AUTHTOKEN = "your copied auth token goes here"
> $env:NGROK_API_KEY   = "your copied api key goes here"
> ```

### Install the Operator

Install the Operator into the default `ngrok-operator` namespace.
You can change this in the commands below based on your cluster configuration, but by default, the Operator works with `Gateway` and routing resources in all namespaces.

You can choose between passing your credentials directly with Helm or creating a Kubernetes `Secret` resource that you pass to the Helm chart.
The simple method only takes one command, but with the secure method, you:

* Prevent anyone with `kubectl` access to the cluster from viewing your API key and authtoken with `helm get values`.
* Integrate more cleanly with infrastructure as code (IaC) tools and processes.

> [tab] Simple
>
> ```bash theme={null}
> helm install ngrok-operator ngrok/ngrok-operator \
> --namespace ngrok-operator \
> --create-namespace \
> --set credentials.apiKey=$NGROK_API_KEY \
> --set credentials.authtoken=$NGROK_AUTHTOKEN
> ```

> [tab] More secure
>
> First, prepare new environment variables.
>
> ```bash theme={null}
> export ENCODED_NGROK_AUTHTOKEN=$(echo -n "$NGROK_AUTHTOKEN" | base64)
> export ENCODED_NGROK_API_KEY=$(echo -n "$NGROK_API_KEY" | base64)
> ```
>
> Create the namespace and apply a `Secret` resource with your encoded credentials.
>
> ```bash theme={null}
> kubectl create namespace ngrok-operator
> kubectl apply -f -<<EOF
> apiVersion: v1
> kind: Secret
> metadata:
>   name: ngrok-operator-credentials
>   namespace: ngrok-operator
> data:
>   API_KEY: "$ENCODED_NGROK_API_KEY"
>   AUTHTOKEN: "$ENCODED_NGROK_AUTHTOKEN"
> EOF
> ```
>
> Next, install the Operator and reference the `Secret` resource.
>
> ```bash theme={null}
> helm install ngrok-operator ngrok/ngrok-operator \
> --namespace ngrok-operator \
> --create-namespace \
> --set credentials.secret.name=ngrok-operator-credentials
> ```

## 2. Register your domain

Choose how ngrok gets a TLS certificate for your domain during the migration.

### Option 1: Upload your current certificate (no downtime)

ngrok can't issue an automated certificate until your DNS points at ngrok.
Uploading the certificate that ingress-nginx serves today lets ngrok serve valid HTTPS from the moment you change DNS.

If cert-manager issues your certificate, export it from the `Secret` named in your `Ingress`'s `spec.tls`:

```bash theme={null}
kubectl get secret $TLS_SECRET -n $NAMESPACE -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt
kubectl get secret $TLS_SECRET -n $NAMESPACE -o jsonpath='{.data.tls\.key}' | base64 -d > tls.key
```

Treat `tls.key` as a secret, and make sure the certificate won't expire before you finish the migration.

1. Upload `tls.crt` and `tls.key` on the [**TLS Certificates**](https://dashboard.ngrok.com/tls-certs) page of the dashboard.
2. Add your domain on the [**Domains**](https://dashboard.ngrok.com/domains) page and select the certificate you uploaded.
3. Don't change your DNS records yet.

Do this before you create the ngrok `Ingress`.
The Operator uses a domain that already exists in your account instead of reserving a new one.

### Option 2: Use an automated certificate (brief certificate warning)

Skip the dashboard.
When you create the ngrok `Ingress` in the next step, the Operator reserves your domain with an automated certificate from Let's Encrypt.

ngrok requests that certificate only after you change DNS.
Until ngrok issues it, usually within a few minutes, clients see a certificate name-mismatch warning.
See [TLS certificate provisioning](https://ngrok.com/docs/gateway/domains/tls-certificates.md#expected-behavior-while-a-certificate-is-provisioning) for details.

## 3. Create a parallel ngrok Ingress

Copy your existing `Ingress` to a new one with a different name and `ingressClassName: ngrok`.
Leave the original in place so ingress-nginx keeps serving traffic until you change DNS.

Drop `spec.tls` and any `cert-manager.io/*` annotations from the copy.
ngrok terminates TLS with your domain's certificate, and the Operator ignores `spec.tls`.

```yaml title="ngrok-ingress.yaml" theme={null}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ngrok
  namespace: default
spec:
  ingressClassName: ngrok
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
```

If your original `Ingress` has `nginx.ingress.kubernetes.io/*` annotations, translate them with the [annotation table](#map-ingress-nginx-annotations-to-ngrok) before you continue.

Apply the manifest and check its address:

```bash theme={null}
kubectl apply -f ngrok-ingress.yaml
kubectl get ingress my-app-ngrok -n default
```

After a few seconds, the `ADDRESS` column shows a hostname ending in `ngrok-cname.com`.
This guide calls it `$CNAME_TARGET`.
If the address stays empty, see [troubleshooting](https://ngrok.com/docs/gateway/k8s/guides/troubleshooting.md).

Test the ngrok path before you change DNS by sending a request straight to `$CNAME_TARGET`:

```bash theme={null}
curl --connect-to app.example.com:443:$CNAME_TARGET:443 https://app.example.com/
```

If you chose an automated certificate, add `--insecure`, because ngrok can't issue your certificate until DNS points at it.

## 4. Cut over DNS

1. A day ahead, lower the TTL on your domain's current DNS record so the change propagates quickly.
2. Replace the record with a CNAME that points to `$CNAME_TARGET`.
   For an apex domain such as `example.com`, use an ALIAS, ANAME, or flattened CNAME record instead, as described in [apex domains](https://ngrok.com/docs/gateway/domains/custom-domains.md#apex-domains).
3. Confirm requests arrive in [Traffic Inspector](https://dashboard.ngrok.com/traffic-inspector).

If you run [ExternalDNS](https://github.com/kubernetes-sigs/external-dns), it can publish the ngrok `Ingress`'s address as soon as the Operator sets it.
Limit ExternalDNS to the `nginx` ingress class (for example, `--ingress-class=nginx`) until you're ready to cut over, or let it make the DNS change for you at this step.

## 5. Switch to automated TLS

If you uploaded a certificate, switch your domain to automated TLS certificates on the [**Domains**](https://dashboard.ngrok.com/domains) page as soon as DNS points at ngrok.
ngrok keeps serving your uploaded certificate until the automated one is ready, so clients don't notice the change.

Afterward, delete the uploaded certificate from the [**TLS Certificates**](https://dashboard.ngrok.com/tls-certs) page.
An uploaded certificate costs \$0.27 per domain per active hour, and ngrok doesn't renew it for you.
See [TLS certificates pricing](https://ngrok.com/docs/gateway/domains/tls-certificates.md#tls-certificates-pricing).

## 6. Remove ingress-nginx

Once your old DNS record's TTL has passed and no traffic reaches ingress-nginx, delete the original `Ingress`:

```bash theme={null}
kubectl delete ingress my-app -n default
```

When no `Ingress` uses the `nginx` class anymore, uninstall ingress-nginx.
If you installed it with Helm, run `helm uninstall ingress-nginx --namespace ingress-nginx`.

## Map ingress-nginx annotations to ngrok

The ngrok Operator doesn't read `nginx.ingress.kubernetes.io/*` annotations.
Most of them map to [Traffic Policy](https://ngrok.com/docs/gateway/traffic-policy.md) actions, which you put in an `NgrokTrafficPolicy` resource and attach to your `Ingress` with the `ngrok.com/traffic-policy` annotation.
The table omits the `nginx.ingress.kubernetes.io/` prefix.

| ingress-nginx annotation | ngrok equivalent | Guide |
| - | - | - |
| `kubernetes.io/ingress.class: nginx` | `spec.ingressClassName: ngrok` | [Using Ingresses](https://ngrok.com/docs/gateway/k8s/guides/using-ingresses.md) |
| `rewrite-target` | `url-rewrite` action | [Rewrite URL](https://ngrok.com/docs/gateway/k8s/guides/how-to/rewrite-url.md) |
| `permanent-redirect`, `temporal-redirect`, `app-root`, `from-to-www-redirect` | `redirect` action | [Redirects](https://ngrok.com/docs/gateway/k8s/guides/how-to/redirects.md) |
| `ssl-redirect`, `force-ssl-redirect` | Not needed for the `Ingress`, because its endpoints only serve HTTPS. To redirect plain HTTP, create a [`CloudEndpoint`](https://ngrok.com/docs/gateway/k8s/crds/cloudendpoint) with an `http://` URL and a `redirect` action. | [Redirects](https://ngrok.com/docs/gateway/k8s/guides/how-to/redirects.md) |
| `limit-rps`, `limit-rpm` | `rate-limit` action with `bucket_key: [conn.client_ip]`. The shortest window is `60s`, so `limit-rps: "10"` becomes `capacity: 600` with `rate: 60s`. | [Rate limiting](https://ngrok.com/docs/gateway/k8s/guides/how-to/rate-limiting.md) |
| `auth-type: basic`, `auth-secret`, `auth-realm` | `basic-auth` action | [Basic auth](https://ngrok.com/docs/gateway/k8s/guides/how-to/basic-auth.md) |
| `auth-url`, `auth-signin` | No direct equivalent. Use the built-in `oauth`, `oidc`, or `jwt-validation` action. | [OAuth](https://ngrok.com/docs/gateway/k8s/guides/how-to/oauth.md), [OIDC](https://ngrok.com/docs/gateway/k8s/guides/how-to/oidc.md), [JWT validation](https://ngrok.com/docs/gateway/k8s/guides/how-to/jwt-validation.md) |
| `whitelist-source-range`, `denylist-source-range` | `restrict-ips` action | [Restrict IPs](https://ngrok.com/docs/gateway/k8s/guides/how-to/restrict-ips.md) |
| `custom-headers`, `x-forwarded-prefix`, header changes in `configuration-snippet` | `add-headers` and `remove-headers` actions | [Manipulate headers](https://ngrok.com/docs/gateway/k8s/guides/how-to/manipulate-headers.md) |
| `auth-tls-secret`, `auth-tls-verify-client` | `terminate-tls` action with `mutual_tls_certificate_authorities` | [Terminate TLS](https://ngrok.com/docs/gateway/k8s/guides/how-to/terminate-tls.md) |
| `backend-protocol: "HTTPS"` | `ngrok.com/app-protocols` annotation on the `Service` | [Ingress HTTPS upstreams](https://ngrok.com/docs/gateway/k8s/guides/using-ingresses.md#ingress-https-upstreams) |
| `proxy-ssl-secret`, `proxy-ssl-verify` | Upstream TLS or mutual TLS | [Upstream TLS](https://ngrok.com/docs/gateway/k8s/guides/how-to/upstream-tls.md) |
| `ssl-passthrough` | TLS endpoint that forwards without terminating | [TLS routing](https://ngrok.com/docs/gateway/k8s/guides/how-to/tls-routing.md) |
| `canary`, `canary-weight`, `canary-by-header` | Traffic Policy rules that forward to internal endpoints | [Canary deployments](https://ngrok.com/docs/gateway/examples/canary-deployments.md) |
| `enable-modsecurity`, `enable-owasp-core-rules` | `owasp-crs-request` and `owasp-crs-response` actions | [OWASP CRS request action](https://ngrok.com/docs/gateway/traffic-policy/actions/owasp-crs-request.md) |
| `use-regex` | The Operator treats `ImplementationSpecific` paths as `Prefix`. Use `Prefix` or `Exact` paths, or match with a regular expression in a rule's `expressions`. | [Expressions](https://ngrok.com/docs/gateway/traffic-policy/concepts/expressions.md) |
| `server-alias` | Add a `rules[].host` entry for each hostname | [Using Ingresses](https://ngrok.com/docs/gateway/k8s/guides/using-ingresses.md) |

Annotations that tune NGINX itself, such as timeouts, buffer sizes, and `server-snippet`, have no one-to-one equivalent.
Review what each one does for your app before you cut over.

### Example: IP allowlist and rate limit

This ingress-nginx `Ingress` allows one IP range and limits each client to 60 requests per minute:

```yaml theme={null}
metadata:
  annotations:
    nginx.ingress.kubernetes.io/whitelist-source-range: "203.0.113.0/24"
    nginx.ingress.kubernetes.io/limit-rpm: "60"
```

The ngrok version puts both behaviors in one `NgrokTrafficPolicy` and attaches it to the parallel `Ingress`:

```yaml title="ngrok-ingress.yaml" theme={null}
apiVersion: ngrok.k8s.ngrok.com/v1alpha1
kind: NgrokTrafficPolicy
metadata:
  name: my-app-policy
  namespace: default
spec:
  policy:
    on_tcp_connect:
      - actions:
          - type: restrict-ips
            config:
              enforce: true
              allow:
                - 203.0.113.0/24
    on_http_request:
      - actions:
          - type: rate-limit
            config:
              name: 60 requests per minute per client
              algorithm: sliding_window
              capacity: 60
              rate: 60s
              bucket_key:
                - conn.client_ip
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ngrok
  namespace: default
  annotations:
    ngrok.com/traffic-policy: my-app-policy
spec:
  ingressClassName: ngrok
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
```

> [!NOTE]
>
> The `ngrok.com/traffic-policy` annotation applies to every request for the `Ingress`'s hostnames, and every `Ingress` that shares a hostname must use the same policy.
> If you split paths across several ingress-nginx `Ingress` resources to give them different annotations, add `expressions` such as `req.url.path.startsWith('/api')` to the policy's rules instead.
> See [using `NgrokTrafficPolicy` with `Ingress`](https://ngrok.com/docs/gateway/k8s/guides/using-ingresses#using-ngroktrafficpolicy-with-ingress).

> [!NOTE]
>
> Operator versions before 0.22 (Helm chart 0.24) read the older `k8s.ngrok.com/` annotation prefix.
> Newer versions accept both prefixes but prefer `ngrok.com/`.

## Use Gateway API instead

The ngrok Operator also implements [Gateway API](https://ngrok.com/docs/getting-started/kubernetes/gateway-api.md), the Kubernetes successor to `Ingress`.
If you'd rather move off `Ingress` during this migration, [ingress2gateway](https://github.com/kubernetes-sigs/ingress2gateway) can convert your ingress-nginx resources with `ingress2gateway print --providers=ingress-nginx`.
It translates some annotations, such as `rewrite-target`, into Gateway API filters, and warns about the ones it can't translate.

Make three changes to its output before you apply it:

1. Set `gatewayClassName: ngrok` on each `Gateway`.
   ingress2gateway names the class after your old ingress class.
2. Change each `Gateway` listener to `protocol: HTTPS` and `port: 443`.
   ingress2gateway writes HTTP listeners on port 80, and the Operator creates only an `http://` endpoint for them, so HTTPS requests fail with [ERR\_NGROK\_3200](https://ngrok.com/docs/errors/err_ngrok_3200).
   Leave the `port` in each `HTTPRoute`'s `backendRefs` unchanged, because it's your `Service`'s port.
3. Replace each `RegularExpression` path match with a `PathPrefix` or `Exact` match.
   ingress2gateway writes matches such as `(?i)/.*` for an `Ingress` that uses `rewrite-target` or `use-regex`.
   The Operator doesn't support regular expression matches and compares the value as a literal path prefix, so those routes return 404.

```yaml theme={null}
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: my-app
  namespace: default
spec:
  gatewayClassName: ngrok
  listeners:
    - name: app-example-com
      hostname: app.example.com
      protocol: HTTPS
      port: 443
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
  namespace: default
spec:
  parentRefs:
    - name: my-app
  hostnames:
    - app.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: my-app
          port: 80
```

If you already applied a `Gateway` with an HTTP listener, delete it and apply it again.
The Operator can't change an endpoint from `http://` to `https://` in place, so updating the listener fails with [ERR\_NGROK\_18005](https://ngrok.com/docs/errors/err_ngrok_18005).

The domain, DNS, and TLS steps in this guide stay the same.
See [using Gateway API](https://ngrok.com/docs/gateway/k8s/guides/using-gwapi.md) for how the Operator handles these resources.


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