Containers

Securing API endpoints using Amazon API Gateway and Amazon VPC Lattice

Introduction

In microservices architectures, teams often build and manage internal applications that they expose as private API endpoints and publicly expose those endpoints through a centralized API gateway where security protections are centrally managed. These API endpoints allow both internal and external users to leverage the functionality of those applications. The separation of concerns between private and public endpoints allows customers to ensure that both public and internal security mechanisms use approved tools and services.

For public API endpoints, you can use Amazon API Gateway for north-south traffic. With API Gateway, you can enable access control mechanisms like OAuth2 and perimeter protection with AWS Shield Advanced, Amazon CloudFront, or AWS Web Application Firewall (AWS WAF).

For internal API endpoints, you can build your microservice applications using different compute options like AWS Lambda, Amazon Elastic Container Service (Amazon ECS), and Amazon Elastic Kubernetes Service (Amazon EKS). You can then deploy your applications across multiple AWS accounts and multiple VPCs with Amazon VPC Lattice. VPC Lattice enables east-west traffic across accounts, service discovery, traffic management, and access control. All traffic throughout this architecture is protected using encryption in transit.

Solution overview

The architecture includes public access for end users through Amazon API Gateway and private access through Amazon VPC Lattice.

Walkthrough

This post explores approaches to building this pattern using a number of AWS services and dives deep into how AWS secures your API workloads.

To see the code, visit AWS Samples in GitHub. The code samples are provided for proof-of-concept and testing purposes. Given the number and the complexity of the components that comprise this example, the code should be viewed as supporting material for this article and not for production purposes.

Inbound access through API Gateway

When exposing API endpoints to the public internet, you can use an edge-optimized or regional REST API endpoint as your centralized API endpoint for all north-south traffic. You can then add layers of security protections against a variety of potential attacks by using Amazon Cognito, Amazon CloudFront, AWS Shield Advanced, and AWS WAF.

Clients access the applications through a public API Gateway endpoint.

With API Gateway as the single public endpoint, you configure Amazon Cognito for authenticating and authorizing incoming API requests. Amazon Cognito functions as your identity provider, vending JSON web tokens (JWT) when a user successfully authenticates.

After configuring your user pool, users, and associated federation pool, the client application (1)  requests tokens from the Amazon Cognito endpoint. Here’s an example of a curl request for retrieving tokens from the Amazon Cognito identity provider endpoint:

curl -s -XPOST -H "content-type: application/x-amz-json-1.1" -H "x-amz-target: AWSCognitoIdentityProviderService.InitiateAuth" -d @etc/auth.json https://cognito-idp.us-east-1.amazonaws.com

The auth.json payload includes details associated with your client application:

{
    "AuthParameters": {
        "USERNAME": "your-user-name",
        "PASSWORD": "your-secure-password"
    },
    "AuthFlow": "USER_PASSWORD_AUTH",
    "ClientId": "your-client-id"
}

The Amazon Cognito endpoint responds with the following:

{
    "AuthenticationResult": {
        "AccessToken": "your-access-token",
        "ExpiresIn": 3600,
        "IdToken": "your-id-token",
        "RefreshToken": "your-refresh-token",
        "TokenType": "Bearer"
    },
    "ChallengeParameters": {}
}

Your client application (2) then uses the access token for making an authenticated request to the API Gateway endpoint, which uses the Amazon Cognito user pool as the authorizer.

curl -s -XPOST -H "Authorization: your-access-token" -d @etc/event.json https://your-api-id.execute-api.us-east-1.amazonaws.com/stage/

You can also configure the API endpoint with Amazon CloudFront (3) and AWS Shield Advanced for layer 7 distributed denial of service (DDoS) protection. Additionally, you can use AWS WAF (4) for blocking common web exploits like cross-site scripting or filter out unwanted traffic by IP address. These filter out unwanted traffic from reaching your API Gateway endpoint. For all other requests, configure access logs so that they are logged in a Amazon CloudWatch log group.

AWS Lambda function as a proxy

