AWS Serverless у дії: Як я будував фішингову serverless-архітектуру на AWS
Я — Михайло, 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‑файли.
- API Gateway із публічним HTTPS‑ендпоінтом
Фронтенд не намагається авторизувати користувача в 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 /logAPI 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 і форматом коду.
Логіка самої функції максимально проста:
- Розпарсити
event.bodyяк JSON. - Додати або змінити кілька полів (наприклад, додати серверний час).
- Сформувати унікальний ключ для об’єкта в S3 (наприклад, на основі часу чи UUID).
- Викликати
PutObjectCommandі записати JSON у потрібний бакет. - Повернути коректну 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.

— підтвердження того, що політика успішно додана до ролі.
Як виглядає повний шлях запиту
Якщо пройтись крок за кроком, типовий сценарій виглядає так:
- Я (або тестовий користувач) відкриваю у браузері клон сторінки логіну Instagram.
- Вводжу вигаданий логін і пароль.
- Фронтенд валідує поля й активує кнопку Log in.
- Після кліку JavaScript викликає
POST /logна API Gateway й відправляє JSON‑payload. - Перед цим браузер робить
OPTIONS /log(CORS preflight), на який API Gateway відповідає заздалегідь налаштованими заголовками. - Коли
POST /logпроходить, API Gateway у режимі Lambda Proxy Integration передає всю інформацію вLogCollectorFunction. - Lambda парсить тіло, за потреби збагачує його й пише JSON‑об’єкт у S3‑бекет.
- API Gateway формує кінцеву HTTP‑відповідь із потрібними заголовками, а фронтенд може показати користувачу «успішний» результат (наприклад, повідомлення або редирект).
У підсумку я маю:
- зручний UI, який поводиться як справжня сторінка логіну;
- повністю керовану backend‑інфраструктуру без серверів, якими треба адмініструвати;
- історію всіх подій у S3 у форматі JSON.
Візуально це добре видно на наступних скріншотах:

— приклад payload’у з «вкраденими» обліковими даними в UI.
Безпека — це не тільки про «захистити бекенд»
Коли я працюю як розробник, легко скотитися в думку «головне — не дати зламати API чи базу». Але цей проєкт ще раз показав, що endpoint може бути ідеально захищеним, але користувач просто віддасть свої дані сторонній сторінці.
Фішинг не зламує криптографію, він зламує людей. А завдання інженера — розуміти обидві сторони: і як будуються подібні схеми, і як їх можна помічати, блокувати й пояснювати іншим.
Висновки
Я не будував цей проєкт для того, щоб показати, «як легко красти паролі». Я будував його для себе як end‑to‑end стенд на базі AWS serverless‑сервісів, де:
- фронтенд імітує реальний фішинговий UX;
- бекенд приймає й логує події, використовуючи API Gateway, Lambda та S3;
- IAM показує, як важливо розуміти, хто й куди має доступ.
У процесі роботи я не тільки поглибив знання про AWS, але й ще раз переконався, наскільки крихкою є довіра користувача до знайомих інтерфейсів. Якщо ми як інженери розуміємо, як такі речі будуються, нам значно простіше будувати й системи захисту.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

8 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів