AWS Serverless у дії: Як я будував фішингову serverless-архітектуру на AWS

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Я — Михайло, Full‑stack інженер із понад п’ятьма роками досвіду у фулстак-розробці, а останній рік цілеспрямовано вчусь DevOps і працюю з AWS, Terraform та багато інших DevOps-технологій.

Так з’явилася ідея побудувати повністю serverless‑архітектуру, яка імітує класичну фішингову атаку: я створив піксель‑перфектний клон сторінки логіну Instagram, який замість реальної авторизації відправляє введені користувачем дані у серверлес‑пайплайн на AWS — через API Gateway → Lambda → S3.

Фішинг як навчальний кейс

Фішинг — один із найстаріших і водночас найефективніших векторів атак. Компанії можуть витрачати мільйони на захист інфраструктури, але одна необережна дія користувача в листі чи браузері зводить ці інвестиції нанівець. Я неодноразово бачив, як навіть технічні люди «клюють» на добре зроблені фішингові сторінки або емейл-повідомлення.

Я поставив собі питання: наскільки складно, спираючись на мій досвід та зацікавленність у security, зібрати робочий фішинговий пайплайн на AWS за сучасними best practices? Скільки для цього потрібно коду, конфігурації, IAM‑прав, і які підводні камені виникнуть у процесі?

Важливий моральний аспект: я будував цю штуку як освітній лабораторний проєкт:

  • Ніяких реальних жертв.
  • Дані — тестові.
  • Мета — навчитись.

При цьому я намагався зробити все максимально наближеним до production‑рівня, щоб проєкт був корисним і мені як інженеру, і тим, хто захищає системи від подібних атак.

Загальна ідея проєкту

Як виглядає архітектура, якщо спростити її до суті:

  • На фронтенді — клон логін‑форми Instagram.
  • На бекенді — serverless‑пайплайн на AWS:
    • API Gateway із публічним HTTPS‑ендпоінтом /log.
    • Lambda на Node.js 22.x, яка приймає подію, парсить JSON та пише її в S3.
    • S3‑бакет, куди складаються всі «залоговані» події як окремі JSON‑файли.

Фронтенд не намагається авторизувати користувача в Instagram. Його єдина реальна функція — зібрати введені дані, сформувати коректний JSON і відправити його на AWS.

У ролі «хакера» я свідомо максимально спираюся на керовані сервіси: не піднімаю свої сервери, не налаштовую власноруч Nginx, не тримаю базу. Мені важливо було зрозуміти, як далеко можна зайти, використовуючи лише managed‑компоненти.

Front-End: клон сторінки логіну Instagram

Почав я з найвидимішої частини — інтерфейсу, якому люди повірять.

Я зробив піксель‑перфектну копію сторінки логіну Instagram за допомогою звичайних HTML/CSS/JavaScript. Головний виклик тут — не технічний, а психологічний: сторінка має виглядати достатньо знайомо й «офіційно», щоб людина не задумалась, що це підробка.

Ось як виглядає реальна сторінка логіну Instagram:

Хаха! А це моя копія, повірили, так?) (напишіть в коменти, що повірили)

Я приділив увагу дрібницям:

  • розміщенню форми та логотипу;
  • шрифтам і відступам;
  • поведінці кнопки Log in;
  • адаптивності для різних розмірів екранів.

Під капотом сторінка робить зовсім інше, ніж очікує користувач. Замість справжньої авторизації я:

  • валідую форму на фронтенді (щоб не відправляти пусті поля);
  • збираю дані у структурований JSON‑об’єкт;
  • додаю ще поля (час, URL, user agent тощо);
  • відправляю цей JSON на ендпоінт AWS API Gateway методом POST /log.

У підсумку ззовні все виглядає як банальний логін, але насправді я буду зберігати введені дані в S3‑бакеті.

Які дані я логую

Мій фронтенд відправляє не тільки логін і пароль. Я хотів, щоб лог був максимально корисним для подальшого аналізу, тому в JSON входять:

  • stolen_user — введене ім’я користувача;
  • stolen_pass — введений пароль;
  • source_url — URL сторінки, з якої користувач відправив форму;
  • timestamp — ISO‑мітка часу, згенерована в браузері.

