Development
16 min read
48 views

Kubernetes-Native Tunneling: Declarative Ingress with Operators and CRDs

IT
InstaTunnel Team
Published by the InstaTunnel team | Editorial policy
Kubernetes-Native Tunneling: Declarative Ingress with Operators and CRDs

Quick answer

Kubernetes Native Tunneling: Webhook Relay & CRD Operators: webhook testing answer

For local webhook testing, run your app locally, expose it with a public HTTPS tunnel, and paste the stable callback URL into the provider dashboard.

How do I test webhooks on localhost?

Start your local server, open a public HTTPS tunnel to that port, configure the provider webhook URL, and inspect events in your local logs.

Why does a stable webhook URL matter?

Stable URLs prevent provider dashboards from needing manual callback updates every time you restart a tunnel.

Cloud-native teams describe infrastructure in YAML and let controllers make it true. Yet the way most developers get a private cluster onto the internet is still imperative: open a terminal, run a tunnel binary, leave it running.

A growing set of tools closes that gap. A controller inside your cluster watches Kubernetes objects (a custom resource, an Ingress, a Gateway API route, or an annotated Service), dials out to a provider’s edge network, and keeps the tunnel reconciled with what your manifests say. Delete the object and the tunnel goes away.

This guide covers what actually exists today, with working manifests, honest caveats, and the GitOps details that tend to bite. It also covers one project that used to be on every list of “Kubernetes tunnel operators” and no longer exists.


Why imperative tunnels don’t fit Kubernetes

Tools like ngrok’s CLI, localtunnel, or a hand-rolled SSH reverse proxy are fine for a ten-minute debugging session. In a cluster they have three problems:

  • No lifecycle. If a node drains or a pod is evicted, a tunnel running in a detached process or an unmanaged sidecar dies with it. Nothing in Kubernetes knows the tunnel exists, so nothing recreates it.
  • No source of truth. The tunnel lives in someone’s shell history, not in Git. The cluster and the repository drift apart.
  • No visibility. You can’t kubectl get a tunnel’s health.

Exposing every service the traditional way (a cloud LoadBalancer, static IPs, firewall rules) is expensive and often impossible on home labs, on-premises clusters, edge sites, and laptops behind NAT. That is the gap operator-managed tunnels fill.

How operator-managed tunnels work

The shared pattern is simple:

  1. An agent or operator runs inside your cluster.
  2. It opens an outbound connection to a provider’s edge, so no inbound firewall rule or public IP is needed.
  3. A controller watches Kubernetes objects and configures the provider through its API.
  4. If the connection drops or the object changes, the controller reconciles. If the object is deleted, it cleans up.

Under that shared pattern, tools differ in what you write:

Tool What you write Best for Caveat
Webhook Relay Operator WebhookRelayForward custom resource Receiving webhooks and API callbacks in a private cluster Webhooks only; last tagged release is 0.6.0 (Nov 2022)
Webhook Relay Ingress Controller Ingress-style resources (separate product) Bidirectional tunnels to services like Grafana or Prometheus Different product from the Operator
ngrok Kubernetes Operator AgentEndpoint / CloudEndpoint CRDs, Ingress, or Gateway API Full public ingress with edge policy (IP restrictions, rate limits) Needs an ngrok account and reserved domain
Tailscale Kubernetes Operator Standard Ingress with ingressClassName: tailscale, or annotated Service Private tailnet access, with optional public exposure via Funnel Funnel has fixed ports and bandwidth limits
Cloudflare Tunnel A cloudflared Deployment (official); community operators exist Public HTTP(S) ingress on Cloudflare’s network Official path is a plain Deployment, not a CRD
Pangolin (Newt) Helm chart Self-hosted tunneled ingress Newt is a Helm-deployed agent, not a CRD operator
KubeSail n/a n/a Discontinued

Webhook Relay: the webhook-shaped operator

The Webhook Relay Operator is built for one job: receiving webhooks and API requests (from GitHub, Stripe, Slack) inside a cluster that has no public IP or load balancer. Its README names on-premises, edge, and K3s deployments as target environments. You install it with Helm and describe public endpoints and forwarding destinations in a WebhookRelayForward resource.

Install it, passing the access token key and secret from your Webhook Relay account:

helm repo add webhookrelay https://charts.webhookrelay.com
helm repo update

