Tailscale for lazy application authentication
Who am I?
Hello! I'm Elliot, I'm a founding engineer at Sunbeam. I've been working in various software engineering roles for about a decade, and so it'll come as no surprise that this talk is going look at integrating tailscale into web apps.
It's my understanding that this is an audience from quite diverse jobs roles and experience. We've got everyone ranging from homelab enthusiasts to IT greybeards in this audience, so I intend to stay at quite a high architectural level. I won't be going into specific code samples today,
Our context
Context is very important, just because the content in this works well for me doesn't mean it's the best thing for you. Let me give you a quick overview of the context I'm working in right now.
We're a Bristol based startup working on "feedback analytics". In short, we take all your text-based review data and produce a set of insights based on what people are saying. We do this with all sorts of customers from HR teams trying to understand their employee experience surveys, to product teams looking at their customer feedback.
Sunbeam is young, and lean! There's 5 of us, and we're only a year and a half old. This forces us to be picky about the challenges we spend time on, and to aim for simple and robust solutions.
The problem space
As with any new SaaS startup, once we had our product to take to market we quickly needed some internal tools to support our customers and sales process. So we built an internal web app for onboarding organisations, plugging in their data sources, and configuring their feedback categories, and scheduling analyses.
Building the web app is one thing, but securing it is another. This needed to only be accessible to our staff, and as we grow we will want to limit the staff who have access to the tool.
We already had Tailscale up and running as our VPN so today we're going to look at how we piggybacked off Tailscale to handle authentication, authorisation, and identity.
Overview
Tailscale has a few ways of doing this, and today we'll look two in particular:
tailscale serve
- tsnet for Native Tailscale Apps
We won't be going into code today to help make the topic more accessible. Instead we'll be looking at the architecture of these two solutions, and their pros and cons.
Tailscale Serve
tailscale serve
lets you share a local service securely within your tailnet.
This is how the documentation introduces the command, and it's a pretty good summary. Serve basically acts as a small tcp, http, or https proxy - it listens for connections from your tailnet, and forwards them on to a destination service.
Here's a very simple diagram of the traffic flow when you run it locally, pointing https traffic to a web service on your machine.

Your colleague can visit https://elliot-mbp.my-co.ts.net:443
, https will be handled by the tailscale client and it'll forward traffic onto your web app.
All of this sits behind your grants (or ACL's), so you don't need to worry about Joe from HR being able to access the super secret customer data app. You just configure the grants you want and away you go!
Serve also generates and attaches identity headers as it proxies requests to your application. This gives your service a way to know specifically who is accessing it for every single request, without having to issue tokens, and without any additional login step. I can't overstate how great this is for users, and developers.
Tailscale-User-Login
- This is the tailscale login name, on a custom domain it'll be something like[email protected]
, with GitHub as the auth provider it might bealice@github
.Tailscale-User-Name
- The display name attached to the tailscale userTailscale-User-Profile-Pic
- The URL of the users profile picture, if the identity provider offers one.
So with tailscale serve we can use grants to manage broad access to the application, and know exactly who's accessing it. Incidentally, this is also pretty much how the Tailscale plugin for Caddy works under the hood.
This simple setup works really well, but in our case we needed to be able to scale the destination service. Let's look at how we might achieve that.

This is a pretty good representation of our setup right now, it's come about due to a few constraints. First, we have a lot more experience with nginx so decided to start there although we do plan to switch over to Caddy for this soon. Second, because the API we were targetting serves a few purposes we needed to go through a load balancer so we could auto-scale horizontally.
Even with this more complex setup, the tailscale side is a very easy way to achieve our goal of great UX and strong security. But, like all of us, it's not without it's flaws, mainly:
- When deploying this onto a production environment, you end up needing some kind of sidecar like setup. Either you need to deploy onto a host that has tailscale installed and then configure serve, or you need a container based sidecar for environments like kubernetes.
- Your application needs to trust the tailscale headers from the upstream, so you it relies on your wider infrastructure to ensure nothing can inject them maliciously along the way.
- If you wish to use a load balancer, serve will need to sit in-front of the load balancer as an intermediate reverse proxy. This adds another layer to the request chain and is one more thing to manage. If your load balancer isn't running on the same device as your tailscale serve node, you'll also need yet another hop - because serve will only proxy to services running on locahost. This means to go from serve to something like this:
tsnet for Native Tailscale Apps
With the open source Tailscale Go library we go in a different direction, and have our application connect to our tailnet directly. Going back to our original example, using the Go library we can cut out the extra hop of the separate tailscale client.

In this case it looks like all we've done is remove a single hop, but when we apply this to our production AWS example, our architecture ends up exactly the same. On our diagram you just replace "My local laptop" with "AWS" and you're done.
This nets us all the benefits or Serve, with fewer components, fewer network hops, and fewer nodes to secure.
Application capabilities
If you're using Grants, you can also fetch a users application capabilities through tsnet as well! This lets you go beyond the question of just "who are you?" to "what permissions should this individual have?"
It is possible to fetch these through the tailscale whois
command, and the Local API in the client. But neither are as easy as a function call into tsnet, and they require your application to be aware of the tailscale client attached to the node.
So, is this the promised land and what we should be using for all our internal business applications? Unfortunately not, or at least not yet. There are two issues with this:
- This only works with Go, which is a shame.
- If your service needs to scale beyond 1 node, you'll need the tailnet connection to be at or before the load balancer anyway. That said, this may be less of a problem if you're using the Kubernetes operator.
libtailscale for additional language support
libtailscale solves the language issue for building native tailscale applications. It does this by providing a C archive of the tailscale library. This allows anyone to write bindings to pull in native tailnet functionality to their service.
The project is still very much in an alpha state right now. There are already bindings in the repo for Python, Ruby, and Swift. Community members have also been building their own bindings and packages for other languages too - such as Elixir.
This is a really exciting project, but my understanding is this is more of a side project for them right now. It's very much usable, but would be great to see it supported a little more in the future.
For scaling, we're never going to get away from needing some kind of load balancer, but we are seeing more load balancers and reverse proxies getting tailscale support. Projects like the Caddy plugin are very active, and even older ones like Tailscale's nginx-auth are straight forward to setup. Hopefully we're only at the beginning of this journey and we'll start to see more options emerge soon.
Summary
Auth and access to internal tools is mostly a solved problem with tailscale. It's for sure a lot easier than integrating a SAML provider!
Serve is a very easy way to get up and running, and if you're likely to need horizontal scaling it may be your best option.
For those of you using Go, tsnet is an excellent choice and comes with some added extras.
Member discussion