Звісно, у реальному світі така кількість даних перетворює невинний «логін» у потужне джерело чутливої інформації. У моєму випадку це лише тестова лабораторія, але саме тому такий проєкт добре відкриває очі на масштаби ризиків.

AWS Architecture: serverless‑пайплайн для логування

Бекенд у цьому проєкті — це комбінація трьох сервісів AWS:

  • API Gateway як вхідна точка HTTP;
  • Lambda на Node.js як обробник подій;
  • S3 як довгострокове сховище логів.

Я спеціально не використовував додаткові сервіси на кшталт DynamoDB або RDS, щоб залишити архітектуру максимально простою й фокусною саме на serverless-сценарії (та, звичайно, дешевшою).

AWS API Gateway: Lambda Proxy Integration і CORS

Я створив REST API в регіоні eu-central-1 з ресурсом /log. Для нього налаштовано два методи:

  • POST /log — основний вхідний ендпоінт, який прив’язаний до Lambda через Lambda Proxy Integration;
  • OPTIONS /log — Mock‑метод для обробки CORS preflight‑запитів.

Чому я вибрав Lambda Proxy Integration?

  • я отримую повний HTTP‑запит у Lambda: тіло, заголовки, параметри, метод;
  • я можу централізовано керувати CORS‑заголовками в API Gateway, а не розмазувати це між кодом і конфігурацією;

Особливу увагу я приділив CORS. Якщо CORS налаштувати неправильно, браузер просто не дозволить фронтенду звертатися до бекенду, і вся «атака» провалиться ще до Lambda.

Тому я зробив так:

  • для OPTIONS /log API Gateway завжди повертає:
    • Access-Control-Allow-Origin: *;
    • Access-Control-Allow-Methods: OPTIONS,POST;
    • Access-Control-Allow-Headers: Content-Type,Authorization;
  • для POST /log потрібні заголовки так само задаються на рівні метод/інтеграція в API Gateway через мапінги у Integration Response.

Це дозволяє мені тримати Lambda чистою від CORS‑логіки і сфокусованою лише на бізнес‑частині — обробці події та записі в S3.

Щоб було наочніше, ось кілька скріншотів із консолі AWS, які фіксують ключові кроки налаштування API Gateway:

— створення публічного REST API, де живе ресурс /log.

— створення ресурсу /log під API.

— створення методу POST /log, який приймає події з форми логіну.

— створення OPTIONS /log як Mock‑методу для CORS preflight.

— загальний вигляд остаточної конфігурації /log у консолі.

— діалог деплою API на stage, після чого ендпоінт стає доступним зовні.

— сторінка зі stage та фінальним invoke URL, який я використовую у script.js.

Request/response shaping:

— фінальний mapping template, який безпечно передає сирий JSON у Lambda.

— додаткові заголовки, які я перекидаю з клієнта в Lambda.

— мапінг заголовків в Integration Response, щоб віддати їх клієнту.

— фінальний набір заголовків у Method Response для CORS.

— налаштовані CORS‑заголовки спеціально для Mock‑методу OPTIONS /log.

AWS Lambda: перехід на Node.js 22.x та ES Modules

Lambda‑функцію я назвав LogCollectorFunction. Вона працює на Node.js 22.x і використовує модульну систему ES Modules (import / export).

Спочатку я, за звичкою, написав код у стилі CommonJS (require). На старих рантаймах AWS Lambda це працювало б без проблем, але на Node.js 22.x я відразу отримав винятки на кшталт ReferenceError / Runtime.Unknown. Це змусило мене по‑справжньому розібратися з тим, як саме Lambda інтерпретує модулі й як коректно використовувати AWS SDK v3 в середовищі ES Modules.

У підсумку я:

  • переписав імпорти на синтаксис import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';;
  • налаштував структуру файлу так, щоб хендлер експортувався як export const handler = async (event) => { ... };
  • переконався, що конфігурація Lambda йде в ногу з версією Node.js і форматом коду.