With a valid and accepted API request, you now aim to process and pass it along in a secure manner.

API Gateway uses a Lambda function to proxy requests into the VPC Lattice service network.

The request passes from API Gateway to an AWS Lambda function. By default, no principal is allowed to invoke your function. You configure a resource-based policy (3) to allow the API Gateway endpoint to invoke your function.

{
    "Effect": "Allow",
    "Principal": {
        "Service": "apigateway.amazonaws.com"
    },
    "Action": "lambda:InvokeFunction",
    "Resource": "arn:aws:lambda:us-east-1:445566778899:function:your-function",
    "Condition": {
        "ArnLike": {
            "AWS:SourceArn": "arn:aws:execute-api:us-east-1:445566778899:your-api-id/*/*/*"
        }
    }
}

The function (4) parses headers and the request body to determine where the request should be forwarded within your private network. You can also use the identity that was passed through the JWT token to restrict which API endpoints can be accessed within Amazon VPC Lattice.

The event payload for your function includes the access token that you passed through the Authentication header, both as the raw header token and parsed in the request context. You can also parse the raw token at jwt.io.

{
    (...)
    "headers": {
        "Authorization": "your-access-token",
        (...)
    },
    (...)
    "requestContext": {
        "authorizer": {
            "claims": {
                "sub": "...",
                "email_verified": "true",
                "iss": "https://cognito-idp.us-east-1.amazonaws.com/your-user-pool-id",
                "cognito:username": "your-username",
                "origin_jti": "...",
                "aud": "...",
                "event_id": "...",
                "token_use": "id",
                "auth_time": "1695509790",
                "exp": "Sat Sep 23 23:56:30 UTC 2023",
                "iat": "Sat Sep 23 22:56:30 UTC 2023",
                "jti": "...",
                "email": "user@example.com"
            }
        },
        (...)
    }
}

The function (4) then makes an API request to an API endpoint that is exposed privately within the VPC Lattice service network (5). To do this, first determine which credentials should be used to sign the request. You can use either the credentials of the Lambda function execution role or a role that the function is allowed to assume. Then you SigV4 sign the request with the selected credentials and submit the API request. If you want to use your function as a proxy, you could write code to send requests into the service network based on this example Python code.

This function acts as a proxy for all API requests that are targeted at the Amazon VPC Lattice service network. In other words, this function is invoked for every single API request. Therefore, this function should be performance optimized to reduce latency and cost overhead. For example, you could write the function in a compiled language like Go to get the function to invoke within 10-20 milliseconds. This minimizes the additional latency for the end-to-end API request and reduces the cost associated with the function invocation for every API request. If running the proxy as an AWS Lambda function doesn’t meet your performance or cost requirements, you can alternatively deploy this proxy as a container in Amazon ECS or Amazon EKS.

Network connectivity through Amazon VPC Lattice

With a signed request ready for submission to a private backend API, you can configure the Amazon VPC Lattice service network to allow the request to reach the appropriate target endpoint.

A Lambda function is used to proxy requests into the VPC Lattice service network and to the backend services.

You configure your AWS Lambda function with a security group (4a) in a VPC that is associated with the Amazon VPC Lattice service network (VPC-1). By default, new security groups start with only an outbound rule that allows all traffic to leave the function. Since AWS Lambda functions are invoked with event payloads and do not listen for inbound network connections, no inbound rules are required. Since this Lambda function is configured on a private subnet with no NAT Gateway for public Internet access, no additional outbound rules are added. If you need to restrict outbound access for your function to specific subnets or to other security groups, then you can update your outbound rules accordingly.

The Amazon VPC Lattice service network is configured in its own AWS account, typically in a central shared services account. You also configure an access log subscription in that account so that all requests across the service network are logged in a Amazon CloudWatch log group.

You then share the service network across accounts using AWS Resource Access Manager from the central shared services account. This allows each of the accounts to see the service network as a resource. You then configure a VPC association (4b, 6, 7, 8) in each of the accounts, which associates the VPC in each account with the service network.

