ForwardAuth with Traefik: Streamlining Security for Microservices
Tomaz Lovrec
Posted on October 11, 2024
In the information age, nothing is more important than the security of our services—and more importantly, the security of our users and their data. Ensuring security within a (micro)services architecture can quickly become complex and burdensome. As services grow and more spring to life within the ecosystem, maintaining consistent and reliable access control becomes a top priority. To streamline access control, we can leverage Forward Authentication, which centralizes the authentication mechanism and simplifies securing our (micro)services.
While many reverse proxies offer functionality for routing and authentication, Traefik stands out with its ease of use and flexibility. In addition to efficiently routing traffic, Traefik’s integration with Forward Authentication (ForwardAuth) enables us to centralize user authentication across all services. Using a centralized authentication not only makes the access management easier, but also strengthens the security of our services at the gateway level, making Traefik an excellent choice for the task.
In this post, we will look into the process of setting up and configuring ForwardAuth with Traefik and explore some advantages of using this authentication method within a (micro)services architecture to secure access to a protected Order resource in our hypothetical application.
What is Authentication
First a quick look into what authentication actually is. When we ask our users to authenticate, we are essentially asking them to prove to our application that they are who they say they are. In the most basic form of authentication, the users often prove their identity by supplying our application with an email address, or a unique username, and a password. Since the communication between the user/client and the server in the application leveraging the HTTP protocol is more often than not stateless, the user must prove their identity with every request.
If an attacker can obtain the users password which they are using to prove their identity with, the attacker can now identify as the user to our application at any time, until the user changes their password. And since users must prove their identity with the password on every request, the attacker has a lot of chances to get to the users password. To mitigate this, the access control system in our application will usually issue a short lived access token to our user, after they have proven their identity with their password. After this point, the user can use this access token to prove their identity, until the token expires, and they have to obtain a new one.
What is Forward Authentication
When our application now receives a request to the protected GET /order/{id} resource endpoint, it verifies that the request also contains the access token, and it ensures that the token is valid and belonging to a user. Since we are in a (micro)services architecture and we are using multiple services to serve different resources to our users, as in example, the request to retrieve an order with the ID {id} is handled by the order
service, we would typically need to implement identity verification in each service, or rather implement an authentication
service which will verify the identity of the users for us through the POST /auth/verify endpoint.
Instead of having each service verify user identities by calling the authentication
service to verify the users identity, which quickly becomes cumbersome and can lead to inconsistencies between services, we can employ the Forward Authentication mechanism in our reverse proxy, Traefik. When using ForwardAuth each request that is received to a protected URI, like in example GET /order/{id}, Traefik will automatically forward the request to our authentication
service for user authentication, before sending the request onward to the resource service, in our example, order
.
Configuring ForwardAuth in Traefik
Below we have hypothetical service and http router definitions for our authentication
and order
services and routers in Traefik:
[http]
[http.routers]
[http.routers.order]
rule = "Host(`order.domain.tld`) && PathPrefix(`/order`)"
service = "order"
[http.routers.authentication]
rule = "Host(`order.domain.tld`) && PathPrefix(`/login`)"
service = "order"
[http.services]
[http.services.order.loadbalancer]
[[http.services.order.loadbalancer.server]]
port = 8000
[http.services.authentication.loadbalancer]
[[http.services.authentication.loadbalancer.server]]
port = 8000
In this configuration we have configured 2 services that we are running with docker, order
and authentication
, and Traefik will route all requests to https://order.domain.tld/order to the order
service and all requests made to https://order.domain.tld/login the authentication
service. We do not need to route the /auth/verify route, since we are going to be calling this only inside the same docker network bridge.
With this setup, the order
service would have to manually call https://authentication:8000/auth/verify manually on each request made to the /order/{id} endpoint, and of course any other endpoint or service would need to do the same. To centralize the authentication and not require each service to handle it individually, we are going to use ForwardAuth by defining a middleware in Traefik and add it to our router for the order service:
[http]
[http.middlewares]
[http.middlewares.auth-user.forwardauth]
address = "https://authentication:8000/auth/verify"
[http.routers]
[http.routers.order-secure]
rule = "Host(`order.domain.tld`) && PathRegexp(`/order/[0-9]+`)"
service = "order"
middlewares = ["auth-user"]
[http.routers.order]
# kept the same as before
# services defined as before
We have not defined a new ForwardAuth middleware with the name auth-user
, and instructed Traefik to redirect all requests made to all routers that employ this middleware to the https://authentication:8000/auth/verify address. Further on, we have now added a new router, order-secure
which uses the auth-user
middleware, and catches only requests that are made to https://order.domain.tld/order/{id} address while keeping the https://order.domain.tld/order requests non-authenticated.
Now whenever the order
service receives a request to the /order/{id} URI, we know we can trust this request as it was already forwarded to our authentication
service for user identity verification by Traefik and we can proceed servicing the request right away.
Sharing information between the services
In real-world scenarios, we might also want to share some user information from the authentication
service to the order
service once the user has been authenticated, since the authentication
service will already have probably accessed the database, and we want to limit the access to the database in the downstream services in order to help maintain high performance.
To achieve this, we have a couple of options:
- use of JSON Web Tokens or JWT
- set any required data to the response headers with the
authentication
service
Use of JSON Web Tokens
If we are using JWTs for authentication of the users, we can already keep the required user data in the JWT itself, since it was designed for exactly this. When the user sends the JWT in the request headers, Traefik will naturally forward this JWT to the authentication
service through the ForwardAuth middleware, as well as send this JWT on to the order
service, where the authentication
service can check the validity of the JWT token and the order
service can extract user data from it.
Setting required data to response headers after authentication
Since JWTs expose user data, we might want to avoid that, and can therefore set any user data that the downstream services will require in the response headers of the /auth/verify call and instruct Traefik to add data from that specific header to the follow-up request headers that will be sent to the order
service. Ideally in a way where the downstream services will be able to verify that the data was indeed set by the authentication
service. We achieve this by adding a slight modification in our ForwardAuth middleware:
[http]
[http.middlewares]
[http.middlewares.auth-user.forwardauth]
address = "https://authentication:8000/auth/verify"
authResponseHeaders = ["X-Authenticated-User"]
# all else remains the same
Putting it all together
Now let’s put everything we have learned together:
[http]
[http.middlewares]
[http.middlewares.auth-user.forwardauth]
address = "https://authentication:8000/auth/verify"
authResponseHeaders = ["X-Authenticated-User"]
[http.routers]
[http.routers.order-secure]
rule = "Host(`order.domain.tld`) && PathRegexp(`/order/[0-9]+`)"
service = "order"
middlewares = ["auth-user"]
[http.routers.order]
rule = "Host(`order.domain.tld`) && PathPrefix(`/order`)"
service = "order"
[http.routers.authentication]
rule = "Host(`order.domain.tld`) && PathPrefix(`/login`)"
service = "order"
[http.services]
[http.services.order.loadbalancer]
[[http.services.order.loadbalancer.server]]
port = 8000
[http.services.authentication.loadbalancer]
[[http.services.authentication.loadbalancer.server]]
port = 8000
Note: the above configuration is omitting HTTPS configuration as it is beyond the scope of this post.
And here we have it, congratulations! We now have a secure GET /order/{id} endpoint in our order
service, and to secure any more services or endpoints, we simply need to add the middleware to their route definitions. Now let's take look at the lifecycle of the GET /order/{id} request:
Posted on October 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.