An easy and secure way to protect API Gateway endpoints
1. The scenario
Bob’s newest task is to secure the communication flow between the company’s microservices. They have to use REST API, so each service has a dedicated API Gateway provisioned.
APIs are public by default, so Bob must protect them from unwanted traffic. He decides to use AWS IAM to authorize requests for interservice communication.
2. The solution
AWS IAM is the most secure way to protect endpoints. We use policies and request signing to control access to the API.
For the sake of simplicity, let’s assume that Bob has two microservices. Both of them use Lambda functions, but the principle would be the same for other types of services, like ECS.
He set up the infrastructure in CDK.
2.1. Defining AuthorizationType: AWS_IAM in API Gateway
I will use the sample GET /pets
endpoint to demonstrate the AWS IAM authorization workflow. This API is available for everyone to try out API Gateway.
The relevant part of the infrastructure code can look like this:
const petsApi = new RestApi(this, 'PetsApi', {
restApiName: 'PetsApi',
description: 'AWS IAM auth demo',
deploy: true,
endpointTypes: [EndpointType.REGIONAL],
});
const pets = petsApi.root.addResource('pets');
const getPets = pets.addMethod(
'GET',
new HttpIntegration(
'http://petstore.execute-api.us-east-1.amazonaws.com/petstore/pets'
),
{
// This is the most important thing!!
authorizationType: AuthorizationType.IAM,
}
);
It’s the same HTTP Integration
as the sample PetsApi
we can set up in the Console.
We should set authorizationType: AuthorizationType.IAM
for the GET /pets
endpoint. The value of the enum is AWS_IAM
, which will tell API Gateway to connect to AWS IAM and check if the calling entity has the relevant permissions.
2.2. Adding permission to the Lambda execution role to invoke the endpoint
The CDK code for the Lambda function that calls the other service’s API Gateway can look like this:
const allowedFn = new NodejsFunction(this, 'allowed', {
bundling: {
target: 'es2020',
keepNames: true,
logLevel: LogLevel.INFO,
sourceMap: true,
minify: true,
},
runtime: Runtime.NODEJS_16_X,
timeout: Duration.seconds(6),
memorySize: 256,
logRetention: RetentionDays.ONE_DAY,
environment: {
NODE_OPTIONS: '--enable-source-maps',
API_URL: `${petsApi.url}pets`,
},
functionName: 'AllowedFn',
});
// add permission to invoke the GET /pets endpoint
allowedFn.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['execute-api:Invoke'],
resources: [getPets.methodArn],
})
);
If we don’t set up proper permissions, we will receive a denied response from the GET /pets
endpoint. Bob deployed both services to the same account, so adding the permissions to the Lambda function’s execution role will work here. We could also add the relevant policy to the API Gateway’s resource-based policy.
2.3. Signing requests with aws-sdk/signature-v4
The last compulsory step is to sign the request to the endpoint. If we don’t do it, API Gateway will deny the request with a 403
error.
This post describes how to sign requests using the signature-v4
package available in the AWS SDKs.
The Lambda function handler can have the following minimal code:
import axios from 'axios';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-js';
const { API_URL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN } = process.env;
const apiUrl = new URL(API_URL);
const sigv4 = new SignatureV4({
service: 'execute-api',
region: 'us-east-1',
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
sessionToken: AWS_SESSION_TOKEN,
},
sha256: Sha256,
});
export const handler = async () => {
const signed = await sigv4.sign({
method: 'GET',
hostname: apiUrl.host,
path: apiUrl.pathname,
protocol: apiUrl.protocol,
headers: {
'Content-Type': 'application/json',
host: apiUrl.hostname,
},
});
try {
const { data } = await axios({
...signed,
url: API_URL,
});
console.log('Successfully received data: ', data);
return data;
} catch (error) {
console.log('An error occurred', error);
throw error;
}
};
The linked post explains the signature process in detail.
3. Testing
Now that we have all three mandatory components, we can deploy the stack to the cloud and test the architecture.
If we invoke the Lambda function (for example, by creating a Test event where the input can be an empty object), we should get a successful response with the pets.
4. Alternative solutions
AWS_IAM
is not the only way to controlling access to an API. We can use Lambda authorizers or Cognito (Part 1 and Part 2) for securing internal microservice communications.
5. Summary
IAM provides the most secure API protection with minimal setup when we want to control access to API Gateway.
The architecture must fulfill three necessary conditions.
First, set the authorizer type to AWS_IAM
. Second, add permission to invoke the API to the calling service. Third, sign the request with a suitable package.
AWS_IAM
is a good choice for authorizing internal communication between microservices.
6. References, further reading
Controlling and managing access to a REST API in API Gateway - Ways to control access in API Gateway