Expose the Slackbot with Tailscale Funnel
Slack delivers events to the Slackbot over public HTTPS (/api/webhooks/slack).
The Slackbot listens on plain HTTP (port 3001) as an in-cluster ClusterIP
service and does not terminate TLS itself. For a durable, in-cluster way to make
it reachable from Slack, use the Tailscale Kubernetes operator
with Funnel: the operator publishes a
public endpoint at https://<name>.<tailnet>.ts.net, terminates TLS with an
auto-renewed Let's Encrypt certificate, and forwards plain HTTP to the Slackbot.
This is the production-style alternative to the ad-hoc laptop tunnel in
Mac Mini-style setup
(kubectl port-forward + cloudflared/tailscale funnel), which is fine for
quick local testing but ephemeral.
Prerequisites
- Funnel enabled for your tailnet: in the Tailscale admin console DNS page, enable MagicDNS and HTTPS certificates.
- A tailnet policy (ACL) that defines the operator tags and grants Funnel to the
operator's proxy nodes:
Target
{ "tagOwners": { "tag:k8s-operator": [], "tag:k8s": ["tag:k8s-operator"] }, "nodeAttrs": [ { "target": ["tag:k8s"], "attr": ["funnel"] } ] }tag:k8s(the operator's default proxy tag), notautogroup:member: tagged proxy nodes are not members, so the default Funnel grant would not cover them and the device would come up tailnet-only. - An OAuth client for the operator
(scopes
devices:coreandauth_keys, ownertag:k8s-operator). - The Tailscale operator installed in the
tailscalenamespace:helm repo add tailscale https://pkgs.tailscale.com/helmcharts && helm repo update helm upgrade --install tailscale-operator tailscale/tailscale-operator \ -n tailscale --create-namespace \ --set-string oauth.clientId=<id> --set-string oauth.clientSecret=<secret> --wait
Configure the chart
Expose the Slackbot with a Tailscale Funnel Ingress. A ready-to-use sample
lives at contrib/chart/values.tailscale-funnel.example.yaml:
ingress:
enabled: true
className: tailscale
defaultBackend: true # the operator's Funnel Ingress expects a single backend
annotations:
tailscale.com/funnel: "true" # public Funnel exposure; omit for tailnet-only
tls:
- hosts:
- centaur-slackbot # -> https://centaur-slackbot.<your-tailnet>.ts.net
networkPolicy:
ingressControllerNamespaces:
- kube-system
- tailscaleWhat each piece does:
ingress.defaultBackend: truemakes the chart emit a singlespec.defaultBackend(instead of host/path rules) — the shape the Tailscale operator's Funnel Ingress expects.ingress.className: tailscaleroutes the Ingress to the operator.- The
tailscale.com/funnel: "true"annotation makes the endpoint public. Omit it to keep the Slackbot reachable only inside your tailnet. tls.hosts[0]sets the device's MagicDNS name (<name>.<tailnet>.ts.net).- Adding
tailscaletonetworkPolicy.ingressControllerNamespaceslets the operator's proxy pods reach the Slackbot on port 3001. The Slackbot NetworkPolicy otherwise admits only the API, workflow-run pods, and the listed ingress-controller namespaces (defaultkube-system).
Deploy
Layer the example file on top of your normal values with the CENTAUR_EXTRA_VALUES
hook, which keeps the shared values.dev.yaml untouched:
CENTAUR_EXTRA_VALUES=contrib/chart/values.tailscale-funnel.example.yaml just upOr with Helm directly:
helm upgrade --install centaur contrib/chart -n centaur \
-f contrib/chart/values.dev.yaml \
-f contrib/chart/values.tailscale-funnel.example.yamlPoint Slack at it
Set the Slack app's Event Subscriptions Request URL to:
https://<name>.<tailnet>.ts.net/api/webhooks/slackThen finish the Slack app in
Deploying in Production → Configure Slack:
subscribe to app_mention and the message.* events you want, and make sure the
bot has the chat:write scope — the Slackbot delivers replies with Slack's
streaming API, which requires it.
Verify
kubectl get ingress -n centaur # ADDRESS resolves to <name>.<tailnet>.ts.net
kubectl get pods -n tailscale # operator + a ts-...-slackbot-... proxy, both RunningAn unsigned POST should reach the Slackbot and be rejected by the app — proof that TLS termination, routing, and the NetworkPolicy all work end to end:
curl -i -X POST https://<name>.<tailnet>.ts.net/api/webhooks/slack
# HTTP/2 401 {"ok":false,"error":"missing_signature_headers"}A 401 from the Slackbot means success: curl validated the public Let's Encrypt
certificate without -k. Saving the Request URL in Slack should then verify green.
Troubleshooting
- Device appears but Funnel is off (tailnet-only): the
funnelnodeAttr is missing or targetsautogroup:memberinstead oftag:k8s, or HTTPS certificates are not enabled for the tailnet. - Connection hangs or times out: the Slackbot NetworkPolicy is still blocking
the operator's proxy — confirm
tailscaleis innetworkPolicy.ingressControllerNamespacesand that the namespace carries thekubernetes.io/metadata.name: tailscalelabel (automatic on Kubernetes ≥ 1.22).