This blog post is a follow-up to our previous post, where we implemented tiered access to S3 data using presigned URLs.
In most production applications, CloudFront is used to serve static content to users.
In this post, we will explore how to implement restricted access when serving content through CloudFront.
You can find the complete example here.
What is CloudFront?
In simple terms, CloudFront is a content delivery network (CDN) managed by AWS. A CDN is a network of servers deployed close to end users, serving as a caching layer to improve content delivery speed and reliability.
When a user requests content that is not present on the edge server, the edge server pulls the content from your backend and caches it. If the content is already present on the edge server, it is returned directly to the user without contacting your backend.
Benefits and Limitations
Benefits:
Offloading Backend: Reduces the load on your backend servers by caching content at the edge.
Lower Latency: Delivers content faster to users by serving it from a location closer to them.
Support for Different Origins: Can fetch content from multiple backend sources, including S3 buckets, EC2 instances, and custom origins.
Reliable Traffic Between Edge Locations and Origin: Ensures consistent and reliable data transfer between edge locations and your origin servers.
Support for Static Content and Video Streaming: Efficiently handles both static files (like images and CSS) and video streaming, improving the user experience.
Limitations:
Stale Content: Cached content might become outdated, requiring careful management of cache invalidation and refresh strategies.
Cost: Additional costs for data transfer and request handling, which can add up depending on usage.
Caching Strategy Needs Tweaking: Requires fine-tuning of caching policies to balance performance and content freshness effectively.
Restricting Access
CloudFront generally supports two ways to restrict access to content: signed URLs and signed cookies. Signed URLs are used to grant access to a single file, while signed cookies are used for accessing multiple files.
For simpler authorization mechanisms, you can also use Lambda@Edge functions. However, these functions have limited package sizes and can introduce additional overhead.
Since we already implemented signed URLs with S3 in our previous post, this blog post will focus on using signed cookies for restricting access to CloudFront content.
Architecture
Fig 1. Architecture
To demonstrate tiered access, CloudFront is configured to serve both public and private content from an S3 origin using path-based routing. Private content is secured with signed cookies, utilizing trusted key groups and public key cryptography.
When deploying CloudFront, we provide a public key that CloudFront uses to validate cookie data. The private key is stored in AWS Secrets Manager and accessed by a Lambda function that creates signed cookies.
CloudFront and the Api Gateway need to share a domain to avoid client-side security issues when issuing cookies. For this purpose, we use Route 53 for DNS resolution and AWS Certificate Manager for managing TLS certificates for our custom domains.
Here’s the flow for accessing private content:
User Authentication: The user authenticates with Cognito and retrieves an authentication token.
Request Signed Cookie: The user uses the authentication token to request a signed cookie from a secure API, implemented with API Gateway and a Lambda function.
Lambda Function Operation:
The Lambda function retrieves the private key from Secrets Manager.
It generates the cookie data, including the policy, key ID, and signature.
Access Private Data: With the signed cookie, the user requests private data from CloudFront.
CloudFront Validation: CloudFront validates the cookie’s signature. If the signature is correct, CloudFront returns the requested data to the user.
Key Management
CloudFront uses public key cryptography and trusted key groups to sign and validate cookies. Unfortunately, CloudFront trusted key groups do not integrate with AWS KMS at this time, so key management must be handled manually.
CloudFront trusted key groups will hold the public key and are deployed with infrastructure configuration. Be sure to follow security best practices to secure your CI workflow.
Public and private keys are RSA 2048 keys in PKCS#1 format. Keys are generated as part of Terraform. If you are generating them yourself, use the following commands:
The infrastructure is provisioned using Terraform.
Certificates and Encryption Keys
CloudFront content will be available at https://DOMAIN, and the API will be accessible at https://api.DOMAIN.
In the following steps, we will provision a certificate from AWS Certificate Manager. We will also create an RSA key pair and upload the private key to AWS Secrets Manager.
The CloudFront distribution is configured with one S3 origin and two cache behaviors: one for public content and one for private content. The private content is secured using trusted key groups created from the public key generated in the previous step.
The S3 origin bucket policy is modified to allow access only from the CloudFront distribution. This prevents users from directly accessing the origin and bypassing the security policies implemented in CloudFront.
A regional API Gateway is configured with a Cognito authorizer and has one secure GET endpoint, signed-cookies.
Since there is no official Python example for issuing signed cookies, the Lambda function is implemented in Python. All relevant information is passed via environment variables, and permissions are updated to include pulling the private key from AWS Secrets Manager.
The Lambda function has an rsa dependency that needs to be packaged with the code. Below are the helper functions for retrieving the private key and creating cookie values.
Helper Functions
These helper functions are used to retrieve the private key from AWS Secrets Manager and to create signed cookie values.
importosfromdatetimeimportdatetime,timedeltafrom.helpersimportCloudfrontUtils,get_sm_secret# global variablesCDN_DOMAIN_NAME=os.environ["CDN_DOMAIN_NAME"]CDN_PRIVATE_PATH=os.environ["CDN_PRIVATE_PATH"]REGION_NAME=os.environ["REGION_NAME"]SECRET_NAME=os.environ["SM_PRIVATE_KEY_ID"]PRIVATE_KEY_ID=os.environ["PRIVATE_KEY_ID"]CF_UTILS=CloudfrontUtils(private_key_id=PRIVATE_KEY_ID,priv_key_value=get_sm_secret(secret_name=SECRET_NAME,region_name=REGION_NAME),)deflambda_handler(event:dict,context:object)->dict:expire_at=datetime.now()+timedelta(minutes=5)signed_cookies=CF_UTILS.generate_signed_cookies(url=CDN_PRIVATE_PATH,expire_at=expire_at)return{"statusCode":200,"multiValueHeaders":{"Set-Cookie":[f"{key}={value};Path=/;Secure;HttpOnly;Domain={CDN_DOMAIN_NAME}"forkey,valueinsigned_cookies.items()],},}
Complete Example
You can find the complete example with instructions here.
Conclusion
In this blog post, we have explored how to implement restricted access to CloudFront content using signed cookies. We covered a sample architecture with code examples and discussed some of the intricacies of the implementation .