Логіка самої функції максимально проста:

  1. Розпарсити event.body як JSON.
  2. Додати або змінити кілька полів (наприклад, додати серверний час).
  3. Сформувати унікальний ключ для об’єкта в S3 (наприклад, на основі часу чи UUID).
  4. Викликати PutObjectCommand і записати JSON у потрібний бакет.
  5. Повернути коректну HTTP‑відповідь (наприклад, statusCode: 200 і просте JSON‑повідомлення).

Приклад того, як виглядає реальна Lambda‑функція:

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({ region: 'eu-central-1' });
const BUCKET_NAME = 'YOUR_BUCKET_NAME'

export const handler = async event => {
  if (!event.body) {
    console.error('Missing request body');
    return {
      statusCode: 400,
      body: JSON.stringify({ message: 'Missing request body' }),
    };
  }

  let logData;
  try {
    logData = JSON.parse(event.body);
  } catch (e) {
    console.error('JSON Parsing Error:', e);
    return {
      statusCode: 400,
      body: JSON.stringify({ message: 'Invalid JSON format in request body' }),
    };
  }

  const logString = JSON.stringify(logData, null, 2);
  const timestamp = new Date().toISOString();
  const key = `logs/${timestamp}-${Math.random().toString(36).substring(2, 9)}.json`;

  try {
    await s3.send(
      new PutObjectCommand({
        Bucket: BUCKET_NAME,
        Key: key,
        Body: logString,
        ContentType: 'application/json',
      }),
    );

    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Log recorded successfully' }),
    };
  } catch (error) {
    console.error('Final Critical Error writing to S3:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Internal Server Error (Check CloudWatch Logs)' }),
    };
  }
};

Далі більше, а саме IAM та політики доступу.

S3: просте, але показове сховище логів

Для зберігання логів я використовую S3 (наприклад, logs). Кожен вхідний запит із фішингового фронтенда перетворюється на окремий JSON‑файл у цьому бекеті.

Це дає мені кілька переваг:

  • усі події зберігаються в сирому вигляді — я завжди можу повернутись і подивитися оригінальний payload;
  • легко підключити додаткову аналітику — наприклад, Athena чи Glue, якщо захочу аналізувати статистику по «атаках»;
  • у production‑сценаріях подібний підхід дає хорошу основу для побудови security‑моніторингу та розслідувань інцидентів.

Приклад того, як виглядають збережені файли в S3, та їхній вміст:

— приклад збереженого JSON‑логу в бекеті logs-22321.

— вміст одного з файлів у logs-22321.

Звичайно, у реальній системі логувати паролі в чистому вигляді — це табу. Але в рамках лабораторного фішингового стенду це навпаки підкреслює, наскільки небезпечною може бути навіть проста форма на сторінці.

IAM: мінімально необхідні права (плюс одна свідома поблажка)

Окремий блок, який я хотів краще відпрацювати на практиці, — це IAM‑ролі.

Для Lambda‑функції я створив роль, у якій:

  • є стандартна політика AWSLambdaBasicExecutionRole — логування в CloudWatch та базові операції Lambda;
  • додана політика, яка дає доступ до S3. У моєму навчальному варіанті це була AmazonS3FullAccess.

У продакшн‑системі таку політику майже завжди треба замінити на щось набагато точніше: доступ тільки до конкретного бекета, а краще — ще й до конкретного префікса, без права видаляти об’єкти тощо. Але в моєму випадку я свідомо залишив full access, щоб не витрачати час на побудову надто детальної IAM‑моделі й сконцентруватися на загальній картині.

Попри це, сама практика налаштування ролей, прикріплення політик до ролей і функцій добре освіжає розуміння того, як AWS реально працює з доступами.

— прикріплення політики AmazonS3FullAccess до ролі Lambda.

— підтвердження того, що політика успішно додана до ролі.

Як виглядає повний шлях запиту

Якщо пройтись крок за кроком, типовий сценарій виглядає так:

  1. Я (або тестовий користувач) відкриваю у браузері клон сторінки логіну Instagram.
  2. Вводжу вигаданий логін і пароль.
  3. Фронтенд валідує поля й активує кнопку Log in.
  4. Після кліку JavaScript викликає POST /log на API Gateway й відправляє JSON‑payload.
  5. Перед цим браузер робить OPTIONS /log (CORS preflight), на який API Gateway відповідає заздалегідь налаштованими заголовками.
  6. Коли POST /log проходить, API Gateway у режимі Lambda Proxy Integration передає всю інформацію в LogCollectorFunction.
  7. Lambda парсить тіло, за потреби збагачує його й пише JSON‑об’єкт у S3‑бекет.
  8. API Gateway формує кінцеву HTTP‑відповідь із потрібними заголовками, а фронтенд може показати користувачу «успішний» результат (наприклад, повідомлення або редирект).

