Securing Containerised Entry Points with AWS HTTP APIs, Cognito, VPC Link & ALB
Using AWS’ “new” HTTP API Gateway to connect to a private Application Load Balancer as an ECS entry point.
As modern user facing applications gravitate towards mobile & SPA designs -resulting in the separation of the backend and frontend - we need a way to publicly expose these backend services securely, while also only allowing access to authorised recipients.
Many of these backend environments represent some sort of containerised environment. Containerised environments have transitioned from the ‘latest fad’ to the ‘norm,’ leaving application deployments on raw servers and VM’s behind in obscurity. This, together with the cloud movement, has created an entirely new landscape of how we release applications and it is essential that we know how to secure them.
This article will propose one solution to the problem. I will be securing an Application Load Balancer that distributes traffic to a various ECS tasks. It is worth noting that in a containerised environment this ALB could be an Ingress for EKS or a docker swarm entry point running on EC2.
Below, you can see a bird’s eye view of the architecture showing how we can use AWS’ new HTTP API Gateway to connect to a private ALB via a VPC Link. The ALB will be using path-based routing to distribute traffic to two ECS tasks.
All Infrastructure was coded in Terraform, which can be found here (If you would prefer to read code :) ).
Let’s dive into each service:
HTTP API Gateway
In December 2019 AWS announced HTTP API Gateway as the latest feature to the API Gateway. With HTTP API promising cheaper, easier to use and improved performance when compared to the regular Rest API, it is the perfect proxy for our private ALB. However HTTP is not as feature rich as REST API, if you don’t believe me, a feature comparison can be found here. However, the core feature for the purposes of this project is the ability to privately integrate with AWS’ ALB, a feature that is currently not available with REST APIs so we’ll stick with HTTP API. Refer to the below for creating an HTTP API Gateway with Terraform:
First, we’ll create the HTTP API, making sure we set the protocol type to “HTTP” (There are only two options). Then we move onto the route, which directs traffic to the correct backend service. Multiple routes can be linked to a single API. The route is comprised of two components, the HTTP method and the path. In the above example, this route accepts any HTTP method and uses the greedy variable to route all traffic through this route “ANY /{proxy+}” . The target for our route will be an integration.
A integration connects an API to a backend resource, with multiple integration types possible. The above integration connection type is a VPC link, which gives us the ability to connect to our private Application load Balancer. The above example uses HTTP/TCP but TLS configuration can also be set up for this private integration. The last required component for our API is the stage. An API stage is a reference to a point in the lifecycle of an API (dev, uat or prod). The deployed API will then have the stage name as the prefix to the API url. In the above example, the default stage was used. By using the default stage the API will be serviced for the base url (ie. no prefix).
Moving along (and down the stack), we arrive at the VPC link. (The link between the public world and our private workload).
VPC Link
When creating a VPC Link, API Gateway creates and manages elastic network interfaces for the VPC link. This is how the VPC Link can connect to our private resources. A VPC Link can be created as easy as:
Cognito
Before we get ahead of ourselves, we need to make sure that only authorised users can call our API. Luckily, AWS HTTP API can integrate with JWT authorizers.
For the purposes of this article, a very rudimentary Cognito user pool and app client was created. The app client attached to the user pool only has the OAuth Client credential flow enabled (Client credential flow should only be used for machine to machine communication, for authenticating users and other OAuth flows, more info can be found here).
Now we need to attach an authorizer to the HTTP API Gateway using the newly created Cognito user group. As seen below, the JWT authorizer configurations take the client ID as the audience and the Cognito endpoint as the issuer:
Now that we have secured the entry point to our AWS environment, let’s see how the private ALB can be used to route traffic to different ECS services.
ALB
AWS has a variety of different load balancer services. This particular design used an Application Load Balancer, due to its ability to route traffic on level 7 of the OSI Model (ie: path based roting). Each of the microservices expose APIs on a specific base path, therefore an ALB was a perfect fit.
*Note on the Terraform module design:
We want the ability to continuously add microservices to the ALB as more services are introduced to an application. The creation of the ALB resource was separated from the ALB listeners. The ALB listeners are created in the ECS services module (i.e a listener is created together with its respective microservices).
So the creation of the ALB modules and a default listener is as follows:
ECS Fargate
As mentioned earlier, containerised environments have become the norm in the IT environment. Some of the reasons for it rapid rise to wide scale implementation is its increased speed of application delivery, portability and more efficient operations. It is also easier (subjective) to migrate most workloads to a containerised environment. This article servers as a reference on how to securely expose containerised environments and not the different environments themselves. So I won’t go into details of the ECS Fargate creation. This architecture can be used to expose a variety of containerised environments (K8, docker swarm etc) and not exclusively ECS Fargate. The full Terraform code for the creation of ECS Fargate and it services can be found in the github repo.
Now that all the resources are in place…
DEMO TIME!
It’s time to see all the above resources in action together in order to access two dummy microservices. The two microservices (service A and service B -original naming, I know) are simple springboot applications which expose a single API that says ‘Welcome to Service A/B’. The code for the microservices is also in the repo.
After deploying all the mentioned resources and attaching a custom domain to the HTTP API Gateway, let’s try access the microservices:
Hmmm, well that is a good sign. The HTTP API Gateway is blocking access to the microservices (the whole point of the article). To gain access to these microservices an OAuth access token linked to the client app that was created as part of the Cognito user pool is needed. Remembering we only enabled the client credential OAuth flow to our client app, the access token can be accessed using the following curl:
The above will produce an access token as seen below:
We can then use this token in the Authorization header of the request. (I used the Modheader chrome extension to edit my request headers). Now when we try access microservices again:
BOOM! We have successfully sent a request to our HTTP API Gateway, which then did a JWT authorizer confirmation with cognito, then passes the traffic through a VPC Link to our private AWS environment. Once the request hits our ALB, the ALB performs path based routing on ‘service-A’ or ‘serivce-B’ to the respective springboot application running in a container on ECS Fargate.
Conclusion
With the pace at which applications are getting released and updated these days, its becoming ever so imperative that we build simple but secure cloud environments. As was shown, this simple architecture allows you to deploy private containerised applications into AWS while also securely exposing them to the internet, without the need of public subnets, internet gateaways etc.