How do I configure an HTTP API to allow access only to specific IP addresses?

5 minute read
1

I want to integrate an HTTP API with an AWS Lambda function to authorize and allow only specific IP addresses.

Short description

To restrict HTTP API access to specific, predefined IP addresses, deploy a Lambda function as an authorizer. The Lambda function performs IP address validation to determine if incoming requests originate from approved sources. The authorizer then grants or denies access to the HTTP API based on the IP address.

Resolution

Create a Lambda Function

First, create a Lambda function to use as the authorizer. You can configure the Lambda function to use environment variables for an allowlist:

Key: IP_RANGE, Value: ['0.0.0.0','XX.XX.XX.XX']

The Value can be either a specific IP address that you want to allow or a complete CIDR range.

After you deploy the code, configure environment variables for the Lambda function to define a specific variable as IP_RANGE.

For security purposes, it's a best practice to encrypt the environment variables with AWS Key Management Service (AWS KMS).

Create an HTTP API

Create an HTTP API to use as the entry point for incoming requests. Because HTTP APIs are public by default, use the authorizer Lambda function to control access.

Attach the Lambda authorizer

Associate the Lambda function as an authorizer to the API. When a request reaches the API, the authorizer evaluates the source IP address against the allowlist. If the authorizer allows the IP address, then it grants access to the HTTP API endpoint. Otherwise, it denies the request.

To evaluate IP addresses based on your allowlisted environment variables, use the following example Lambda function:

Note: This example passes an authorizationtoken with a value of secretcode. However, you can configure these parameters to use another value.

# -*- coding: utf-8 -*-
# ========================
# AWS Lambda Python Runtime
# ========================

# Import necessary modules
import os
from ipaddress import ip_network, ip_address
import uuid
import ast

# Function to check if an IP address is within the specified IP range
def check_ip(IP_ADDRESS, IP_RANGE):
    VALID_IP = False
    # Check if any of the elements in IP_RANGE contain a CIDR notation (e.g., "192.168.0.0/24")
    cidr_blocks = list(filter(lambda element: "/" in element, IP_RANGE))
    if cidr_blocks:
        for cidr in cidr_blocks:
            # Create an IP network from the CIDR notation
            net = ip_network(cidr)
            # Check if the IP_ADDRESS is within the network
            VALID_IP = ip_address(IP_ADDRESS) in net
            if VALID_IP:
                break
    # If the IP_ADDRESS is not within any CIDR block, check if it matches an exact IP in IP_RANGE
    if not VALID_IP and IP_ADDRESS in IP_RANGE:
        VALID_IP = True

    return VALID_IP

# Lambda function handler
def lambda_handler(event, context):
    # Extract the source IP address from the request's RequestContext
    IP_ADDRESS = event["requestContext"]["http"]["sourceIp"]
    # Parse the IP_RANGE environment variable, which is a list of allowed IP addresses or CIDR blocks
    IP_RANGE = ast.literal_eval(os.environ.get("IP_RANGE", "[]"))
    # Check if the source IP is within the allowed IP range
    VALID_IP = check_ip(IP_ADDRESS, IP_RANGE)
    
    # Extract relevant information from the request event
    
    # Extract the AWS API Gateway's API ID from the event 
    API_ID = event["requestContext"]["apiId"]
    # Extract the AWS account ID from the event 
    ACC_ID = event["requestContext"]["accountId"]
 
   # Extract the AWS API Gateway's Details Method, Stage and Route from the event  
    METHOD = event["requestContext"]["http"]["method"]
    STAGE = event["requestContext"]["stage"]
    ROUTE = event["requestContext"]["http"]["path"]

    # Check if the request includes a valid authorization token and the source IP is allowed
    # Use the {region} based on the region of the API Gateway's configuration.
   
    if event["headers"]["authorizationtoken"] == "secretcode" and VALID_IP:

        # If authorized, allow the execution of the API
        response = {
            "principalId": f"{uuid.uuid4().hex}",
            "policyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Action": "execute-api:Invoke",
                        "Effect": "Allow",
                        "Resource": f"arn:aws:execute-api:{region}:{ACC_ID}:{API_ID}/{STAGE}/{METHOD}{ROUTE}",
                    }
                ],
            },
            "context": {"exampleKey": "exampleValue"},
        }

        return response

    # If the request is not authorized or the IP is not allowed, deny execution
    # Use the Acc_ID, region & API_ID as per the HTTP API ID Details
    
    response = {
        "principalId": f"{uuid.uuid4().hex}",
        "policyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "execute-api:Invoke",
                    "Effect": "Deny",
                    "Resource": f"arn:aws:execute-api:{region}:{ACC_ID}:{API_ID}/*/*/*",
                }
            ],
        },
        "context": {"exampleKey": "exampleValue"},
    }

    return response

Note: If you require additional Python modules, then use pip to install the modules in the Lambda function's environment. Package the Lambda function with the necessary dependencies.

Test your authorizer

Before you deploy your function for production, be sure to test the function in a testing environment. To check the IP addresses that you want to access the HTTP API endpoint, use access logging. Use context variables to identify an IP address, and include the address in your allowlist through the IP_RANGE environment variable. The following example includes context variables for access logging in HTTP API:

{"ownerAccountId":"$context.accountId","apiId":"$context.apiId","requestId":"$context.requestId", /  
"ip":"$context.identity.sourceIp","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod", /  
"routeKey":"$context.routeKey","status":"$context.status","protocol":"$context.protocol", /  
"responseLength":"$context.responseLength","errorMessage":"$context.error.message", /  
"errorMessageString":"$context.error.messageString","extendedRequestId":"$context.extendedRequestId", /  
"responseLatency":"$context.responseLatency","stage":"$context.stage","path":"$context.path", /  
"requestTimeEpoch":"$context.requestTimeEpoch","protocol":"$context.protocol","userAgent":"$context.identity.userAgent", /  
"errorResponseType":"$context.error.responseType","integrationStatus":"$context.integration.status", /  
"integrationErrorMessage":"$context.integrationErrorMessage","integrationRequestId":"$context.integration.requestId", /  
"integrationLatency":"$context.integration.latency","integrationStatusfromIntegration":"$context.integration.integrationStatus", /  
"integrationError":"$context.integration.error","awsEndpointRequestId":"$context.awsEndpointRequestId", /  
"customAuthorizerUser":"$context.identity.user","authorizerProperty":"$context.authorizer.property", /  
"customAuthorizerprincipalId":"$context.authorizer.principalId","authorizerError":"$context.authorizer.error"}

The variable ip":"$context.identity.sourceIp provides the IP address that requests the API endpoint. This allows you to identify IP addresses and add them to your allowlist.

AWS OFFICIAL
AWS OFFICIALUpdated a year ago