The VPC association also includes a security group (4b), which defines who within the VPC is allowed to make inbound requests into the service network. For example, in VPC-1, the AWS Lambda function is configured with a security group (4a). The VPC association security group (4b) includes a rule that allows all traffic from the AWS Lambda function security group (4a), which allows the function to make API requests into the service network. Each VPC association is configured with a security group (4b, 6, 7, 8) to control who is allowed to make inbound calls into the service network.

You use different condition keys to configure an auth policy (5) for the service network to restrict traffic that passes through the network. For example, this auth policy allows traffic from only the VPCs that are specified in the policy condition:

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "vpc-lattice-svcs:Invoke",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "vpc-lattice-svcs:SourceVpc": [
                        "vpc-id-for-vpc-1",
                        "vpc-id-for-vpc-2",
                        "vpc-id-for-vpc-3",
                        "vpc-id-for-vpc-4"
                    ]
                }
            }
        }
    ]
}

Services in Amazon VPC Lattice

With a service network configured across accounts and VPCs, application teams can deploy backend APIs into their own AWS accounts which can then be accessed within the service network as an Amazon VPC Lattice service.

The VPC Lattice service network allows services to connect to each other in a secure manner.

When you create a service, you can configure a custom domain name (6, 7, 8) by attaching an AWS Certificate Manager (ACM) certificate (6, 7, 8) and configuring an Amazon Route 53 CNAME that routes traffic from your custom domain name to the VPC Lattice generated domain name like your-service-0123456789.abcdef.vpc-lattice-svcs.us-east-1.on.aws. Whether you use your own custom domain name or the VPC Lattice generated domain name, Amazon VPC Lattice terminates TLS for your applications, after which you can choose to also re-initiate a TLS connection over HTTPS to the backend services.

After you create a service, you need to associate the service to a service network. You can then configure an auth policy (6, 7, 8) for each service to complement the auth policy for the service network. This auth policy only allows traffic from the associated service network and allows only authenticated requests:

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "vpc-lattice-svcs:Invoke",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "vpc-lattice-svcs:ServiceNetworkArn": "arn:aws:vpc-lattice:us-east-1:001122334455:servicenetwork/your-service-network-id"
                },
                "StringNotEquals": {
                    "aws:PrincipalType": "Anonymous"
                }
            }
        }
    ]
}

An Amazon VPC Lattice service can be configured in its own AWS account, typically in a line of business account that has a VPC associated with the service network. You also configure an access log subscription in that account so that all requests to the service are logged in an Amazon CloudWatch log group.

Similar to load balancers, a service can use an HTTP or HTTPS listener to forward traffic to target groups. The target group can be configured with four settings: LAMBDA, ALB, IP, or INSTANCE. The following sections cover LAMBDA, ALB, and IP using an HTTPS listener.

Backends on AWS Lambda

A development team can choose to deploy their application using AWS Lambda functions.

This diagram shows a VPC Lattice service configuration with a backend Lambda function.

To configure a target group with an AWS Lambda function as the target, you need to use the following properties:

  ServiceTargetGroup:
    Type: AWS::VpcLattice::TargetGroup
    Properties:
      Type: LAMBDA
      Targets:
        - Id: your-lambda-function-arn
      Config:
        LambdaEventStructureVersion: V2

You also configure a resource-based policy (6) to allow the Amazon VPC Lattice target group to invoke your function.

{
    "Effect": "Allow",
    "Principal": {
        "Service": "vpc-lattice.amazonaws.com"
    },
    "Action": "lambda:InvokeFunction",
    "Resource": "arn:aws:lambda:us-east-1:223344556677:function:your-lambda-function-id",
    "Condition": {
        "ArnLike": {
            "AWS:SourceArn": "arn:aws:vpc-lattice:us-east-1:223344556677:targetgroup/your-lattice-target-group-id"
        }
    }
}

