Мы можем использовать функции Lambda@Edge для авторизации запросов, поступающих в наш дистрибутив CloudFront. Таким образом, запрос не будет отправлен к источнику, если он не содержит необходимых заголовков.


1. Сценарий

Предположим, что наше приложение хочет получать уведомления почти в реальном времени от стороннего поставщика. Мы создаем вебхук куда партнер может отправлять события.

URL-адреса лямбда-функций — отличный выбор для этой цели. Их легко настроить, и Шлюз API — хотя это правильное решение — здесь, вероятно, было бы излишним.

Одним из недостатков URL-адресов функций Lambda является то, что они не позволяют использовать пользовательские домены на момент написания этой статьи. Обходной путь к этой проблеме состоит в том, чтобы создать CloudFront дистрибутив, где источником является URL-адрес функции. Затем мы можем добавить дистрибутив в качестве цели для Маршрут 53 записывать.

Но мы каким-то образом должны авторизовать запросы веб-хуков и пропускать только те из них, которые содержат требуемый токен в соответствующем заголовке.

Давайте посмотрим на решение этого сценария.


2. Авторизация с помощью Lambda@Edge

Мы можем использовать Лямбда@край функция для выполнения авторизации.


2.1. Чего нет в этом посте

В этой статье основное внимание уделяется разрешениям и некоторым другим незначительным деталям. Это не объяснит, как

  • создать раздачу CloudFront
  • прикрепить функции Lambda@Edge к дистрибутиву
  • создавать лямбда-функции с URL-адресом
  • добавьте URL-адрес функции в раздачу в качестве источника.

У меня есть несколько ссылок внизу страницы, которые описывают эти операции.


2.2. Некоторый код

Начнем с функции Lambda@Edge. Так как это обычный лямбда функции, которую AWS распространяет и развертывает в периферийных местоположениях, у нас может быть — более или менее — стандартный код Node.js:

const { SSM } = require('aws-sdk');

const ssm = new SSM({region: 'us-east-1'});

exports.handler = async (event) => {
  const request = event.Records[0].cf.request;

  const apiKeyHeader = request.headers['x-api-key'];
  if (!apiKeyHeader || apiKeyHeader.length === 0) {
      throw new Error('Missing token');
  }

  let apiKey;
  try {
    const ssmResponse = await ssm.getParameter({
      Name: '/my/encrypted/secret',
      WithDecryption: true,
    }).promise();
    apiKey = ssmResponse.Parameter.Value;
  } catch (error) {
    throw error;
  }

  const headerValue = apiKeyHeader[0].value;
  if (headerValue !== apiKey) {
    throw new Error('Invalid token');
  }

  return request;
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Мы храним секретное значение заголовка в Хранилище параметров. Функция Lambda@Edge получит его оттуда, а затем сравнит с заголовком запроса. Если они совпадают, функция вернет объект запроса CloudFront и сможет перейти к источнику. В противном случае мы выдадим ошибку, если требуемый заголовок отсутствует или секреты не совпадают.

На этом пост можно было бы закончить. Но, как и в случае с CloudFront в целом, некоторые мелкие детали могут сделать жизнь разработчика облачных вычислений более насыщенной.


3. Разрешения

Lambda@Edge будет использовать роль выполнения Lambda, которую мы создали для функции.


3.1. Принять роль

Мы должны сделать эту роль допустимой для Lambda@Edge. роли политика доверия должно выглядеть так:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "lambda.amazonaws.com",
          "edgelambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Мы должны добавить edgelambda.amazonaws.com к Principal элемент, поэтому Lambda@Edge может взять на себя эту роль.


3.2. Разрешения службы

В этом примере функция Lambda@Edge вызывает хранилище параметров, поэтому мы должны добавить ssm:GetParameter и — если секрет в SecureString формат — kms:Decrypt разрешения на роль.

Это не специфичное для Lambda@Edge разрешение. Обычная функция Lambda, выполняющая ту же логику, также должна иметь эти разрешения.


3.3. Вход в другой регион

Мы можем думать, что мы готовы идти. Мы могли бы, но есть вероятность, что мы получим следующую ошибку, если вызовем URL-адрес раздачи CloudFront (или личного домена):

503 ERROR
The Lambda function associated with the CloudFront distribution
is invalid or doesn't have the required permissions. We can't
connect to the server for this app or website at this time. There
might be too much traffic or a configuration error. Try again later,
or contact the app or website owner.
Войти в полноэкранный режим

Выйти из полноэкранного режима

Сообщение об ошибке относится к некоторым отсутствующим разрешениям.

CloudFront развертывает функцию Lambda как минимум в некоторых периферийных местоположениях, отличных от региона, в котором мы ее создали. Когда мы вызываем URL-адрес CloudFront, будет запущен Lambda@Edge, ближайший к нашему географическому местоположению.

В этом примере я создал функцию в us-east-1но ближайшим ко мне расположением края является eu-central-1. Это означает, что лямбда-функция в eu-central-1 запустится и выполнит логику авторизации!

Если он работает в eu-central-1, он также будет входить в этот регион. Проблема в том, что в настоящее время у него нет разрешения на это.

Первоначально CloudWatch права доступа к логам выглядят так:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "logs:CreateLogGroup",
      "Resource": "arn:aws:logs:us-east-1:123456789012:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/FUNCTION_NAME:*"
      ]
    }
  ]
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Lambda автоматически прикрепляет эту политику к роли исполнения, когда мы создаем функцию в консоли (или с помощью некоторых фреймворков). Resource все элементы указывают на группу журналов в us-east-1.

