An easy and secure way to protect API Gateway endpoints
Tags: aws, security, api, serverless
Published: 2022-06-23 05:37:26

We can protect our APIs using AWS IAM, which is the most secure way of controlling access to our endpoints. We can quickly set it up with no code and minimal configuration.

## 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 <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a> provisioned.

APIs are public by default, so Bob must protect them from unwanted traffic. He decides to use <a href="https://aws.amazon.com/iam/" class="link" target="_blank" rel="noopener noreferrer">AWS IAM</a> 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 <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> functions, but the principle would be the same for other types of services, like <a href="https://aws.amazon.com/ecs/" class="link" target="_blank" rel="noopener noreferrer">ECS</a>.

He set up the infrastructure in <a href="https://aws.amazon.com/cdk/" class="link" target="_blank" rel="noopener noreferrer">CDK</a>.

### 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:

```ts
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:

```ts
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.

<a href="https://dev.to/aws-builders/signing-requests-with-aws-sdk-in-lambda-functions-476" class="link" target="_blank" rel="noopener noreferrer">This post</a> 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:

```ts
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 <a href="https://dev.to/aws-builders/controlling-access-to-the-api-with-lambda-authorizers-5bbj" class="link" target="_blank" rel="noopener noreferrer">Lambda authorizers</a> or <a href="https://aws.amazon.com/cognito/" class="link" target="_blank" rel="noopener noreferrer">Cognito</a> (<a href="https://dev.to/aws-builders/controlling-access-in-service-to-service-communications-with-cognito-part-1-5dc1" class="link" target="_blank" rel="noopener noreferrer">Part 1</a> and <a href="https://dev.to/aws-builders/controlling-access-in-service-to-service-communications-with-cognito-part-2-gbl" class="link" target="_blank" rel="noopener noreferrer">Part 2</a>) 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

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-control-access-to-api.html" class="link" target="_blank" rel="noopener noreferrer">Controlling and managing access to a REST API in API Gateway</a> - Ways to control access in API Gateway