helm upgrade --install webhookrelay-operator --namespace=default webhookrelay/webhookrelay-operator \
  --set credentials.key=$RELAY_KEY --set credentials.secret=$RELAY_SECRET

Then declare the forward. An inputs entry creates the public endpoint, and an outputs entry says where requests go. Without an output, you have an endpoint that forwards nowhere.

apiVersion: forward.webhookrelay.com/v1
kind: WebhookRelayForward
metadata:
  name: stripe-webhook-forwarder
  namespace: payment-services
spec:
  buckets:
    - name: k8s-operator
      inputs:
        - name: public-endpoint
          description: "Stripe webhook receiver"
          responseBody: "OK"
          responseStatusCode: 200
      outputs:
        - name: stripe-receiver
          lockPath: true
          destination: http://stripe-receiver.payment-services:8080/webhooks/stripe

If several teams share the operator, you can skip the Helm-level credentials and reference a Secret from each resource instead, using secretRefName (and optionally secretRefNamespace) in spec. The Secret holds key and secret values for a Webhook Relay access token.

The operator ensures the bucket, inputs, and outputs exist, emits Kubernetes events for the actions it takes, and writes status back to the resource. Read the public URL to hand to the webhook sender:

kubectl get webhookrelayforwards.forward.webhookrelay.com stripe-webhook-forwarder \
  -n payment-services -o 'jsonpath={.status.publicEndpoints[0]}'

Two practical notes:

  • It is the webhook product, not the general one. Webhook Relay’s docs describe a separate ingress controller as the recommended way to open bidirectional tunnels to services such as Grafana or Prometheus. Don’t expect the Operator to expose arbitrary services.
  • It is stable but quiet. The latest tagged release is 0.6.0 from November 2022. The vendor’s docs still recommend it for forwarding webhooks into a cluster, but pin your chart version and check the repository before adopting it for something critical.

Whatever the transport, verify the sender’s signature (for Stripe, the signature header) inside your application. The tunnel gets the request to you; it doesn’t prove who sent it.

ngrok Kubernetes Operator: CRDs, Ingress, and Gateway API

ngrok’s operator is the most flexible option on this list. It offers two native custom resources, AgentEndpoint and CloudEndpoint. It also translates standard Ingress and Gateway API resources into those same two types. ngrok states the operator is available to all users at no additional charge; you pay only for the resources it provisions.

helm repo add ngrok https://charts.ngrok.com
helm repo update

helm install ngrok-operator ngrok/ngrok-operator \
  --namespace ngrok-operator \
  --create-namespace \
  --set credentials.apiKey=$NGROK_API_KEY \
  --set credentials.authtoken=$NGROK_AUTHTOKEN

You’ll need an ngrok account and a reserved domain. The simplest exposure is a single AgentEndpoint:

apiVersion: ngrok.k8s.ngrok.com/v1alpha1
kind: AgentEndpoint
metadata:
  name: auth-service-endpoint
  namespace: dev
spec:
  url: https://YOUR_RESERVED_DOMAIN
  upstream:
    url: http://auth-service.dev:8080

Because endpoint policy is part of the resource, security can live in Git too. This adds an IP allowlist:

spec:
  url: https://YOUR_RESERVED_DOMAIN
  upstream:
    url: http://auth-service.dev:8080
  trafficPolicy:
    inline:
      on_http_request:
        - actions:
            - type: restrict-ips
              config:
                enforce: true
                allow:
                  - 203.0.113.10

For several services behind one hostname, ngrok pairs a public CloudEndpoint with internal AgentEndpoints and routes between them using the forward-internal Traffic Policy action. If you use the Gateway API, each Gateway listener hostname becomes a CloudEndpoint, and each upstream service referenced by an HTTPRoute becomes an internal AgentEndpoint. Request-mirror filters are not yet supported.

Tailscale Kubernetes Operator: standard Ingress, private by default

Tailscale’s operator takes a different approach: no tunnel-specific CRD is needed for basic exposure. You use a standard Ingress with ingressClassName: tailscale, and the operator creates proxy pods that forward traffic to your Service. A default install also creates a tailscale IngressClass and CRDs including ProxyClass, Connector, ProxyGroup, DNSConfig, and Recorder.

Install with Helm after creating an OAuth client and tags in the admin console:

helm repo add tailscale https://pkgs.tailscale.com/helmcharts
helm repo update