When AWS Lambda functions are configured as the target in the Amazon VPC Lattice target group, the function is invoked directly through the AWS Lambda data plane. Thus, the function can be VPC-enabled if it requires access to other VPC resources or needs to make API calls to other Amazon VPC Lattice services but is optional. In the following diagram, the AWS Lambda function only interacts with Amazon DynamoDB and doesn’t need to be VPC-enabled.

When the target group invokes the function, an event payload is used to trigger the function. Because the service policy is set with the condition that StringNotEquals: {“aws:PrincipalType”: “Anonymous”}, a SigV4 signed request is required. The event payload for a SigV4 signed request in Amazon VPC Lattice includes additional identity information in the request context:

{
    "requestContext": {
        "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:001122334455:servicenetwork/your-service-network-id",
        "serviceArn": "arn:aws:vpc-lattice:us-east-1:223344556677:service/your-service-id",
        "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:223344556677:targetgroup/your-target-group-id",
        "identity": {
            "sourceVpcArn": "arn:aws:ec2:us-east-1:445566778899:vpc/your-source-vpc-id",
            "type": "AWS_IAM",
            "principal": "arn:aws:sts::445566778899:assumed-role/your-function-role/your-function-name",
            "sessionName": "your-function-name"
        },
        "region": "us-east-1",
        "timeEpoch": "1695506039659581"
    }
}

In the request context, there are three different AWS account IDs represented:

  • 001122334455 for the central shared services account where the Amazon VPC Lattice service network was created
  • 223344556677 for the application account with the backend API application (in this case a AWS Lambda function)
  • 445566778899 for the source client account with the proxy AWS Lambda function

If an anonymous request is made and is allowed, then the identity in the request context of the event payload only includes the sourceVpcArn.

You can also look at the Amazon CloudWatch logs in the central shared services account with the Amazon VPC Lattice service network to investigate the access logs. When an authenticated request is made through the service network, you see the following snippet from an access log entry in the service network logs. It shows that a 200 response was returned, that an AWS Identity and Access Management (AWS IAM) role was resolved, and that authentication was allowed.

{
    (...)
    "responseCode": 200,
    "resolvedUser": "arn:aws:sts::445566778899:assumed-role/your-function-role/your-function-name",
    "authDeniedReason": "-"
}

Alternatively, when an unauthenticated request is made through the service network but is rejected, you see the following snippet. It shows that a 403 response was returned, that no user was resolved, and that the service denied the request.

{
    (...)
    "responseCode": 403,
    "resolvedUser": "Anonymous",
    "authDeniedReason": "Service"
}

Backends on Amazon ECS

A development team can choose to deploy their application using Amazon ECS with AWS Fargate behind an Application Load Balancer (ALB).

This diagram shows a VPC Lattice service configuration with a backend ECS service.

To configure a target group with Amazon ECS service behind an ALB as the target, you need to use the following properties:

  ServiceTargetGroup:
    Type: AWS::VpcLattice::TargetGroup
    Properties:
      Type: ALB
      Targets:
        - Id: your-application-load-balancer-arn
      Config:
        Port: 443
        Protocol: HTTPS
        VpcIdentifier: your-vpc-id

You configure an internal ALB with an Amazon Certificate Manager (ACM) certificate (7b) to terminate TLS and set up a Route 53 CNAME to redirect traffic from the custom domain name to the Amazon VPC Lattice generated domain name. The ALB also has a security group (7b), which you configure to allow traffic from VPC Lattice via a managed prefix list (5). The prefix list includes the local link addresses that originate from Amazon VPC Lattice.

  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ALB
      VpcId: your-vpc-id
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          SourcePrefixListId: pl-0123456789abcdef

If your Amazon ECS application needs to initiate connections into the service network, then you also need to update the VPC association security group (7a) to allow traffic from the Amazon ECS service/task security group (7c) on the appropriate ports.

Backends on Amazon EKS

A development team can choose to deploy their application using Amazon EKS with the AWS Gateway API Controller. Gateway API is an open-source project managed by the Kubernetes networking community and is a collection of resources that model application networking in Kubernetes. With AWS, the Gateway API is an implementation that integrates Amazon VPC Lattice with the AWS Gateway API Controller. You can find details on how to deploy the AWS Gateway API Controller in the deployment guide.