Давайте исправим ошибку. Мы можем заменить us-east-1 с * персонаж в заявлении:

{
  "Effect": "Allow",
  "Action": "logs:CreateLogGroup",
  "Resource": "arn:aws:logs:*:123456789012:*"
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Каждая функция Lambda@Edge теперь может создавать свои группы журналов в соответствующем регионе.

Мы также должны изменить Resource для logs:PutLogEvents действие. Это связано с тем, что имя группы журналов будет /aws/lambda/us-east-1.FUNCTION_NAME в краевой области расположения.

{
  "Effect": "Allow",
  "Action": [
    "logs:CreateLogStream",
    "logs:PutLogEvents"
  ],
  "Resource": [
    "arn:aws:logs:*:123456789012:log-group:/aws/lambda/FUNCTION_NAME:*",
    "arn:aws:logs:*:123456789012:log-group:/aws/lambda/us-east-1.FUNCTION_NAME:*"
  ]
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

CloudFront не ответит сообщением об ошибке, если мы не добавим пограничную версию группы журналов в Resource элемент. Он просто ничего не будет регистрировать.


4. Соображения

Теперь, когда мы предоставили необходимые разрешения, давайте рассмотрим несколько потенциальных и реальных недостатков.


4.1. Ограниченный размер пакета

Lambda@Edge поддерживает максимальный размер заархивированного пакета 1 МБ. Так что мы действительно не можем npm install ничего, потому что мы, вероятно, превысим этот предел.

К счастью, Lambda поддерживает AWS SDK «из коробки». Все, что нам нужно сделать, это require это в верхней части index.js обработчик, как если бы это была установленная зависимость.

Лямбда поставляется с SDK v2 из коробки на момент написания этого. Он еще не поддерживает модульную версию 3, но я не считаю это недостатком для данного примера, потому что здесь нам не нужно беспокоиться о размере пакета.

К сожалению, мы не можем далеко тянуться с другими зависимостями, поэтому мы вынуждены использовать собственные языковые решения.


4.2. Нет слоев

К сожалению, мы не можем использовать новый Расширение хранилища параметров потому что Lambda@Edge не поддерживает слои.

В этом примере это не проблема, потому что мы можем использовать SDK, который Lambda поддерживает из коробки.


4.3. Политика запроса происхождения

Если мы вызовем URL-адрес CloudFront, авторизатор Lambda@Edge запустится, но логика выдаст исключение, поскольку он не получит заголовок с секретным ключом API в запросе.

Мы должны изменить Политика запроса происхождения в Поведение раздел, чтобы CloudFront перенаправлял все заголовки в Lambda@Edge.

Выбор правильной политики запроса происхождения

Если мы выберем AllViewer здесь полезная нагрузка запроса будет содержать секретный заголовок.


5. Вывод

Мы можем использовать функции Lambda@Edge для перехвата запросов в CloudFront и выполнения авторизации.

Lambda@Edge использует ту же роль выполнения, что и соответствующая обычная функция Lambda. Мы должны убедиться, что мы добавили все регионы в разрешения журнала и перенаправили все заголовки в функцию авторизатора.


6. Дальнейшее чтение

URL-адреса лямбда-функций — Начните здесь для URL-адресов функций

Создание дистрибутива — Как создать раздачу CloudFront

Использование AWS Lambda с CloudFront Lambda@Edge — Добавить лямбда-функцию в дистрибутив

Использование URL-адреса лямбда-функции — URL-адрес лямбда-функции в качестве источника CloudFront.