Using Tailscale Certificates in Kubernetes
As I mentioned earlier , I’m a huge fan of Tailscale . Last year, they added a beta ability to issue X.509v3 certificates (via Let’s Encrypt ) to systems on your Tailnet. As they explain in a blog post , they do this by giving you a (somewhat randomish) FQDN, and then doing the DNS-01 challenge dance for you.
To get the certificate:
$ sudo tailscale cert $FQDN
Weirdly, if you don’t provide the domain, it tells you what domain to use, but it’s unclear if you can use a domain that isn’t the one of the host you’re on. Perhaps you can call this from another place to get the right certificate.
Anyway, that will dump
.key files into the current working
directory. You can then load them into the appropriate k8s secret:
$ kubectl create secret tls tailscale-tls --namespace test --key $FQDN.key --cert $FQDN.crt secret/tailscale-tls created
Your secret needs to be in the same namespace as where you’re putting the Ingress Controller. I’m trying to figure out a way to make sure that the key never hits the disk, likely involving some shell pipe magic.
Note: The normal behavior is to run an Ingress
on every node (in a
For my cluster, this means running the
this setup, you are using only one of the ingress controllers (because you’re
sending the traffic to one specific node, and not balancing it). If you were
balancing between all the nodes (in my case, 3), then you wouldn’t be able to
use a Tailscale managed certificate, but would need a normal one. You might be
able to get something managed by cert-manager
Let’s Encrypt. Of course that can be complicated when managing internal-only
routes. I’ve opened a feature
for Tailscale, and
we’ll see where that goes.
Back to the example, though. For this, we’re going to use Google’s sample
hello-app. This presumes you have an ingress controller already
deployed, and that you’re going to deploy into a namespace named
we have the Deployment itself, which runs 3 copies of the demo container and
exposes the service on port 8080:
apiVersion: apps/v1 kind: Deployment metadata: name: hello-app namespace: test spec: selector: matchLabels: app: hello replicas: 3 template: metadata: labels: app: hello spec: containers: - name: hello image: "gcr.io/google-samples/hello-app:2.0" ports: - containerPort: 8080
Next, we’ll expose it as a Service inside the cluster on a dedicated cluster IP:
apiVersion: v1 kind: Service metadata: name: hello-service namespace: test labels: app: hello spec: type: ClusterIP selector: app: hello ports: - port: 80 targetPort: 8080 protocol: TCP
Now we have a service we can access internally, but we’re still unencrypted. The next step is to expose it via an Ingress Controller, and hook up the TLS certificate:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: hello-app-ingress namespace: test spec: ingressClassName: public tls: - hosts: - flora.superior-owl.ts.net secretName: tailscale-tls rules: - host: "$FQDN" http: paths: - pathType: Prefix path: "/" backend: service: name: hello-service port: number: 80
And now you can access it in your browser with everything linked up by going to
https://$FQDN/, and you should see something like this:
Hello, world! Version: 2.0.0 Hostname: hello-app-5c554f556c-hd7mn
Hostname will be different for you, but if you refresh, you’ll see the
last segment changes among 3 different Pods.