Suppose someone has a legacy application to migrate to the cloud, but it is missing some of the security layers, such as authorization and TLS encryption, that are essential in this day and age. What can they do about that?
This post aims at describing a combination of two patterns that can be used to add such cross-cutting features to an application, and effectively build a secured tin-can phone between two services.
A simple application following that description looks like this:
- The order service somehow receives a request to create an order.
- Once processed, the order service creates a fulfillment order in the fulfillment service.
- At the moment, calls are neither are authorized or encrypted.
Both services are running on aging infrastructure, and a poor soul has been tasked to “lift and shift” these somewhere, and possibly start the creation of an “API”. The code has aged and is crippled with technical debt, and adding an authorization layer in and outbound would be a costly and risky task. The goal is do so with little to no changes to the application itself.
The patterns being used in that situation are:
- decorator, to add infrastructure features to inbound interfaces, such as authorization,
- ambassador, to do the same for outbound requests.
While both patterns can be leveraged independently for the same intent, this post is focusing on their conjoint use.
Modernization through containerization
One of the solutions consists in containerizing the application and lifting it to a container orchestrator, such as Kubernetes. Moving to an orchestrator already brings some value by itself, such as infrastructure automation and scalability. More advanced configurations can provide networking lock-down through frameworks such as istio and envoy/ambassador, and can add TLS encryption with Cert Manager.
But it still doesn’t add an authorization scheme to your application, and despite all the attention and benefits of such orchestrators, they still have security breaches once in a while - authorization is better.
For the remainder of this post, it is assumed the application has been containerized and lifted to Kubernetes.
Ambassador pattern to add outbound authorization and encryption
The first problem is that the service doesn’t authorize its outbound requests. There is no way for the services being called to determine where the request is coming from, or whether it is authorized.
The pattern used to solve that problem is ambassador1. An ambassador is a local service that acts as a proxy to the remote service, and enriches outbound requests with infrastructure functionalities2. In this case, the interface of the ambassador will look exactly like that of the service being secured, i.e. it’s offering a local anonymous endpoint, and the service is configured3 to point to the ambassador locally instead of the actual service. Then the ambassador acts as an adapter proxy to the new, secured service.
- An ambassador sits next to the service on the same host.
- The service send an anonymous request to the ambassador
- The ambassador handles authorization to the authorization service
- The ambassador forwards the now authentication request to the secured service using https instead of http.
This code is an example of the ambassador pattern. It creates a proxy to a remote authorization service that authenticates the service and authorizes it to access a given
AUDIENCE, then forwards it to a configured service. The same pattern is used to convert outbound unencrypted calls to https calls.
With an authorization ambassador the application can now call authorized endpoints.
Decorator pattern to add inbound authorization
The second problem tackled here is that the fulfillment service accepts anonymous requests. In the case of a deployment on public cloud, requests should be authorized before they are served.
The pattern being leveraged is the decorator pattern. While originally described as a code pattern by the GoF, decorator can also be used at a component level in this use-case ; the problems it solves4 and the solution it offers5 fit perfectly with the issue at hand.
The application of the decorator pattern in this case looks like this:
- A decorator seats in front of the service on the same host,
- The client sends authorized requests to this decorator instead of the service
- The decorator validates the request based on the selected authorization scheme (e.g. validates an oauth2
- Provided that authorization was successful, the decorator invokes the service, then forwards its response to the client.
- When authorization fails, the decorator declines the request and drops it.
This code is an example of a decorator, which validates the signature of an access token, and that it was issued for the correct audience. This example could be extended to validate some custom claims, or that the client is allowed for a specific method and a specific endpoint, etc.6
With a decorator, the application now authenticates requests to its endpoint, time to put it altogether.
Orchestrating both patterns
When lifting a containerized service to Kubernetes, the service gets deployed in a “Pod”. Pods are logical boxes that can group containers together, they are outlined by blue boxes in this post. Pods behave like a single host: unless you mention it in the configuration, every ports are closed to the external world, but containers deployed within the pod can chat together on
localhost, where unencrypted, unauthorized calls are more acceptable1.
An authorization decorator gets deployed with the fulfillment service, and an ambassador gets deployed with the order service. The decorator and the ambassador containers are commonly referred to as sidecars: utility containers that get deployed alongside a functional container and provide it with local services.
No port to the actual order or fulfillment services gets opened, only to the authorization decorator. If you want to invoke the fulfillment service, requests need to be authenticated first, or they won’t be passed to the actual service.
Similarly, the unauthorized order service calls to the fulfillment service are only local to the ambassador. Then the ambassador adds the authorization information before forwarding the requests.
This couple of patterns effectively creates a private line between the order and fulfillment service, without having to modify the codebase for any of these two services, with ambassador and decorator acting as the tin-cans of your tin-can phone.
This pattern can then be applied to all components of the architecture requiring to be retrofitted into the authorization scheme - order service would receive its own authorization decorator, its clients use an authorization ambassador, etc.
As illustrated above, the coupling between an ambassador and its counterpart decorator is only virtual: the decorator of a service is creating a single “secured” back door for all clients. Respectively, depending on implementation choices, a single ambassador could be used to enhance all outbound calls. Services that already support the desired feature (such as authorization) don’t have to implement to pattern.
The apparent complexity of the architecture above is only visual. Most likely, all ambassadors (respectively: decorators) will be the same container image configured for a different audience or to point to another service.
This is a complete example of this pattern for Kubernetes, including ambassador and decorator, sample services and a lightweight authorization service. As illustrated in this solution, the code for this pattern is relatively simple to implement.
These two patterns are practical to add features to an existing application, without having to modify it. Combining them together allows to upgrade the entire architecture with new features.
Their combination can also be leveraged for other use-cases, such as
- transforming XML interfaces or JSON,
- adding logging of incoming/outgoing requests
- adding TLS (https)
There are two main costs that need to be considered for that solution:
- additional resource utilization - which is relatively limited (a few megabytes of memory and transparent CPU usage in this examples), and
- networking delays due to additional HTTP calls - these are fairly limited since calls are local, but they might still add a few milliseconds to each requests, and it needs to be tested.
Finally, these patterns have been used here for modernization purposes, but they can legitimately be considered for new applications. This decouples platform functionality from business functionality at the infrastructure level, rather than code level. The main benefit is that updating platform (e.g. changing the claim structure, updating encryption, etc.) requires only a deployment of the new sidecar container rather than having to upgrade the library in the code. This can be used to update all of the architecture at once, rather than having to synchronize with the development teams to do so.
such as authorization, circuit breakers, https, etc. ↩
either by changing a URL in a configuration file, or using a
etc/hostsfile to point to localhost. ↩
Responsibilities should be added to an object at run-time ↩
implement the interface of the extended (decorated) object (Component) transparently by forwarding all requests to it and perform additional functionality before/after forwarding a request. ↩
since activating encryption is a matter of deploying a TLS certificate and turning on TLS on the Kubernetes service, it’s not covered here. But the same pattern could be used for other orchestrators or systems like Swarm to add https tunneling. ↩