Instagram Twitter Pocket GitHub Linked In

I was recently asked to set up an S3 bucket with basic authentication enabled over https. This is not a configuration offered by AWS but there is a nice blog post that shows you how to do this using Lambda@Edge.

We’re using serverless to deploy our AWS resources and while the configuration is pretty well documented there were some unexpected behaviours from running Lambda@Edge that I wasn’t aware of, namely:

  • The CloudWatch logs are recorded in the region that the CloudFront deployment is accessed on; additionally these logs have a different path from the deployed function (presumably so as not to conflict if the lambda function is deployed directly into that region).
  • Environment variables are not available to Lambda@Edge functions

Both of these are actually reasonable and sensible restrictions and clearly documented but it wasn’t something I was aware of going into this and the logging in particular made things harder to track down.

In addition I had the lambda permissions restricted to the region I deployed the function into, not taking into account that Lambda@Edge meant it would be accessed from a different region. In the end I chose to make the function explicitly set this region no matter where it was being called from.

Debugging Lambda@Edge

Testing and Debugging Lambda@Edge Functions - Amazon CloudFront

There’s code provided by AWS that allows you determine the region your Lambda@Edge function is executing in as well as the path of the log:

FUNCTION_NAME=function_name_without_qualifiers
for region in $(aws --output text  ec2 describe-regions | cut -f 3)
do
    for loggroup in $(aws --output text  logs describe-log-groups --log-group-name "/aws/lambda/us-east-1.$FUNCTION_NAME" --region $region --query 'logGroups[].logGroupName')
    do
        echo $region $loggroup
    done
done

The output for this looks like:

us-east-1 /aws/lambda/us-east-1.ecogy-repository-debian-int-basicAuth
us-west-2 /aws/lambda/us-east-1.ecogy-repository-debian-int-basicAuth

letting you know the region and the path of the log in CloudWatch.

Lambda@Edge restrictions

Requirements and Restrictions on Lambda Functions - Amazon CloudFront

I wanted the basic auth credentials to be configurable and the first option I tried using was environment variables, something it quickly became apparent didn’t work but again clearly documented.

My solution to this was to retrieve the details direct from the Parameter Store, something which worked great as soon as I got the region permissions worked out.

The code

The serverless configuration is straight forwards, making use of the serverless-plugin-cloudfront-lambda-edge plugin to handle the versioning that is required by Lambda@Edge of the lambda functions.

/# Ecogy protected repository for debian packages hosted on S3/

service: ecogy-repository-debian

plugins:
  - "@silvermine/serverless-plugin-cloudfront-lambda-edge"
  - serverless-pseudo-parameters

provider:
  name: aws
  region: us-east-1
  runtime: nodejs8.10

  iamRoleStatements:
    - Effect: Allow
      Action:
        - ssm:GetParameter
      Resource: arn:aws:ssm:#{AWS::Region}:#{AWS::AccountId}:parameter/REPOSITORY/DEBIAN/*

functions:
  basicAuth:
    handler: src/handler.basicAuth
    memorySize: 128 /# limit for lambda @ edge/
    timeout: 5 /# limit for lambda @ edge/
    lambdaAtEdge:
      distribution: DebianRepositoryDistribution
      eventType: viewer-request

