— 10 min read
Using API Gateway to deliver egress IPs with AWS and Terraform
Here at Airkit, we provide an HTTP(s) endpoint (ip-ranges.airkit.com) that produces a JSON list of all possible egress IP addresses that traffic could use when establishing a connection between Airkit and any other external service.
Hopefully one day the internet as a whole can move past deny/allow lists to full zero-trust security models. Until that day comes, we built this to help provide both granularity and dynamism to our customers, as they use this information to help in managing their own firewall deny/allow rules. As a self-service endpoint, our customers can access this always-up-to-date information whenever they want.With the continued growth of our services, the incorporation of new geographical locations, and use of modern high-availability upgrade procedures, the chances of this list remaining unchanged becomes smaller. Where in years past this information would have been kept up to date by hand, modern firewalls are often capable of directly using this information to implement changes.
Technically speaking, the API we created is derived from a hello-world API, that we then wrapped with a few extra bonus features (caching, http-redirect, etc). Below we will step through the design and implementation, with the understanding that the code snippets provided are primarily the deviations from an otherwise-standard hello-world example.
To do this in AWS, we built an API that has a CDN in front and is backed by a Lambda function. In diagram form, the service we created looks like:
- Public internet traffic hits the DNS
ARecord points to the CloudFront distribution (ex:
- The CloudFront distribution is responsible for:
- Edge caching
- Forwarding traffic to the API Gateway endpoint
- The API Gateway then executes the
- The Lambda function does the heavy-lifting:
- Uses a limited IAM Role specifically for describing our NAT Gateways
- Loops through all Airkit deployment regions
- Coalesces and returns NAT Gateway IP addresses from every region
API G(ateway) + Lambda
The CDN & DNS are definitely usability adds; However, the true functionality is provided by this powerhouse APIG + Lambda combination.
Take a look at the following NodeJS that we use in our Lambda:
- The EC2 Service Interface is configured to connect to specific
regions. This makes the individual requests in parallel, as they eventually populate the
- The contents of
paramsincludes a Filter; Specifically filtering by AWS tags containing
proxy. This narrows the returned NAT Gateway list to only contain the specific NAT Gateways for our egress HTTP proxy. NOTE: Filtering by tag requires the tags to have been setup previously.
- Errors are logged if they occur, and otherwise the
dataobject is looped over, using a Set()+Array() to provide a unique list of the filtered NAT Gateways IP addresses
- We tie this all together with a HTTP-specific return, with
bodyset to return the
resultobject we created that contains the list of addresses.
IAM Role: Describe NAT Gateways
Let’s start by giving the above Lambda function permissions to carry out the AWS API functions we set it up for; namely describing NAT Gateways:
Breaking down the above terraform, we have:
assume_rolepolicy document that is actually a Trust Policy defined to allow Lambda to use the Role permissions we are about to define. This is done by giving the
lambda.amazonaws.comService Principal permission to call the
assume_rolepolicy document must then be attached as the
assume_role_policyfor the IAM Role that our Lambda application will use to execute with
accesspolicy document that is where we define 2 statements to be used by the core functionality of our Lambda application code:
- Allowing the
ec2:describeNatGatewaysaction to be run against all accounts/regions
- Allowing 3 different Logs actions to let us log our console output to CloudWatch
- Allowing the
accesspolicy document must then also be attached as an additional policy to the same IAM Role
HTTPS: Certificate Validation
In order to share the same certificate between our Lambda (running in
us-west-2) and CloudFront (running in
us-east-1) we need to make sure the certificate exists in both locations in ACM.
To do this, we run the following:
The important takeaways from the above:
- We create the 2 different (but identical) certificates because AWS services are dependent on having regionally-colocated certificates.
- By using
DNSvalidation, we can auto-generate the required Route53 records by looping over the combined
domain_validation_optionsfrom the certificates.
- We use a certificate validation resource (aka:
aws_acm_certificate_validation) specifically to wait for the
DNSvalidation to succeed, and then use the validated certificate inside our
aws_apigatewayv2_domain_name(the API Gateway will use the certificate deployed in
us-west-2while the CloudFront Distribution will use the certificated deployed in
API Gateway: HTTP API
With AWS API Gateway (v2 because we are creating an HTTP API) one would typically define some endpoint
route_keys that would execute specific APIs. However in the Airkit case, we want to access the API without needing an API route.
A few key callouts from the above:
- We specifically set the default route settings (aka:
throttling_rate_limit) to a low value to prevent our API from potentially causing runaway Lambda costs.
- Because we created an HTTP API (API Gateway v2) we use the special
$defaultroute key that is available to us. Using this allows the base URL to be hit as the endpoint for our API.
What we should have defined at this point is a functioning API Gateway endpoint URL that we can access in order to call our Lambda.
CloudFront: Cache & HTTP Redirect
AWS CloudFront is typically deployed as an edge-cache to be backed by some form of S3 bucket. In our case however, we want the backend to be the API Gateway we just created.
Additionally, because we can perform HTTP-to-HTTPS redirecting in AWS CloudFront, we will use that feature to compensate for the missing HTTP endpoint in the API Gateway.
Breaking down the
http_redirect distribution, there are a few key elements that make this work:
us-east-1provider is because CloudFront in conjunction with ACM is locked to that region
- Regex is used to parse out the
origin_pathfrom the URL endpoint provided by API Gateway. (You can also view this “Invoke URL” in the AWS Console)
custom_origin_configis required to prevent CloudFront from assuming an S3 backend
- We pass in the
var.domain_name(ex: prod.airkit.com) and
var.subject_alternative_names(ex: Automate Digital Customer Experiences Faster ) as variables for use in both attaining SSL/TLS certificates from ACM, but also in naming our
target_origin_idlater in the file), and providing CloudFront with distribution
redirect-to-httpsfixes our previously-missing handling of HTTP requests
Route53: Alias for CloudFront
Again breaking it down:
- We loop over all Domains and SAN (Subject Alternative Names), creating an alias for each within the same Route53 HostedZone, namely
domain(In Airkit’s particular case the ZoneID is always the same for any given set of Domain + SAN)
- We point the alias destination to the CloudFront distribution we just set up. Completing both the
HTTPpaths from our URL to our Lambda function.
Airkit’s entire list of egress IP addresses is available now at ip-ranges.airkit.com. This is not a substitute for authentication, proper access controls and AI-driven attack mitigation. However, it can provide an additional level of granularity to an existing security stature, hopefully driving an experience that both we and our customers can love.
After all, empowering our customers to build experiences that people love, is what we do.