Why we built authentik Outposts as microservices
authentik is an open source Identity Provider that unifies your identity needs into a single platform, replacing Okta, Active Directory, and auth0. Authentik Security is a public benefit company building on top of the open source project.
We’ve already seen high-profile migrations away from microservices (for example Amazon, Uber, and Google), and just recently The Pragmatic Engineer shared how teams at some companies have suffered in the wake of mass layoffs, as there simply aren’t enough staff to operate the thousands of services built by what used to be much larger engineering organizations. The tide has turned against microservices.
We’re happy to see a shift away from architecture inspired by buzzwords. In many cases (especially if you’re a small startup), you really don’t need microservices, you just need well-demarcated code. There are some good use cases for microservices however—when they address a genuine technical challenge—and this article is about one of them.
authentik is a monolith, except for Outposts
authentik provides authentication protocols (which we call providers) to authenticate to external applications. Outposts are how we implement some of these protocols outside of the main authentik process, either for efficiency or other technical reasons (which we’ll explore below).
We have four types of Outposts:
- Proxy Provider
- LDAP Provider
- RADIUS Provider
- Remote Access Control Provider
Proxy Provider
Say you have a website that requires user authentication to access, but for various reasons that authentication is not implemented in the main website itself (because it’s a static page, for example).
Here’s what happens when your reverse proxy (e.g. NGINX, Traffic, Caddy) gets an incoming request from a user:
- First, it checks if that request is authenticated (usually with a cookie that has data attached to it, such as a session key).
- To do so, it calls an external service to validate if the user is authenticated.
- Because of the way the request forwarding works, you can’t forward the request over another reverse proxy because the headers are altered in a way that will break this method.
- So, the reverse proxy needs to be able to communicate with that authenticating service directly.
Enter Outposts. You may not have an authentik instance running right next to the reverse proxy that performs this check, but you can run an Outpost right next to it to forward the request and communicate with the authenticating service.
LDAP Provider
There are two main reasons for the LDAP Provider Outpost being a microservice:
- We can use Go
We’ve talked before about our choice of Python and Django for authentik—for better and worse—and while it’s been the right decision overall, it does pose some limitations. LDAP, being its own protocol (not an HTTP protocol), would be annoying to integrate with Django. That would mean having Gunicorn listen on other ports and route the requests, depending on where they originate.
However, we’re not limited to Python and Django, since our Outposts are microservices. All our Outposts are written in Go, and there are great existing Go libraries for LDAP which we’re now free to use.
- Some actions are much faster
When you authenticate with LDAP, the request goes to the Outpost, which then forwards it to the authentik server. If you have caching enabled, the information may already be in memory, which speeds up the process.
RADIUS Provider
We have the RADIUS Provider as an Outpost for the same reasons as LDAP, but also:
RADIUS is mainly used for Wi-Fi authentication and low-level network authentication, like when you need to authenticate to the switch you plug a machine into before getting access to the network. This is not a very secure protocol and ideally you wouldn’t be spanning multiple data centers to get to the thing that’s performing authentication.
Instead, you can run your Outpost close to the device, switch, or access point, providing some protection against this protocol being spoofed. The rest of the transaction happens over HTTPS, which is secure.
Remote Access Provider (RAC)
For the protocols that we use to access remote machines (RDP, SSH, VNC), we’re using Apache Guacamole. We have a Go process that runs [guacd](https://guacamole.apache.org/doc/gug/guacamole-architecture.html#guacd)
and then communicates with it. Again, this would be tricky to integrate directly into Django, but we are spared that headache because the Outpost is written in Go.
Additionally, customers might not want their authentik instance to have access to the machines they want to proxy to. If you’re operating a “zero trust” policy, you may want your machine to trust only a select few connections.
In this case, you can limit access to the RAC Provider Outpost vs the entire authentik instance: you put the Outpost in between the machines that are to be accessed and your authentik instance, so only the Outpost needs to access the machines you want to connect to.
For our customers, Outposts being microservices offers flexibility, because you can run Outposts wherever you need them.
For example, you might have several points of presence across the globe, with a central authentik instance in, for example, Europe because that’s where your company is based and most employees are located there. If you need to do authentication over LDAP in one of your points of presence elsewhere, like the US, you can run your LDAP Outposts in the US, enable caching, and save a lot of round-trip time between the US and Europe.
For enhanced security, you can also split access requirements to authentik:
- On the network, you only give authentik access to your users, your Outpost, and nothing else
- Your end applications that need access to your Outpost can only access your Outpost and nothing else
Other cases where microservices make sense
Solving for organizational scale
Openstack, the distributed hypervisor, is developed as microservices. Nova is in charge of compute, Neutron does networking, Cinder handles block storage, and so on. Each of these services is made by a different team at Openstack.
At some point those services need to talk to each other, which happens via a standardized API that’s exposed to other services. Then, internally, they can break stuff all they want, as long as they have the stable API that’s exposed to the outside. Teams can evolve and iterate on their own without depending on other teams, except when they need to make a breaking change on this external API.
For Openstack, microservices make sense because they have several teams working on very specialized problems that don’t affect other parts of the system. The services are so substantial in themselves they are more like monoliths than microservices. If you’re at the point where you could split your monolith into several smaller monoliths (or “modular monoliths” in Shopify’s case), then microservices-type architecture may be the right choice.
What companies often get wrong is that they try to apply “designing with scale in mind” (of which we are usually big fans) to things that don’t scale down as well as up.
“This has been a problem in a few places I’ve worked at that decided to build ‘microservices’ (read: simple apps that moved the complexity to a higher architectural level, i.e. by having every service talk to every other service over a REST API or event bus), not because it solved a problem they had, but because it MIGHT solve a problem they’d LIKE to have.” — Cthulhu on Hacker News
Unless you have multiple large teams working on independent problems, microservices are going to create unnecessary complexity.
You can still scale up performance within a monolith
If you’re contemplating microservices to solve a performance problem (or anticipated demand), you can often solve for this within a monolith.
For example, we have a few heavier endpoints in authentik that need to make many individual checks. The first page you see when logging into authentik is all the applications that you have access to.
That page is rendered by authentik checking whether the user can access each application in authentik’s database. This process involves checking your organization’s policies and bindings (e.g. only the 1Password SSO users group can access 1Password, only the default admin user can access AWS).
You also might have Python expression policies to check whether the user is accessing from an authorized IP, for example. Running Python can be quite slow, so if we wanted to speed up the loading of this page (instead of extracting this page to a microservice) we can allocate some part of our deployment to handle requests to this endpoint so they can get dedicated resources to handle the load. Another example is Matrix and the Synapse homeserver, which extracted resources-heavy endpoints and tasks to other processes, while keeping the same codebase.
If you’re considering whether microservices is the right approach, the two questions you should be asking are “Why?” and “Why now?”.
“… introducing more complexity when there is a pressing need, could be a more pragmatic approach than starting with a complex setup with potentially hundreds of microservices.” — The Pragmatic Engineer, The end of 0% interest rates: what it means for software engineering practices
If you’re solving a technical challenge or looking for flexibility, you might be on the right track. Otherwise you might be trying to solve problems you don’t yet have, at the expense of things working now.
As always, we look forward to hearing your thoughts on this take. Send us an email to hello@goauthentik.io or reach out on Discord!