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:
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.
- Chalice config file documentation: Configuration File
↩