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 geta 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:
- An agent or operator runs inside your cluster.
- It opens an outbound connection to a provider’s edge, so no inbound firewall rule or public IP is needed.
- A controller watches Kubernetes objects and configures the provider through its API.
- 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.netdomain. - 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
funnelnode 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 serviceopens 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 tunnelcreates a route on your machine to services of typeLoadBalancerand 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
devandstagingby granting permissions only there and withholding them inproduction. To enforce finer rules (for example, which hostnames a namespace may claim), add aValidatingAdmissionPolicy, 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
cloudflaredas 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
- Webhook Relay Operator: https://github.com/webhookrelay/webhookrelay-operator and https://webhookrelay.com/docs/installation/kubernetes/
- ngrok Kubernetes Operator: https://ngrok.com/docs/getting-started/kubernetes/crds and https://ngrok.com/docs/k8s/guides/using-gwapi
- Tailscale Kubernetes Operator: https://tailscale.com/docs/kubernetes-operator/install-operator and https://tailscale.com/docs/kubernetes-operator/ingress
- Tailscale Funnel with Kubernetes: https://tailscale.com/docs/kubernetes-operator/ingress/expose-workload-to-internet
- Tailscale Funnel (limits and TLS behavior): https://tailscale.com/docs/features/tailscale-funnel and https://tailscale.com/blog/tailscale-funnel-beta
- Cloudflare Tunnel on Kubernetes: https://developers.cloudflare.com/tunnel/deployment-guides/kubernetes/
- Community Cloudflare operator: https://github.com/adyanth/cloudflare-operator
- Pangolin Helm and Newt: https://docs.pangolin.net/self-host/manual/kubernetes/helm and https://docs.pangolin.net/self-host/manual/kubernetes/newt/helm
- KubeSail farewell: https://kubesail.com/ and community thread https://lemmy.world/post/38039322
- Minikube: https://minikube.sigs.k8s.io/docs/handbook/accessing/ and https://minikube.sigs.k8s.io/docs/commands/tunnel/
- Argo CD sync options: https://argo-cd.readthedocs.io/en/release-2.9/user-guide/sync-options/
- ValidatingAdmissionPolicy GA: https://kubernetes.io/blog/2024/04/24/validating-admission-policy-ga/
Changelog (editorial notes: remove before publishing)
Major corrections
- KubeSail is discontinued. The draft described KubeSail’s operator,
my-app.kubesail.iosubdomains, 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-Ingressrole is covered by the Tailscale operator instead. - The
LocalTunnelCRD (tunneling.example.com/v1alpha1) was a placeholder, not a real product. The Minikube example is replaced with real manifests: an ngrokAgentEndpointand a Tailscale funnelIngress. - The Webhook Relay manifest was incomplete. It had an input (public endpoint) but no
outputs, so it forwarded nowhere. Added theoutputsblock withdestination,lockPath, andresponseStatusCode. ThesecretRefNamefield is valid: the project README documents it (with optionalsecretRefNamespace) 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. - “Minikube tunnel” was conflated with public exposure.
minikube tunnelroutes LoadBalancer services to the local machine only;minikube servicecovers NodePort. Neither creates a public URL. Rewritten accordingly.
Overstatements softened
- “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.
- “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.
- “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. - Added maintenance context for the Webhook Relay Operator: latest tag 0.6.0, November 2022, while vendor docs still recommend it.
Added content
- 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+). - Clarified that Webhook Relay’s Operator and its Ingress Controller are separate products.
- Added the Argo CD CRD-ordering problem (
SkipDryRunOnMissingResource, sync waves) and secrets-handling guidance. - 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.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.