AWS Cloudfront (с WAF) + API-шлюз: как заставить доступ через Cloudfront?

Я хочу поставить WAF перед интерфейсом API Gateway и с (маленькой) информацией. Я считаю, что это возможно только вручную, добавив дополнительный Cloudfront-дистрибутив с WAF включен, перед APIG. Это немного стыдно, тем более что APIG теперь поддерживает пользовательские области изначально, но он должен работать.

Теперь, чтобы сделать решение безопасным, а не просто неясным, я хочу обеспечить, чтобы API-интерфейсы можно было получить только через дистрибутив Cloudfront. Каков наилучший способ сделать это?

  • Я надеялся использовать "Origin Access Identities", похожие на S3, но не вижу, как это сделать.
  • Если бы я мог назначить пользователя IAM (или роль?) в дистрибутив Cloudfront, я мог бы использовать функцию APIG IAM, но я не вижу, как это можно сделать.
  • Мне может потребоваться ключ API в APIG и передать его как пользовательский заголовок Origin из Cloudfront. Это может сработать, если мы не хотим использовать ключи API для каких-то других целей, поэтому я не совсем этому доволен.
  • Можно использовать произвольный авторизатор (!), с использованием выражения проверки токена, фактически проверяющего секрет, который передается как пользовательский заголовок Origin из Cloudfront. Должен работать, он более гибкий, но немного грязный... или нет?

Любые лучшие идеи? Или, возможно, "правильный путь" для этого существует, но я его не заметил?

Ответ 1

Я из API Gateway.

К сожалению, лучшим решением, которое мы имеем сейчас, является вставка исходного заголовка источника в CloudFront и проверка его в пользовательском авторизаторе (вариант 4 в вашем вопросе).

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

Ответ 2

"Правильный" способ заключается в использовании пользовательского авторизатора в Gateway API, как упоминалось другими.

"Дешевым" способом будет пуля 3, ключ api. Вы, вероятно, только обеспечивали бы waf → cloudfront → api gateway, если бы вы пытались отбить атаку ddos. Поэтому, если кто-то обнаружил ваш URL-адрес шлюза api и решил ddos, что вместо облачного интерфейса пользовательский авторизатор означает, что теперь вы набираете основную тяжесть атаки на лямбда. Api gateway может обрабатывать более 10 тыс. Запросов в секунду, предел по умолчанию лямбда составляет 100 в секунду. Даже если у вас есть amazon, чтобы увеличить свой лимит, вы готовы заплатить за 10 000 лямбда в секунду за устойчивую атаку?

Представители AWS скажут вам: "Ключи API предназначены для идентификации, а не для аутентификации. Ключи не используются для подписи запросов и не должны использоваться в качестве механизма безопасности" https://aws.amazon.com/blogs/aws/new-usage-plans-for-amazon-api-gateway/

Но честно, если вы не собираетесь делать что-то лучше в своей лямбде, чем утверждать какую-то гигантскую беспорядочную цепочку, почему бы не оставить это бремя и не стоить кому-то другому. (Максимальная длина ключа составляет 128 символов)

Возможно, у вас может быть запланированная функция лямбда для выпуска нового ключа api и заголовка облачного облака обновления каждые 6 часов?

Если вы хотите использовать ключи api для других вещей, то для аутентификации просто один источник происхождения api, а другой источник и api-шлюз для всего остального. Таким образом, при атаке ddos ​​вы можете обрабатывать 10k запросов в секунду для своего aph aph, в то время как все остальные клиенты, которые уже вошли в систему, имеют коллективный 10k в секунду для использования вашего api. Cloudfront и waf могут обрабатывать 100K в секунду, поэтому они не будут удерживать вас в этом сценарии.

Еще одно замечание, если вы используете лямбду за шлюзом api, вы можете использовать край лямбда @и просто пропустить api gateway все вместе. (Это не подходит для большинства сценариев, потому что край lambda @сильно ограничен, но я решил, что я его выброшу.)

Но в конечном итоге нам нужно интегрировать WAF с API GATEWAY!!:)

Ответ 3

Вы можете использовать собственное доменное имя и указать DNS для распределения с WAF. Запросы непосредственно к исходному дистрибутиву API Gateway не будут работать.

Ответ 4

Мы обнаружили, что это на самом деле возможно (как я также ответил здесь).

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

Между обычными HTTP-запросами и форматом событий CloudFront существует достаточное количество конверсий, но все это управляемо.

Сначала создайте функцию Lambda @Edge (руководство), а затем убедитесь, что ее исполняющая роль имеет доступ к API-шлюзу, к которому вы хотели бы получить доступ. Для простоты вы можете использовать управляемую политику IAM AmazonAPIGatewayInvokeFullAccess в своей роли выполнения Lambda, которая дает ему доступ для вызова любого шлюза API в вашей учетной записи.

