DEV Community

Arpad Toth for AWS Community Builders

Posted on • Originally published at arpadt.com

An easy and secure way to protect API Gateway endpoints

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 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,
  }
);
Enter fullscreen mode Exit fullscreen mode

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],
  })
);
Enter fullscreen mode Exit fullscreen mode

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;
  }
};
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)