Install AWS CLI v2

$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.0.30.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

Generate Signed URLs with Linux Tools

e.g. for accessing a website behind a CloudFront distribution using a canned policy.

Write the policy file

policy
{
    "Statement": [
        {
            "Resource": "https://xxxxxxxxxxxx.cloudfront.net/",
            "Condition": {
                "DateLessThan": {
                    "AWS:EpochTime": 1648293147
                }
            }
        }
    ]
}

Then apply the following commands[1] - you need to have OpenSSL installed.

cat policy |
tr -d "\n" | (1)
tr -d " \t\n\r" | (2)
openssl sha1 -sign private_key.pem | (3)
openssl base64 -A | (4)
tr -- '+=/' '-_~' (5)
1 replace newlines
2 replace whitespaces
3 sign using the private key
4 base64 encode
5 replace invalid query-string parameters

Then our final signed-URL looks like this:

Resources:

CloudFront

Invalidate CloudFront Cache with a Lambda Function

'use strict';
const aws = require('aws-sdk');

exports.handler = async event => {
  try {
      const params = {
        DistributionId: 'XXXXXXXXXX', (1)
        InvalidationBatch: {
          CallerReference: `web-app-${new Date().getTime()}`,
          Paths: {
            Quantity: 1,
            Items: ["/*"], (2)
          }
        },
      }
      const cloudfront = new aws.CloudFront()
      const data = await cloudfront.createInvalidation(params)
      console.log('success create invalidation', JSON.stringify(params))
      return data.promise()
  } catch (err) {
    if (err) console.error('error invalidating cache', err, err.stack)
    return new Error(err)
  }
}
1 The CloudFront Distribution’s ID
2 The list of origin paths .. here we simply invalidate all ..
It’s important to await the response from createInvalidation or else the lambda function might terminate and no invalidation is done

Resources:

Rewrite Requests for Paths without Index File

async function handler(event) {
    const request = event.request;
    const uri = request.uri;

    // Check whether the URI is missing a file name.
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    }
    // Check whether the URI is missing a file extension.
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}

Add Security Response Headers

async function handler(event) {
    const response = event.response;
    const headers = response.headers;

    headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'};
    headers['content-security-policy'] = { value: "default-src 'self'; base-uri 'self'; script-src 'self' 'nonce-31zwrvSsZOskdTpu' 'unsafe-inline'; style-src 'self' 'nonce-1234567890'; require-trusted-types-for 'script';trusted-types default;"};
    headers['x-content-type-options'] = { value: 'nosniff'};
    headers['x-frame-options'] = {value: 'DENY'};
    headers['x-xss-protection'] = {value: '1; mode=block'};
    headers['referrer-policy'] = {value: 'same-origin'};

    return response;
}