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 sidecars 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_live_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 | stunl/client:latest |
--metrics-bind-address |
Metrics endpoint address | :8080 |
--health-probe-bind-address |
Health check endpoint | :8081 |
--leader-elect |
Enable leader election for HA | true |
The controller requires minimal RBAC: watch/list Services, create/update/delete Deployments in watched namespaces, and record Events.
Store your API key in a Kubernetes Secret. The controller automatically mounts it to tunnel client pods.
The controller deployment includes NetworkPolicy to restrict egress to only the stunl server.
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