Automatically expose Kubernetes services to the internet with a simple annotation
The stunl Kubernetes Controller watches for Services with the stunl.com/expose annotation and automatically creates tunnel client deployments to expose them to the internet.
Benefits
$ kubectl create namespace stunl-system
$ kubectl apply -f https://stunl.com/k8s/rbac.yaml
$ kubectl create secret generic stunl-api-key \
--namespace stunl-system \
--from-literal=api-key=st_pro_your_key_here
$ kubectl apply -f https://stunl.com/k8s/controller.yaml
# Verify controller is running
$ kubectl get pods -n stunl-system
NAME READY STATUS RESTARTS AGE
stunl-controller-7d8f9b6c4-x2k9p 1/1 Running 0 30s
Add the stunl.com/expose: "true" annotation to any Service to expose it.
apiVersion: v1
kind: Service
metadata:
name: my-web-app
annotations:
stunl.com/expose: "true"
stunl.com/subdomain: "myapp" # Optional: custom subdomain
stunl.com/protocol: "http" # Optional: http (default), tcp, udp
spec:
selector:
app: my-web-app
ports:
- port: 80
targetPort: 8080
$ kubectl apply -f service.yaml
# The controller automatically adds the tunnel URL annotation
$ kubectl get svc my-web-app -o jsonpath='{.metadata.annotations.stunl\.com/url}'
https://myapp.stunl.io
| Annotation | Description | Default |
|---|---|---|
stunl.com/expose |
Enable tunnel for this Service | "false" |
stunl.com/subdomain |
Custom subdomain | {name}-{namespace} |
stunl.com/protocol |
Protocol: http, tcp, udp | "http" |
stunl.com/domain |
Pro domain (e.g., localshare.io) | stunl.io |
stunl.com/use-root |
Use root domain (no subdomain) | "false" |
Output Annotation
The controller automatically adds stunl.com/url to the Service with the public tunnel URL once the tunnel is established.
apiVersion: v1
kind: Service
metadata:
name: frontend
annotations:
stunl.com/expose: "true"
stunl.com/subdomain: "demo"
spec:
ports:
- port: 80
# Result: https://demo.stunl.io
apiVersion: v1
kind: Service
metadata:
name: postgres
annotations:
stunl.com/expose: "true"
stunl.com/protocol: "tcp"
stunl.com/subdomain: "mydb"
spec:
ports:
- port: 5432
# Result: mydb.stunl.io:15432
apiVersion: v1
kind: Service
metadata:
name: api
annotations:
stunl.com/expose: "true"
stunl.com/domain: "localshare.io"
stunl.com/subdomain: "api"
spec:
ports:
- port: 8080
# Result: https://api.localshare.io
The controller can be configured via command-line arguments in the deployment:
| Flag | Description | Default |
|---|---|---|
--tunnel-server |
stunl server address | portal.stunl.com |
--client-image |
Tunnel client container image | (required — must specify a pinned version tag, e.g., |
--metrics-bind-address |
Metrics endpoint address | :8080 |
--health-probe-bind-address |
Health check endpoint | :8081 |
--leader-elect |
Enable leader election for HA | false |
The controller requires RBAC permissions to get/list/watch/update/patch Services (for annotations and finalizers), create/update/delete Deployments, record Events, and manage Leases for leader election.
Store your API key in a Kubernetes Secret. The tunnel client pods must be configured to reference this secret via environment variable or volume mount.
The controller deployment includes NetworkPolicy to restrict egress to the Kubernetes API server and DNS only. Tunnel client pods connect to the stunl server directly.
Controller runs as non-root with read-only root filesystem and all capabilities dropped.
Check controller logs:
kubectl logs -n stunl-system deployment/stunl-controller
Check the tunnel deployment logs:
kubectl logs deployment/stunl-{service-name} -n {namespace}
The controller records validation failures as Events:
kubectl get events --field-selector reason=ValidationFailed