Затем, если вы используете aws4 в качестве подписывающего клиента, ваш лямбда-код будет выглядеть так:

const aws4 = require("aws4");

const signCloudFrontOriginRequest = (request) => {
  const searchString = request.querystring === "" ? "" : '?${request.querystring}';

  // Utilize a dummy request because the structure of the CloudFront origin request
  // is different than the signing client expects
  const dummyRequest = {
    host: request.origin.custom.domainName,
    method: request.method,
    path: '${request.origin.custom.path}${request.uri}${searchString}',
  };

  if (Object.hasOwnProperty.call(request, 'body')) {
    const { data, encoding } = request.body;
    const buffer = Buffer.from(data, encoding);
    const decodedBody = buffer.toString('utf8');

    if (decodedBody !== '') {
      dummyRequest.body = decodedBody;
      dummyRequest.headers = { 'content-type': request.headers['content-type'][0].value };
    }
  }

  // Use the Lambda execution role credentials
  const credentials = {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    sessionToken: process.env.AWS_SESSION_TOKEN
  };

  aws4.sign(dummyRequest, credentials); // Signs the dummyRequest object

  // Sign a clone of the CloudFront origin request with appropriate headers from the signed dummyRequest
  const signedRequest = JSON.parse(JSON.stringify(request));
  signedRequest.headers.authorization = [ { key: "Authorization", value: dummyRequest.headers.Authorization } ];
  signedRequest.headers["x-amz-date"] = [ { key: "X-Amz-Date", value: dummyRequest.headers["X-Amz-Date"] } ];
  signedRequest.headers["x-amz-security-token"] = [ { key: "X-Amz-Security-Token", value: dummyRequest.headers["X-Amz-Security-Token"] } ];

  return signedRequest;
};

const handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const signedRequest = signCloudFrontOriginRequest(request);

  callback(null, signedRequest);
};

module.exports.handler = handler;

Ответ 5

Принудительно получить доступ через CloudFront можно с помощью функции Lambda @Edge для подписи SigV4 исходных запросов и последующего включения аутентификации IAM на вашем API-шлюзе. Эту стратегию можно использовать вместе с API-ключами в вашем дистрибутиве CloudFront (руководство).

Предполагая, что вы уже настроили API-шлюз в качестве источника для своего дистрибутива CloudFront, вам сначала нужно создать функцию (руководство) Lambda @Edge, а затем убедиться, что ее исполняющая роль имеет доступ к API-шлюзу, к которому вы хотели бы получить доступ. Для простоты вы можете использовать управляемую политику IAM AmazonAPIGatewayInvokeFullAccess в своей роли выполнения Lambda, которая дает ему доступ для вызова любого шлюза API в вашей учетной записи.

Затем, если вы используете aws4 в качестве подписывающего клиента, ваш лямбда-код будет выглядеть так:

const aws4 = require("aws4");

const signCloudFrontOriginRequest = (request) => {
  const searchString = request.querystring === "" ? "" : '?${request.querystring}';

  // Utilize a dummy request because the structure of the CloudFront origin request
  // is different than the signing client expects
  const dummyRequest = {
    host: request.origin.custom.domainName,
    method: request.method,
    path: '${request.origin.custom.path}${request.uri}${searchString}',
  };

  // Include the body in the signature if present
  if (Object.hasOwnProperty.call(request, 'body')) {
    const { data, encoding } = request.body;
    const buffer = Buffer.from(data, encoding);
    const decodedBody = buffer.toString('utf8');

    if (decodedBody !== '') {
      dummyRequest.body = decodedBody;
      dummyRequest.headers = { 'content-type': request.headers['content-type'][0].value };
    }
  }

  // Use the Lambda execution role credentials
  const credentials = {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    sessionToken: process.env.AWS_SESSION_TOKEN
  };

  aws4.sign(dummyRequest, credentials); // Signs the dummyRequest object

  // Sign a clone of the CloudFront origin request with appropriate headers from the signed dummyRequest
  const signedRequest = JSON.parse(JSON.stringify(request));
  signedRequest.headers.authorization = [ { key: "Authorization", value: dummyRequest.headers.Authorization } ];
  signedRequest.headers["x-amz-date"] = [ { key: "X-Amz-Date", value: dummyRequest.headers["X-Amz-Date"] } ];
  signedRequest.headers["x-amz-security-token"] = [ { key: "X-Amz-Security-Token", value: dummyRequest.headers["X-Amz-Security-Token"] } ];

  return signedRequest;
};

const handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const signedRequest = signCloudFrontOriginRequest(request);

  callback(null, signedRequest);
};

module.exports.handler = handler;

Обратите внимание, что если вы включаете тело в свой запрос, вам придется вручную настроить функцию Lambda @Edge, чтобы оно включало тело через консоль или SDK, или настроить пользовательский ресурс CloudFormation для вызова SDK, поскольку CloudFormation пока не поддерживает его изначально