helm upgrade --install tailscale-operator tailscale/tailscale-operator \
  --namespace=tailscale --create-namespace \
  --set-string oauth.clientId="<OAuth client ID>" \
  --set-string oauth.clientSecret="<OAuth client secret>" \
  --wait

An Ingress on its own exposes the service only to your tailnet. To reach the public internet, you opt in with the tailscale.com/funnel annotation:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: auth-service-funnel
  namespace: dev
  annotations:
    tailscale.com/funnel: "true"
spec:
  ingressClassName: tailscale
  defaultBackend:
    service:
      name: auth-service
      port:
        number: 8080
  tls:
    - hosts:
        - auth-dev

The tls.hosts value becomes the MagicDNS name, for example auth-dev.<your-tailnet>.ts.net. Check kubectl get ingress and read the ADDRESS column.

Funnel comes with constraints you should design around:

  • It can only use DNS names under your tailnet’s ts.net domain.
  • It can only listen on ports 443, 8443, and 10000, and only over TLS.
  • Traffic is subject to bandwidth limits that you cannot configure.
  • Your tailnet policy must permit Funnel (a funnel node attribute), and MagicDNS and HTTPS must be enabled on the tailnet.

For production, Tailscale recommends high-availability mode: a ProxyGroup with multiple replicas rather than a single proxy pod. Also plan for certificate limits when creating many short-lived hostnames. The operator provisions certificates from Let’s Encrypt, and Tailscale’s docs list limits of 50 certificates per week for unique hostnames. The docs suggest a ProxyClass that uses Let’s Encrypt’s staging environment while testing.

One security property is worth knowing. Tailscale documents that Funnel relay servers forward the encrypted stream without decrypting it, and TLS is terminated on your own node.

Cloudflare Tunnel and Pangolin

Cloudflare Tunnel. Cloudflare’s official Kubernetes guide runs cloudflared as an ordinary Deployment authenticated with a tunnel token stored in a Secret. It recommends running cloudflared as its own Deployment, adjacent to your application deployments, so it scales independently. Replicas of the same tunnel share the load, and each replica can reach every Service in the cluster. Cloudflare advises against autoscaling cloudflared, because removing a replica breaks the connections it was carrying. If you want tunnels and DNS records as custom resources, community projects such as adyanth/cloudflare-operator provide Tunnel and TunnelBinding CRDs, but that project describes itself as alpha.

Pangolin. Pangolin is a self-hostable tunneled reverse proxy. Its Kubernetes agent, Newt, ships as a Helm chart (version 1.4.0 at the time of writing) and requires Kubernetes 1.30.14 or newer. Pangolin’s docs list Helm directly, Kustomize overlays, and GitOps tools (Argo CD and Flux) as the supported install methods, and include a Flux HelmRelease example for Newt. The Pangolin server chart itself is still published as a pre-release, so pin exact chart versions. Pangolin’s docs also say to keep Newt credentials in an existing Kubernetes Secret rather than committing them into Helm values.

What happened to KubeSail

Older versions of this topic (including an earlier draft of this article) held up KubeSail as the model of the pattern: a home-lab operator that watched standard Ingress resources and gave each one a public subdomain with TLS. KubeSail has shut down. Its homepage now carries a farewell notice, and community reports place the final shutdown of its hosted gateways around mid-September 2025. A message from the founders, reposted on Lemmy, recommends Tailscale Funnel and Cloudflare Tunnel for remote access.

If you find a guide that tells you to install the KubeSail agent, treat it as obsolete. The workflow it popularized (write a normal Ingress, get a public URL) lives on in the Tailscale operator above.

Minikube: three different things called “tunnel”

Testing OAuth callbacks or third-party webhooks against a local cluster is the classic use case, and Minikube’s own commands cause some confusion here:

  • minikube service opens NodePort services. Minikube’s docs show that with the Docker driver on some platforms it works by spawning an SSH process that forwards a local port to the service. Its default NodePort range is 30000 to 32767.
  • minikube tunnel creates a route on your machine to services of type LoadBalancer and sets their external IP. It must stay running, and stopping it (Ctrl+C) removes access. It makes services reachable from your local machine, not from the internet.
  • Ingress add-on plus hosts-file edits works for a domain that only your own machine resolves.

None of these give a remote coworker or a third-party webhook sender a public URL. For that, run one of the operators above inside Minikube. With ngrok you’ll use the AgentEndpoint shown earlier, and ngrok publishes a guide for local clusters. With Tailscale, the funnel Ingress above works unchanged, provided the cluster can reach Tailscale’s network.

