Skip to content

riboseinc/terraform-aws-lambda-edge-authentication

Repository files navigation

Terraform module for S3 website authentication via Lambda and CloudFront@Edge

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.

How it works

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:

  1. the path being access should be protected

  2. if so, authenticate the client:

    1. if the client was previously authentication (and therefore carries a cookie), allow

    2. with an HTTP authentication, if it matches the configuration, allow

  3. if the client is allowed, place (or update) the cookie to allow for further access.

Setting up the module

Two things are required:

  1. A permission configuration file, used to configure the Lambda function for authentication.

  2. Configuration in Terraform that deploys this module.

Configuration

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:

  1. Specifies a set of paths that are “protected” (i.e. authentication is required)

  2. Specifies a set of usernames and passwords via htpasswd (the typical basic HTTP authentication method)

Authentication credentials

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.)

Protected paths patterns

The module uses micromatch to implement wildcard and glob matching URIs, and all Micromatch Features are supported.

Blacklisting paths

These rules specify blacklisted paths.

/* protect particular file */
"/sample.png",
/* protects all files that end with `.png` inside a subdirectory */
"/sample/*.png"

Whitelisting paths

These rules whitelists otherwise publicly accessible files.

/* do not protect this particular file => all others are protected */
"!/sample.png"

Wildcards

Notice that a full wildcard require double asterisks.

/* all files (i.e. the whole site) */
"**"
/* all files that end in `.png` in the whole site */
"**/*.png"
/* all files inside the `/secret/` subdirectory */
"/secret/**"

Structure of the configuration file

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 */
    "**"
  ]
}

Deploying this module

S3 configuration

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
}

Lambda function

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
  }
}

Cloudfront@Edge association to Lambda

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.

Simple management example

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
}

Confirming functionality

To confirm this works:

  1. Visit a protected path in the browser and confirm that HTTP authentication is required. (You’ll be prompted to log in.)

  2. 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.

Development

  1. Run npm run build to build the lambda typescript code ⇒ main.js is generated

  2. Rerun terraform apply to upload new main.js (webpack compiled entry)