Kubernetes-Native Tunneling: The CRD and Operator Approach for Automated Ingress
IT

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 developers don’t run imperative CLI binaries; they write declarative YAML. Managing external access to private, edge, or local clusters with manually-run reverse proxies is an anti-pattern in a platform engineering workflow. Tools like the Webhook Relay Kubernetes Operator and Tailscale’s Kubernetes Operator let developers manage external network access entirely through standard Kubernetes Custom Resource Definitions (CRDs) and the native Ingress API. By treating tunnels as GitOps-managed resources, platform teams can securely expose internal services, automate inbound webhook routing, and manage edge-cluster ingress without touching a detached CLI process.
The problem with imperative tunnels in a declarative ecosystem
Developers relying on local or private infrastructure have historically used tools like ngrok, localtunnel, or SSH reverse proxies to expose services to the internet. Effective for a quick debugging session, but inherently imperative: you open a terminal, run a command, and leave it running.
In Kubernetes, this breaks down fast. Kubernetes is a declarative state machine — if a node goes down, a pod is evicted, or the cluster scales, an imperative tunnel running in a detached process has no relationship to that reconciliation loop. The orchestrator has no knowledge of the tunnel, no way to monitor its health, and no mechanism to recreate it.
Provisioning static IPs, firewall rules, and cloud LoadBalancers for every microservice or webhook receiver is also expensive and operationally heavy — particularly for on-premise deployments, edge/IoT networks, and local development clusters where public IPs simply aren’t available. That’s the case for treating tunnels as version-controlled, continuously reconciled Kubernetes objects, managed the same way as a Deployment or ConfigMap.
The shift to operator-driven networking
The Kubernetes Operator pattern is a custom controller that watches Custom Resources and reconciles cluster state to match what’s declared in them. Applied to networking, an operator runs inside your cluster, watches for a specific CRD, and — when a developer commits a new CR to Git — authenticates with an external tunneling service and opens a persistent, secure outbound connection. Because the connection is outbound-initiated, it doesn’t require opening inbound firewall ports, configuring NAT traversal, or running a public-facing ingress controller.
This gives platform teams three real advantages:
- State reconciliation — if the tunnel disconnects, the operator sees that current state no longer matches the CR’s desired state and rebuilds the connection.
- No inbound exposure — the cluster doesn’t open any inbound ports; the operator is an authenticated, outbound-only gateway.
- GitOps compatibility — tunnel configuration lives in Git next to the application manifests, so tools like ArgoCD or Flux can deploy an app and its public-facing route in the same sync.
Webhook Relay Kubernetes Operator: routing webhook and API traffic
Receiving webhooks (from GitHub, Stripe, or Slack) inside a private cluster typically means standing up a public API gateway and punching a hole in the corporate firewall. The Webhook Relay Kubernetes Operator solves this without a public IP or load balancer, and it’s aimed specifically at receiving and routing webhooks/API requests — on-premise setups, K3s edge deployments, and IoT are the documented use cases.
Installation is via Helm, and the operator is configured cluster-wide with an access key and secret at install time — not per-CR:
helm repo add webhookrelay https://charts.webhookrelay.com
helm repo update
export RELAY_KEY=*****-****-****-****-*********
export RELAY_SECRET=**********
helm upgrade --install webhookrelay-operator --namespace=default webhookrelay/webhookrelay-operator \
--set credentials.key=$RELAY_KEY --set credentials.secret=$RELAY_SECRET
Once the operator is running, a WebhookRelayForward Custom Resource describes the public endpoint and where to forward it. Note that both an input (the public endpoint) and an output (the internal forwarding destination) are required — a CR with only an input has nowhere to send traffic:
# cr.yaml
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: webhook-receiver
destination: http://destination:5050/webhooks
kubectl apply -f cr.yaml
The operator provisions the bucket, opens the public endpoint, and routes matching traffic to the destination service; deleting the CR (kubectl delete -f cr.yaml) tears the forwarding down again. It also emits Kubernetes events and updates the CR’s status fields, so kubectl describe webhookrelayforward shows live delivery state.
It’s worth being precise about scope: this operator is for webhook/API forwarding specifically. Webhook Relay ships a separate product — the Webhook Relay Ingress Controller (relay ingress init, deployment manifests at github.com/webrelay/ingress) — for general bidirectional tunnels that expose a full service like Grafana or Prometheus through a *.webrelay.io subdomain via standard Ingress resources. The two are installed differently and solve different problems; don’t expect the webhook operator’s CRD to proxy an entire web app.
One newer addition worth flagging for teams already wiring AI agents into their infrastructure: Webhook Relay now exposes an MCP server (https://my.webhookrelay.com/v1/mcp) that lets an agent list buckets, create or update inputs/outputs, inspect webhook delivery logs and stats, and even send synthetic test webhooks through the real pipeline — useful if you want an agent debugging “why did this GitHub webhook fail to reach my cluster” instead of doing it by hand.
The KubeSail gap — and what actually replaces it today
KubeSail was, for several years, the most commonly cited example of “watch a standard Kubernetes Ingress resource and automatically provision a public subdomain with TLS at the edge” for home-lab and bare-metal clusters. It’s important to be upfront that KubeSail shut down its hosted services in September 2025. The company’s own farewell page confirms it: “After six wonderful years, we’ve made the difficult decision to stop operating our hosted services… KubeSail is no longer accepting new users or selling hardware.” Its founders’ explicit parting recommendation was to look at Tailscale Funnel and Cloudflare Tunnel instead. Any current article — or draft — describing KubeSail’s operator as an available option is describing a discontinued product.
The good news is that the pattern KubeSail popularized is alive and, if anything, more mature today via the Tailscale Kubernetes Operator.
Tailscale Kubernetes Operator
Installed via Helm using OAuth client credentials, the operator can expose a cluster workload to your private tailnet — and, optionally, to the public internet — in the same declarative way KubeSail did: by watching a standard Ingress resource. No custom vendor-specific kind is even required for the common case.
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
namespace: dev
annotations:
tailscale.com/funnel: "true"
spec:
defaultBackend:
service:
name: my-app
port:
number: 80
ingressClassName: tailscale
tls:
- hosts:
- my-app
kubectl apply -f ingress.yaml
kubectl get ingress my-app -n dev # watch for the assigned hostname
Setting ingressClassName: tailscale tells the operator to take ownership of the Ingress: it provisions a new Tailscale node, issues a TLS certificate for its MagicDNS name, and proxies traffic to the backend Service — no cluster-side firewall rule required. Left as-is, the workload is reachable only from other devices on your tailnet (a private network), which is already useful for internal dashboards or CI runners. Adding the tailscale.com/funnel: "true" annotation is what makes it publicly reachable from the open internet, which is the closer analog to what KubeSail used to do automatically.
For production-grade setups, a ProxyGroup custom resource lets you run multiple ingress proxy replicas so the route survives a proxy pod restart, and Tailscale’s own documentation explicitly lists managing multi-cluster deployments with ArgoCD as a supported use case — so this slots into the same GitOps model described below. Deleting the Ingress cleanly removes the corresponding Tailscale node, the same “no orphaned connections” guarantee the CRD approach is meant to provide.
Self-hosted option: Pangolin’s Newt Helm chart
For teams that want the same outbound-only pattern but fully self-hosted rather than routed through a vendor’s edge network, Pangolin — a self-hosted, WireGuard-based tunneled reverse proxy (Traefik for TLS termination, Gerbil for the WireGuard side, and a lightweight connector called Newt that dials out from the private network) — now publishes an official Newt Helm chart for Kubernetes deployments, distinct from the Docker Compose install most self-hosters have used until recently. At the time of writing the chart is versioned 1.4.0 against Newt app version 1.12.3 and documents a minimum Kubernetes version of 1.30. It supports per-instance namespaces and reading connection credentials from an existing Kubernetes Secret rather than plaintext values, which matters if you’re deploying it through the same GitOps pipeline as everything else.
Exposing local Minikube workloads to the public internet
Minikube is the standard way to run a single-node cluster on a laptop, but testing public-facing features — OAuth callbacks, third-party webhooks, external API integrations — against it has historically been awkward.
The traditional options are:
kubectl expose+minikube service. This exposes a Deployment via a NodePort, thenminikube service <name>opens a browser window pointed at it. On the Docker driver — which is the default on macOS and Windows, where Docker Desktop can’t route directly to container IPs — this command actually opens an SSH tunnel from the host into the minikube node to reach the service, rather than hitting the NodePort directly. It’s a real mechanism (minikube’skic.ServiceTunnelshells out over SSH), not a simple port lookup, and it only stays open as long as the command keeps running in that terminal.- The Ingress addon (
minikube addons enable ingress) plusminikube tunnel.minikube tunnelis the command for exposingLoadBalancer-type Services and typically needs elevated privileges because it edits host routing; it runs in the foreground until youCtrl-Cit, and forgetting a terminal running it is a common annoyance. Neither of these approaches scales well if you need to share your local environment with a remote teammate or an external webhook provider.
Because the Tailscale operator (above) only cares about the standard Ingress API, it works on Minikube exactly like it works on any other cluster — install the operator with Helm, then apply the same ingress.yaml shown earlier, ingressClassName: tailscale and all, directly against your Minikube context:
minikube start
helm install tailscale-operator tailscale/tailscale-operator \
--namespace=tailscale --create-namespace \
--set-string oauth.clientId=<CLIENT_ID> \
--set-string oauth.clientSecret=<CLIENT_SECRET>
kubectl apply -f ingress.yaml # same manifest, funnel annotation included
This gives you a real public HTTPS URL that GitHub or Google OAuth can send callbacks to, sourced from a workload running inside Minikube on your laptop — without a detached minikube tunnel process, and without hand-editing /etc/hosts. Deleting the Ingress tears the node and its certificate down the same way it would on any cluster, so there’s no orphaned public endpoint left behind after you’re done testing.
GitOps integration and continuous delivery
The CRD/operator approach earns its keep in a GitOps workflow, where Git is the single source of truth for infrastructure and application state. In a traditional setup, a CI/CD pipeline deploys application code, and a human then manually configures the reverse proxy or tunnel to expose it — creating drift between what’s in version control and what’s actually reachable.
With tunnel definitions expressed as CRDs or standard Ingress objects, the routing configuration is just another YAML file next to your Deployment, Service, and ConfigMap manifests:
- A developer pushes a feature branch containing a new microservice plus a
WebhookRelayForwardCR (or a Tailscale-annotatedIngress) to Git. - ArgoCD or Flux detects the commit and syncs cluster state, applying the microservice and the routing object together.
- The relevant operator picks up the new object, contacts its external service, and establishes the route.
- The service is reachable — publicly or on the tailnet — without a human touching a proxy config.
If the cluster is rebuilt from scratch, ArgoCD reapplies the repository to the new cluster, and the operators re-provision every route from the existing objects with no manual intervention. This is exactly the multi-cluster ArgoCD pattern Tailscale documents for its own operator.
Architectural security benefits
Moving from port-forwarding and cloud load balancers to operator-managed outbound tunnels meaningfully hardens a cluster’s security posture. Because the connection is outbound-initiated, there’s no need to open inbound ports on the firewall or cluster security groups — the cluster stays dark to the public internet, unreachable by port scanning or direct DDoS against the cluster’s own IP.
Standard Kubernetes RBAC applies to these CRDs and Ingress resources the same way it applies to any other object: a cluster administrator can scope role bindings so only certain namespaces are permitted to create tunneling CRDs or Ingress resources with ingressClassName: tailscale, letting dev and staging self-serve public endpoints while production traffic is forced through a more heavily audited path. This isn’t a special feature of any one operator — it’s ordinary namespace-scoped RBAC applied to a new kind of object.
TLS termination and, in Tailscale’s and Webhook Relay’s cases, authentication happen at the edge, before traffic ever reaches the cluster — so what arrives at the internal service is already encrypted-in-transit-terminated, and in some configurations already authenticated.
Where this leaves things
The direction is toward abstraction: developers write application logic and a Deployment manifest, not imperative CLI tunnels or hand-managed NAT rules. For webhook and API-specific routing, the Webhook Relay Operator remains a solid, narrowly-scoped fit. For general-purpose ingress — the job KubeSail used to do — the Tailscale Kubernetes Operator is the actively maintained, standard-Ingress-API option today, with Pangolin’s Newt Helm chart as the self-hosted alternative for teams that want the same pattern without routing through a third party’s edge network. All three fit the same shape: a CRD or annotated standard object, reconciled continuously, torn down cleanly on deletion, and deployable through the same Git-based pipeline as everything else in the cluster.
Fact-check and revision log
- KubeSail is discontinued (major correction). The original draft’s entire “KubeSail Reverse Proxy” section was written in the present tense about an active hosted service. KubeSail’s own site now shows a farewell notice: hosted services stopped in September 2025, the company is “no longer accepting new users or selling hardware,” and its founders publicly recommended Tailscale Funnel and Cloudflare Tunnel as replacements. Rewrote that entire section around the Tailscale Kubernetes Operator, which fills the same “watch the standard Ingress API, auto-provision a public endpoint with edge TLS” role and is actively maintained, plus added Pangolin’s newly-documented Newt Helm chart as a self-hosted alternative.
WebhookRelayForwardexample CR was incomplete and had a fabricated field. The draft’s YAML included asecretRefName: whr-credentialsfield that isn’t part of the CRD schema (credentials are supplied to the operator via Helm--set credentials.key/secretat install time, creating a cluster-scoped Secret — not referenced per-CR) and omitted theoutputsblock entirely, meaning the example as written had a public endpoint but nowhere to forward traffic. Corrected against the current official docs at webhookrelay.com/docs/installation/kubernetes/, added the requiredoutputsfield,responseStatusCode, and the correct Helm install flow.- Added a missing distinction: Webhook Relay ships two separate products — the webhook-forwarding Operator (
WebhookRelayForwardCRD) and a separate Ingress Controller (relay ingress init) for general bidirectional service tunnels. The original draft implicitly blurred these; the revision states the scope of each explicitly. - Added new, verified detail not in the draft: Webhook Relay’s MCP server for AI-agent management of buckets/inputs/outputs/logs, which is directly relevant to this blog’s ongoing MCP/AI-agent tunneling coverage.
- Minikube’s SSH-tunnel claim was checked, not assumed. Confirmed against minikube’s own source (
kic.ServiceTunnel, SSH-based) thatminikube serviceon the Docker driver does open an SSH tunnel rather than hitting the NodePort directly — the draft’s claim was accurate — and added the clarification that this specifically matters on macOS/Windows Docker Desktop, where the driver can’t route to container IPs directly. - Replaced the fabricated
tunneling.example.com/v1alpha1/LocalTunnelexample with a real, verified manifest: a standard KubernetesIngresswithingressClassName: tailscaleand thetailscale.com/funnel: "true"annotation, applied directly against a Minikube context using the Tailscale Kubernetes Operator — sourced from Tailscale’s own quickstart and ingress documentation. - Softened the RBAC/production-namespace claim from being described as a specific operator feature to what it actually is: ordinary Kubernetes namespace-scoped RBAC applied to CRDs and Ingress objects, which any of these operators inherit rather than implement themselves.
- Verified, unchanged claims: the general Operator pattern description, the GitOps/ArgoCD reconciliation narrative, and the Webhook Relay Helm install commands all checked out against current sources and were kept with only light copy edits.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Comments
Post a Comment