This diagram shows a VPC Lattice service configuration with a backend EKS service.

When installed in your cluster, the Gateway API controller watches for the creation of resources, such as a Gateway (8c) and an HTTPRoute (8d), and provisions corresponding Amazon VPC Lattice objects, as depicted in the following diagram.

This diagram shows the relationship between EKS resources and the Gateway API controller resources.

When you create a Gateway (8c) on the Amazon EKS cluster using the Gateway API controller, the controller creates a new Amazon VPC Lattice service network (5) using the name you provide in the Gateway. However, because the service network is shared across many accounts and is likely pre-created, you can specify the service network name (not the service network id) of an existing service network in the manifest, and the Gateway API controller links to the existing service network rather than creating a new one. You can also configure listeners for HTTPRoutes to use. For example, you can create an HTTPS listener (8d) using an ACM certificate for TLS connections.

apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name:your-service-network-name
  annotations:
    application-networking.k8s.aws/lattice-vpc-association: "true"
spec:
  gatewayClassName: amazon-vpc-lattice
  listeners:
  - name: https-custom-certificate
    protocol: HTTPS
    port: 443
    tls:
      mode: Terminate
      options:
        application-networking.k8s.aws/certificate-arn: your-acm-certificate-arn

Next you need to configure an HTTPRoute on your Amazon EKS cluster, which is associated with the Gateway API that you created above. The HTTPRoute creates the Amazon VPC Lattice service, listener, and target group. The service uses the listener that you created with the Gateway API and enables TLS termination for the service. The listener forwards traffic to the target group, which is setup as type: IP, forwarding traffic directly to the IP addresses of the pods (8f) of your ClusterIP service (8e).

As of the date of this post, when deploying Amazon VPC Lattice services via the HTTPRoute, an auth policy is not setup with the service by default. You should add the auth policy to your service (as outlined in the Services in VPC Lattice section) using your standard deployment mechanism. Support for attaching the auth policy through the HTTPRoute has been requested.

If you used eksctl to create your cluster, you need to update the eksctl-clustername-nodegroup-ng-X security group (8b) to allow inbound traffic from the VPC Lattice managed prefix. The Amazon VPC Lattice target group health checks originate from the Amazon VPC Lattice managed prefix. If your Amazon EKS application needs to initiate connections into the service network, then you also need to update the VPC association security group (8a) to allow inbound traffic from the node group security group (8b) on the appropriate ports.

For more information about this integration and other scenarios like multi-cluster deployments, please refer to the Getting Started guide.

Prerequisites

In order to deploy resources provided in the sample code, you need an AWS account in which to deploy the resources. You also need the following command line tools: AWS CLI, AWS SAM CLI, and kubectl. Details on how to deploy each component are included in the repository.

Cleaning up

In order to clean up any resources that you deployed using the sample code, delete the associated CloudFormation stack.

Conclusion

In this post, we showed you how your teams can build APIs to expose business logic and data to other teams within the organization and to customers outside the organization. You can use Amazon VPC Lattice to allow cross-account and cross-VPC communication, protected with TLS, authenticated and authorized with AWS IAM, and logged using Amazon CloudWatch logs. You can then expose these applications publicly with Amazon API Gateway, protected with TLS, Amazon CloudFront, AWS Shield Advanced, AWS WAF, and OAuth2 via Amazon Cognito.

This allows teams with different expertise to use the compute platforms and deployment toolchains that they prefer. Thus teams that prefer AWS Lambda can easily interact with other teams that prefer Amazon ECS or Amazon EKS, and vice versa.

To learn more, read other posts about building secure private applications and networks with Amazon VPC Lattice:

Heeki Park

Heeki Park

Heeki Park is a Principal Solutions Architect, Serverless Specialist, at AWS, supporting global and strategic accounts and helping customers build modern applications and event-driven architectures. He is currently thinking about governance in depth for serverless applications, modeling cost for distributed systems, and building applications across different compute platforms.