This module provides an AWS Lambda function that works together with CloudFront@Edge to authenticate S3 website resources (paths).
This module is developed for terraform-aws-s3-cloudfront-website, which helps set up a full static website using using S3, CloudFront and ACM.
Note
|
This module utilizes AWS Lambda — a paid resource. Keep this in mind when adopting this solution. |
Note
|
If you’re using this module with terraform-aws-s3-cloudfront-website, please refer to its website for further instructions. Certain AWS quirks with regions are specifically explained there. |
This module works through applying an AWS Lambda HTTP authentication function to the CloudFront@Edge distribution of the static website.
Specifically, this Lambda function is executed on every access to the site to check whether:
-
the path being access should be protected
-
if so, authenticate the client:
-
if the client was previously authentication (and therefore carries a cookie), allow
-
with an HTTP authentication, if it matches the configuration, allow
-
-
if the client is allowed, place (or update) the cookie to allow for further access.
Two things are required:
-
A permission configuration file, used to configure the Lambda function for authentication.
-
Configuration in Terraform that deploys this module.
To allow the Lambda function to be configurable dynamically (i.e. the configuration is not bound to Terraform), the configuration file (in JSON) is located in an S3 bucket that Terraform may or may not have access to.
Note
|
You could also manually create/update the configuration file. |
You will need to create (or re-use) a S3 bucket to store the configuration file, and this configuration file must be readable for the Lambda function.
The configuration file does two things:
-
Specifies a set of paths that are “protected” (i.e. authentication is required)
-
Specifies a set of usernames and passwords via
htpasswd
(the typical basic HTTP authentication method)
htpasswd
format
allows specification of usernames and password in a single file/string:
-
each line (
\n
) contains one username and its corresponding password -
within each line the username and password are separated by a colon (
:
)
For example:
foobar:$2y$05$1h9cwwFusLcZCIUpdM7Gke.ei1E2QV6ORH/ZmvbR4h2tDGHb7q8lW
zeebaa:$2y$05$aWBOi47GEOOoNB/ZUgdPY.NukDalyc.Bvn.S0aOlKDD9wp0R9mQHm
Assume you want to create a user called foobar
with a password FooBar#PassW0RD
.
Run htaccess
to generate access credentials to upload:
$ htpasswd -nbB foobar FooBar#PassW0RD
foobar:$2y$05$1h9cwwFusLcZCIUpdM7Gke.ei1E2QV6ORH/ZmvbR4h2tDGHb7q8lW
Note
|
This command uses bcrypt to store the password hash. While it is
the best choice out of available htpasswd algorithms (MD5, SHA1, crypt),
remember that by default there is no rate limiting on the Lambda function — meaning that someone can brute force the passwords via the public interface.
(You could use the reserved_concurrent_executions option to limit
Lambda concurrency.)
|
The module uses micromatch to implement wildcard and glob matching URIs, and all Micromatch Features are supported.
These rules specify blacklisted paths.
/* protect particular file */
"/sample.png",
/* protects all files that end with `.png` inside a subdirectory */
"/sample/*.png"
These rules whitelists otherwise publicly accessible files.
/* do not protect this particular file => all others are protected */
"!/sample.png"
In the configuration file:
-
the
htpassword
portion is serialized into a single string -
the protected paths patterns are specified individually.
JSON example:
{
/* store usernames and password in "htpasswd" format */
"htpasswd": "foobar:$2y$05$1h9cwwFusLcZCIUpdM7Gke.ei1E2QV6ORH/ZmvbR4h2tDGHb7q8lW\nzeebaa:$2y$05$aWBOi47GEOOoNB/ZUgdPY.NukDalyc.Bvn.S0aOlKDD9wp0R9mQHm",
/* path patterns to protect in micromatch syntax */
"uriPatterns": [
/* all files that end with `.png` or `.sh` in the first level */
"/*.{png,sh}",
/* all files regardless of depth */
"**"
]
}
Create an S3 bucket and upload the configuration JSON file.
provider "aws" {
region = "us-east-1"
#description = "AWS Region for Cloudfront (ACM certs only supports us-east-1)"
alias = "cloudfront"
}
resource "aws_s3_bucket" "permissions" {
bucket = "my-site-permissions"
acl = "private"
provider = aws.cloudfront
}
resource "aws_s3_bucket_object" "permissions" {
bucket = aws_s3_bucket.permissions.bucket
key = "config.json"
# Assume that your configuration JSON file is stored locally at `config.json`
source = "./config.json"
etag = filemd5("./config.json")
provider = aws.cloudfront
}
Create the authentication Lambda function. Remember that it must use the same provider (same region) as the S3 bucket did.
module "staging-lambda" {
source = "github.com/riboseinc/terraform-aws-lambda-edge-authentication"
/* S3 bucket that stores configuration JSON file. */
bucketName = aws_s3_bucket.permissions.bucket
/* S3 object name of the configuration JSON file in the above bucket. */
bucketKey = aws_s3_bucket_object.permissions.key
/* the domain scope of cookie to be set */
cookieDomain = "my-s3-website-domain-name.com"
providers = {
aws = aws.cloudfront
}
}
Then you have to associate the Lambda function with your CloudFront distribution using CloudFront@Edge.
resource "aws_cloudfront_distribution" "main-lambda-edge" {
provider = aws.cloudfront
enabled = true
http_version = "http2"
aliases = "..."
origin {
# ...
# Use a secret to authenticate CloudFront requests to origin
custom_header {
name = "User-Agent"
value = var.refer_secret
}
}
default_cache_behavior {
# ...
# Link the Lambda function to CloudFront request
# for authenticating
lambda_function_association {
event_type = "viewer-request"
lambda_arn = var.lambda_edge_arn_version
}
# Link the Lambda function to CloudFront response
# for setting the authenticated cookie
lambda_function_association {
event_type = "viewer-response"
lambda_arn = var.lambda_edge_arn_version
}
}
}
Now run terraform apply
and see everything being setup.
Warning
|
Not recommended for security as passwords are stored as plaintext! |
One simple way is to maintain the following files. It’s much easier to add/remove passwords compared to the static JSON file.
-
htpasswd.txt
: for storing plaintext credentials
CanadianMonkey Xz5Z&kWvd3XJ
CaptainMagic Ta3tNk&aaC9v
NewportGroove oaWNcHCqrK$E
-
Use this command to generate the
htaccess
file:
cat htpasswd.txt | xargs -n2 bash -c 'htpasswd -bB htaccess $0 $1'
-
In the permissions configuration JSON, remember to replace your allowance patterns:
resource "aws_s3_bucket_object" "restricted_example_com_json" {
provider = aws.main
key = "restricted.example.com.json"
bucket = aws_s3_bucket.main.id
server_side_encryption = "aws:kms"
content = <<EOF
{
"htpasswd": "${file("htaccess")}",
"uriPatterns": [
"**"
]
}
EOF
}
To confirm this works:
-
Visit a protected path in the browser and confirm that HTTP authentication is required. (You’ll be prompted to log in.)
-
Visit a protected path again in a browser, but this time with caches disabled. Check whether a cookie has been set in your request — it should have been set in the previous successful authentication. It’s working properly if you see it.
How awesome is this!
Note
|
If you’re using this module with terraform-aws-s3-cloudfront-website, please refer to its website for further instructions. Certain AWS quirks with regions are specifically explained there. |