Because the tunnel is defined by a resource, its lifecycle follows the resource. kubectl delete on the AgentEndpoint or Ingress tears the public endpoint down, and there’s no forgotten terminal tab holding a port open.

GitOps: tunnels next to your Deployments

When tunnel definitions are YAML, they can sit beside your Deployment and Service in the same repository, and Argo CD or Flux applies them together. If the cluster is rebuilt, reapplying the repository recreates the operators and then the custom resources, and the tunnels come back.

The main gotcha is ordering. A custom resource can’t be applied before its CRD exists. In Argo CD, the CRD often arrives with the operator’s Helm chart, and Argo CD’s dry-run step fails with “the server could not find the requested resource” when the CRD isn’t yet in the cluster. Two settings help: place the operator in an earlier sync wave (or a separate Application) than the resources that depend on it, and add the SkipDryRunOnMissingResource sync option to those resources:

apiVersion: forward.webhookrelay.com/v1
kind: WebhookRelayForward
metadata:
  name: stripe-webhook-forwarder
  namespace: payment-services
  annotations:
    argocd.argoproj.io/sync-wave: "2"
    argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true

Argo CD’s documentation notes that the dry run still runs when the CRD is already present, so this only relaxes the check when it would otherwise block a first install.

Keep credentials out of Git. Every operator above needs a token, API key, or OAuth secret. Store them as Secrets created out-of-band, or via a secrets-management tool, and reference them from your manifests.

Security: what outbound-only does and doesn’t buy you

Outbound-only connections are a real improvement. There are no inbound ports to open on a corporate firewall, and no cloud LoadBalancer per service. But some claims commonly made about this architecture deserve care:

  • The public hostname is still public. The cluster’s network is closed, but the URL you publish is reachable by anyone unless you add access control. ngrok’s Traffic Policy (such as restrict-ips) is one way to add it at the edge, and an application-level check is another. The DDoS surface moves to the provider and your hostname; it doesn’t disappear.
  • Someone terminates TLS. Providers that apply request-level policy at the edge must decrypt HTTP to do it, so they can see the plaintext. That’s a trust decision to make consciously. Tailscale Funnel is the exception in this list, because TLS ends on your own node.
  • Edge filtering isn’t sanitization. Don’t assume traffic arriving through a tunnel is authenticated or clean. Validate inputs and signatures in the application.
  • RBAC only grants; it never denies. You can limit tunnel-creating resources to dev and staging by granting permissions only there and withholding them in production. To enforce finer rules (for example, which hostnames a namespace may claim), add a ValidatingAdmissionPolicy, which has been generally available since Kubernetes 1.30.
  • Tokens are bearer credentials. Anyone holding a tunnel token can typically run their own agent on that tunnel. Scope and rotate them.

Choosing an approach

  • Only need webhooks in a private cluster? Webhook Relay’s Operator is purpose-built for it.
  • Want full public ingress with edge policy in Git? ngrok’s operator, with AgentEndpoint, Ingress, or Gateway API.
  • Mostly want private access, with the occasional public URL? The Tailscale operator, and treat Funnel as the deliberate exception.
  • Already on Cloudflare? Run cloudflared as a Deployment, and look at community operators only if you accept alpha software.
  • Want to self-host the edge? Pangolin with the Newt Helm chart.
  • Want to use KubeSail? You can’t. It’s gone.

The direction of travel is clear: expose services by declaring them, let a controller hold the connection, and keep the whole thing in version control. The details differ per tool, so check each project’s current docs before you commit. This space moves quickly, as KubeSail’s shutdown showed.


Sources


Changelog (editorial notes: remove before publishing)