У підсумку я маю:

  • зручний UI, який поводиться як справжня сторінка логіну;
  • повністю керовану backend‑інфраструктуру без серверів, якими треба адмініструвати;
  • історію всіх подій у S3 у форматі JSON.

Візуально це добре видно на наступних скріншотах:

— приклад payload’у з «вкраденими» обліковими даними в UI.

Безпека — це не тільки про «захистити бекенд»

Коли я працюю як розробник, легко скотитися в думку «головне — не дати зламати API чи базу». Але цей проєкт ще раз показав, що endpoint може бути ідеально захищеним, але користувач просто віддасть свої дані сторонній сторінці.

Фішинг не зламує криптографію, він зламує людей. А завдання інженера — розуміти обидві сторони: і як будуються подібні схеми, і як їх можна помічати, блокувати й пояснювати іншим.

Висновки

Я не будував цей проєкт для того, щоб показати, «як легко красти паролі». Я будував його для себе як end‑to‑end стенд на базі AWS serverless‑сервісів, де:

  • фронтенд імітує реальний фішинговий UX;
  • бекенд приймає й логує події, використовуючи API Gateway, Lambda та S3;
  • IAM показує, як важливо розуміти, хто й куди має доступ.

У процесі роботи я не тільки поглибив знання про AWS, але й ще раз переконався, наскільки крихкою є довіра користувача до знайомих інтерфейсів. Якщо ми як інженери розуміємо, як такі речі будуються, нам значно простіше будувати й системи захисту.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Amazon таке не апрувить,це прям в політиках прописано. Можуть ваш акк залочити, якщо якось задетектять

Дякую за коментар. Цей проєкт робив у першу чергу для додаткової практики, щоб поклацати та протестувати підхід.
Для AWS ззовні це виглядає як звичайний Public API або як взаємодія між мікросервісами — стандартний патерн, який часто зустрічається в AWS-архітектурах.

Там не все так просто. Навіть якщо ви хостите вебморду десь в іншому місці, у Amazon є сервіси, які моніторять підозрілу активність. Для S3 існує AI-сервіс Amazon Macie, що перевіряє бакети на наявність потенційних даних користувачів. Так, це платний сервіс, який ви можете використовувати, але в них однозначно є внутрішні інструменти для виявлення підозрілої активності.

Ось що з цього приводу каже ChatGpt:

AWS monitors services (including Lambda, API Gateway, S3) for patterns commonly used in phishing:
Domains or URLs mimicking login/financial/bank pages
Collecting personal data from users
Hosting login forms or fake pages
S3 buckets receiving credential-like data
Abuse complaints reported by users, ISPs, or victim organizations
Even if your Lambda simply writes “user data” to S3, AWS can observe:
Request patterns
Logging behavior
API Gateway usage
Abuse reports pointing to your resources
Any such signals can trigger an automatic abuse investigation.

Як останній крок не вистачає редіректу на instagram, адже після введення логін/пароль, якщо нічого не відбулось, то навіть у довірливих користувачів виникнуть підозри.

Як навчальний проект — це чудово. Якщо хочеться більше копнути в тему фішингу, то є кілька готових інструментів, щоб не «вигадувати колесо» з нуля.

getgophish.com
github.com/kgretzky/evilginx2

Дякую за пораду по редіректу, повністю згоден.

Partitioning треба було б в s3 зробити, якщо просто все зберігати в корінь бакета, то на великих об’ємах буде не гуд в плані білінга

Більше питання не в білінгу, а в тому, що на префікс є обмеження по RPS — 5000. Якщо покласти все в корін/один префікс рано чи пізно впретесь в це обмеження.
UPD: Amazon S3 has a limit of 5500 requests per second per prefix. Трошки промахнувся=)

Підписатись на коментарі