resources:
  Resources:
    S3BucketDebianRepository:
      Type: AWS::S3::Bucket
      Properties:
        AccessControl: Private
        BucketName: ecogy-debian-repo-${opt:stage}
        PublicAccessBlockConfiguration:
          BlockPublicAcls: true
          BlockPublicPolicy: true
          IgnorePublicAcls: true
          RestrictPublicBuckets: true

    DebianRepositoryBucketPolicy:
      Type: "AWS::S3::BucketPolicy"
      Properties:
        Bucket:
          Ref: S3BucketDebianRepository
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Action:
                - "s3:GetObject"
              Effect: "Allow"
              Resource:
                Fn::Join:
                  - ""
                  - - "arn:aws:s3:::"
                    - Ref: S3BucketDebianRepository
                    - "/*"
              Principal:
                AWS:
                  {
                    "Fn::Join":
                      [
                        "",
                        [
                          "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ",
                          {
                            Ref: DebianRepositoryCloudFrontOriginAccessIdentity,
                          },
                        ],
                      ],
                  }

    DebianRepositoryDNSRecord:
      Type: AWS::Route53::RecordSet
      Properties:
        HostedZoneId: ${ssm:/CodeBuild/HOSTED_ZONE-${opt:stage}}
        Name: debian.repo.${ssm:/CodeBuild/BASE_DOMAIN-${opt:stage}}
        Type: A
        AliasTarget:
          HostedZoneId: Z2FDTNDATAQYW2 /# Cloudfront/
          DNSName:
            "Fn::GetAtt":
              - DebianRepositoryDistribution
              - DomainName

    DebianRepositoryCloudFrontOriginAccessIdentity:
      Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
      Properties:
        CloudFrontOriginAccessIdentityConfig:
          Comment: DebianRepositoryCloudFrontOriginAccessIdentity

    DebianRepositoryCertificate:
      Type: AWS::CertificateManager::Certificate
      Properties:
        DomainName: debian.repo.${ssm:/CodeBuild/BASE_DOMAIN-${opt:stage}}

    DebianRepositoryDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Aliases:
            - debian.repo.${ssm:/CodeBuild/BASE_DOMAIN-${opt:stage}}
          Origins:
            - Id: S3-ecogy-debian-repo-${opt:stage}
              DomainName: ecogy-debian-repo-${opt:stage}.s3.amazonaws.com
              S3OriginConfig:
                OriginAccessIdentity:
                  {
                    "Fn::Join":
                      [
                        "",
                        [
                          "origin-access-identity/cloudfront/",
                          {
                            Ref: DebianRepositoryCloudFrontOriginAccessIdentity,
                          },
                        ],
                      ],
                  }
          DefaultCacheBehavior:
            TargetOriginId: S3-ecogy-debian-repo-${opt:stage}
            ViewerProtocolPolicy: redirect-to-https
            MinTTL: 0
            AllowedMethods:
              - HEAD
              - GET
              - OPTIONS
            CachedMethods:
              - HEAD
              - GET
            SmoothStreaming: false
            DefaultTTL: 3600
            MaxTTL: 31536000
            Compress: false
            ForwardedValues:
              QueryString: false
          PriceClass: PriceClass_100
          Enabled: true
          ViewerCertificate:
            AcmCertificateArn:
              Ref: DebianRepositoryCertificate
            SslSupportMethod: sni-only
            MinimumProtocolVersion: TLSv1.1_2016


The function itself is mostly lifted from the Hacker Noon blog post, the primary changes being the use of the Parameter Store and explicitly setting the region for when the function is called at the edge from another region:

"use strict"

const AWS = require("aws-sdk") // eslint-disable-line import/no-extraneous-dependencies
AWS.config.update({ region: "us-east-1" })
const ssm = new AWS.SSM()

exports.basicAuth = async (event, context, callback) => {
  try {
    // Configure authentication
    const authUser = await ssm
      .getParameter({ Name: "/REPOSITORY/DEBIAN/USER", WithDecryption: true })
      .promise()
      .then(data => {
        return data.Parameter.Value
      })
    const authPass = await ssm
      .getParameter({ Name: "/REPOSITORY/DEBIAN/SECRET", WithDecryption: true })
      .promise()
      .then(data => {
        return data.Parameter.Value
      })

    // Get request and request headers
    const request = event.Records[0].cf.request
    const headers = request.headers

    // Construct the Basic Auth string
    const authString =
      "Basic " + new Buffer(authUser + ":" + authPass).toString("base64")

    // Require Basic authentication
    if (
      typeof headers.authorization == "undefined" ||
      headers.authorization[0].value != authString
    ) {
      const body = "Unauthorized"
      const response = {
        status: "401",
        statusDescription: "Unauthorized",
        body: body,
        headers: {
          "www-authenticate": [{ key: "WWW-Authenticate", value: "Basic" }]
        }
      }
      callback(null, response)
    }

    // Continue request processing if authentication passed
    callback(null, request)
  } catch (err) {
    console.log("Error while verifying basic auth header")
    console.log(err)
    throw err
  }
}

Setting the region explicitly like this admittedly makes Lambda@Edge a little pointless as it’s always executing in the one region; a better solution would be to configure cross region permissions. The reason for using Lambda@Edge wasn’t for the improved performance in this case though, rather it was to implement the basic authentication which this configuration did.

In the end it turned out we didn’t actually need to support basic authentication at all on the S3 bucket, there was a plugin that allowed the bucket to be accessed using S3 credentials directly and the CloudFront distribution and Lambda@Edge were no longer required.