IP whitelisting your Chalice application

26 Oktober, 2019

Chalice is a very useful framework for quickly developing REST APIs with Python hosted on AWS Lambda and exposed via the AWS API Gateway, no infrastructure provisioning required. So now you’ve written your application, but don’t want to expose it to the world wide internet. This blog post demonstrates how to apply a resource policy in Chalice which limits access to a specific (range of) IP address(es).

Deploying a demo application

For demo purposes, let’s deploy a small Chalice application which returns the current time in the given timezone:

chalice new-project worldtime

In app.py:

import datetime

import pytz
from chalice import Chalice, UnprocessableEntityError
from pytz import UnknownTimeZoneError

app = Chalice(app_name="worldtime")

@app.route("/timezone/{timezone}", methods=["GET"])
def gettime(timezone):
    try:
        return f"It's currently {datetime.datetime.now(pytz.timezone(timezone))} in {timezone}."
    except UnknownTimeZoneError:
        raise UnprocessableEntityError(msg=f"Timezone '{timezone}' unknown to pytz.")

Deploy with chalice deploy to receive the URL the application is deployed on (account details are obfuscated):

$ chalice deploy

Creating deployment package.
Creating IAM role: worldtime-dev
Creating lambda function: worldtime-dev
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:eu-west-1:012345678999:function:worldtime-dev
  - Rest API URL: https://urwolo1et3.execute-api.eu-west-1.amazonaws.com/api/

Chalice created the required resources (o.a. Lambda & API Gateway) and we can now call the deployed API from anywhere on the planet, for example:

curl https://urwolo1et3.execute-api.eu-west-1.amazonaws.com/api/timezone/utc
It's currently 2019-10-26 09:50:04.948863+00:00 in utc.
curl -i https://urwolo1et3.execute-api.eu-west-1.amazonaws.com/api/timezone/donotcompute
HTTP/2 422
...

{"Code":"UnprocessableEntityError","Message":"UnprocessableEntityError: Timezone 'donotcompute' unknown to pytz."}

Limiting access to the API Gateway

If you want your application to be accessible from e.g. only within your company, you can control access to the API Gateway with resource policies. These can be configured in the API Gateway -> Resource Policy tab. First you need the ARN of the deployed endpoint:

API Gateway ARN

Next, insert the following policy in the Resource Policy tab, with your IP address in it (remove /GET/timezone/* to apply the policy to all endpoints):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:eu-west-1:012345678999:urwolo1et3/*/GET/timezone/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": [
                        "123.123.123.123"
                    ]
                }
            }
        }
    ]
}

After saving the resource policy, the API Gateway is however still accessible from everywhere. To enforce the resource policy, we must redeploy the Chalice application. However, when running chalice deploy again, the just configured resource policy disappears, and the endpoint remains open to the world! So, why is this?

Configuring the Chalice application

Upon deployment, Chalice auto-generates and applies policies. It maintains all state within a .chalice directory generated with the project and does not inspect the AWS project state. As a result, the manually configured policy is overridden with, in this case, nothing, since we haven’t configured any policies yet. So let’s configure the policy within Chalice instead of the AWS console.

In the .chalice directory, you have a config.json file. The empty config.json looks as follows1:

{
  "version": "2.0",
  "app_name": "worldtime",
  "stages": {
    "dev": {
      "api_gateway_stage": "api"
    }
  }
}

To apply the resource policy to the API Gateway, add a configuration item api_gateway_policy_file:

{
  "version": "2.0",
  "app_name": "worldtime",
  "api_gateway_policy_file": "ipwhitelist.json",
  "stages": {
    "dev": {
      "api_gateway_stage": "api"
    }
  }
}

Chalice searches for the given filename ipwhitelist.json from the .chalice directory, so create a file .chalice/ipwhitelist.json with the resource policy inside. Next, run chalice deploy once again, and you’ll now find the contents of ipwhitelist.json in the AWS console. When calling the API from an IP not defined in the policy, we now receive an error:

$ curl https://urwolo1et3.execute-api.eu-west-1.amazonaws.com/api/timezone/utc
{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:eu-west-1:********8999:urwolo1et3/api/GET/timezone/utc"}

Chalice is very configurable and allows for a much more detailed configuration than the "global" restriction applied above to the entire application, e.g. a policy per stage to restrict the development endpoint to your company IP and allow the production endpoint to the entire world. It definitely helps to go through the Chalice documentation.

Subscribe to our newsletter

Stay up to date on the latest insights and best-practices by registering for the GoDataDriven newsletter.