Major corrections

  1. KubeSail is discontinued. The draft described KubeSail’s operator, my-app.kubesail.io subdomains, and edge TLS in the present tense as a working option. The kubesail.com homepage now shows a farewell notice, and community reports put the hosted gateways’ final shutdown in mid-September 2025. The section is replaced with a short “what happened” note, and the ingress-via-standard-Ingress role is covered by the Tailscale operator instead.
  2. The LocalTunnel CRD (tunneling.example.com/v1alpha1) was a placeholder, not a real product. The Minikube example is replaced with real manifests: an ngrok AgentEndpoint and a Tailscale funnel Ingress.
  3. The Webhook Relay manifest was incomplete. It had an input (public endpoint) but no outputs, so it forwarded nowhere. Added the outputs block with destination, lockPath, and responseStatusCode. The secretRefName field is valid: the project README documents it (with optional secretRefNamespace) for per-resource credentials in multi-tenant setups. The draft’s mistake was presenting it as the only mode; Helm-level credentials are the default. Also corrected the status command to use the full resource name from Webhook Relay’s docs.
  4. “Minikube tunnel” was conflated with public exposure. minikube tunnel routes LoadBalancer services to the local machine only; minikube service covers NodePort. Neither creates a public URL. Rewritten accordingly.

Overstatements softened

  1. “Immune to port scanning, DDoS, and brute-force attempts” removed. The hostname you publish is still public, and the exposed surface moves to the provider’s edge.
  2. “Traffic that reaches the service is already decrypted, authenticated, and sanitized” removed. Edge TLS termination means the provider sees plaintext (Tailscale Funnel is the documented exception), and edge filtering does not replace application-level validation.
  3. “Strictly deny the creation of tunneling CRs in production” corrected. Kubernetes RBAC is additive-only; you withhold permissions rather than deny them. Added ValidatingAdmissionPolicy (GA in 1.30) for finer rules.
  4. Added maintenance context for the Webhook Relay Operator: latest tag 0.6.0, November 2022, while vendor docs still recommend it.

Added content

  1. New sections on the ngrok Kubernetes Operator (AgentEndpoint, CloudEndpoint, Ingress, Gateway API; free to install; reserved domain required), the Tailscale Kubernetes Operator (Ingress class, Funnel annotation, ProxyGroup HA, Funnel port/bandwidth limits, certificate limits), Cloudflare Tunnel on Kubernetes (official Deployment approach, no-autoscale advice, alpha community operator), and Pangolin’s Newt Helm chart (chart 1.4.0, Kubernetes 1.30.14+).
  2. Clarified that Webhook Relay’s Operator and its Ingress Controller are separate products.
  3. Added the Argo CD CRD-ordering problem (SkipDryRunOnMissingResource, sync waves) and secrets-handling guidance.
  4. Added a comparison table and a decision guide.

Not verified / left out on purpose

  • Current pricing and free-tier limits for every provider (they change often; link to vendor pricing pages if you add them).
  • Whether Tailscale Funnel still carries a beta label; the article makes no claim either way.
  • Webhook Relay’s newer features beyond the Operator and Ingress Controller.

Continue from this article into the most relevant product guides and workflows.

Related Topics

#Webhook Relay Kubernetes Operator, KubeSail reverse proxy, expose local Minikube custom resource, Kubernetes tunnel automation, Kubernetes CRD operator tunneling, GitOps reverse proxy automation, Kubernetes custom resource definitions, local Kubernetes webhook tunneling, cloud native tunnel automation, Minikube external ingress operator, Webhook Relay CRD guide, KubeSail tunnel operator, DevOps GitOps tunneling, platform engineering reverse proxy, Kubernetes ingress alternative, expose localhost to internet Kubernetes, Minikube webhook forwarding, k8s operator tunnel management, Declarative Kubernetes tunneling, YAML driven reverse proxy, Kubernetes local environment tunneling, Kubernetes developer tooling, Webhook Relay installation guide, KubeSail deployment tutorial, Minikube external access setup, Kubernetes webhook listener, Kubernetes custom controller development, cloud native developer workflow, GitOps webhook proxy setup, k8s CRD architecture breakdown, Kubernetes local cluster exposure, secure Kubernetes tunneling, Kubernetes API server extensions, Webhook Relay vs KubeSail, Kubernetes developer productivity, microservices local tunneling, local development cluster ingress, Kubernetes operator pattern tutorial, Minikube tunnel operator, automated local endpoint exposure, Kubernetes external traffic routing, k8s operator lifecycle manager, Kubernetes continuous deployment proxy, infrastructure as code tunneling, cloud native proxy automation, Kubernetes cluster webhook relay, local testing Kubernetes operator, Kubernetes YAML tunnel configuration, edge proxy Kubernetes operator, devops Kubernetes tunnel workflows, Kubernetes local host reverse proxy, KubeSail custom resources, Webhook Relay k8s configuration, Kubernetes native edge ingress, cloud native reverse proxy operator

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles