Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Інфраструктура й секрети

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

15 питань

Секрети - паролі до бази, APP_KEY, ключі API, токени платіжних систем - не повинні жити в коді. У Laravel їх зберігають у файлі .env, який:

  • ніколи не комітиться - він уже є в .gitignore стандартного проєкту;
  • має шаблон .env.example у репозиторії - з назвами змінних, але без справжніх значень;
  • на сервері доступний лише користувачу, від якого працює застосунок (chmod 600), і лежить поза публічним каталогом (public/).

У коді секрети читаються лише через конфігурацію: config('services.stripe.secret'), а env() - тільки у файлах config/*.php. Після php artisan config:cache виклики env() поза конфігурацією повертають null.

Якщо .env потрапив у Git (навіть на хвилину, навіть у приватний репозиторій):

  1. вважати всі секрети з файлу скомпрометованими і змінити їх - згенерувати нові ключі API, паролі до бази, новий токен бота. Це головний крок;
  2. видалити файл з репозиторію й додати в .gitignore;
  3. за потреби переписати історію (git filter-repo) - але це вже другорядне: копія могла лишитися у форках, клонах, кешах CI, на GitHub у вигляді доступних за хешем комітів.

Видалення файлу без ротації ключів нічого не захищає - автоматичні сканери знаходять секрети в публічних репозиторіях за лічені хвилини.

Профілактика:

  • сканування секретів перед пушем: GitHub secret scanning з push protection, gitleaks чи trufflehog у pre-commit або CI;
  • окремі ключі для кожного оточення (локальне, staging, продакшен) - витік локальних ключів не зачіпає продакшен;
  • ключі з найменшими правами - токен, якому потрібно лише читати, не повинен уміти писати;
  • для командної роботи - зашифрований .env (php artisan env:encrypt) чи менеджер секретів, а не пересилання файлу в месенджері.

Докладніше в документації: Laravel: конфігурація оточення

Інструменти, що допомагають під час розробки, на продакшені стають готовою розвідкою для зловмисника: вони показують запити до бази, змінні оточення, сесії, токени й внутрішню структуру застосунку. Сканери шукають їхні адреси автоматично.

Що перевірити перед запуском:

Що Чим небезпечне відкрите Як закрити
Laravel Telescope (/telescope) запити з тілами, SQL, винятки, вміст сесій і кешу лише --dev залежність або gate viewTelescope з переліком адмінів
Laravel Horizon (/horizon) дані завдань у черзі (часто з email чи id), можливість повторити чи видалити завдання Gate::define('viewHorizon', ...) - за замовчуванням на не-локальному середовищі доступу немає ні в кого, але ворота часто «відкривають» для зручності
Debugbar SQL з параметрами, сесія, конфігурація на кожній сторінці лише в require-dev, APP_DEBUG=false
Сторінка винятку (APP_DEBUG=true) стек викликів, шляхи, фрагменти коду, змінні оточення APP_DEBUG=false на продакшені
phpinfo(), info.php версії, модулі, змінні середовища, іноді ключі не тримати такі файли в public/
Pulse, Log Viewer та інші панелі метрики, логи з персональними даними авторизація через Gate
.env, .git, storage/, composer.json секрети й вихідний код корінь вебсервера - лише public/
адмінка й Filament повний доступ до даних canAccessPanel(), двофакторна автентифікація, бажано обмеження за IP чи VPN

Принципи:

  • dev-пакети - у require-dev, а на продакшені composer install --no-dev: якщо пакета немає, його неможливо випадково відкрити;
  • ворота (Gate) замість «таємної адреси» - змінений шлях /telescope-x7k2 не захист, його знаходять перебором і з логів;
  • перевірка ззовні: після деплою відкрити ці адреси без входу (чи прогнати сканер) - очікується 404 чи 403, а не сторінка інструменту;
  • автоматичний тест: Pest-тест, що гість отримує 403 на /horizon і /telescope, ловить випадкову зміну воріт ще до продакшену.

Додатковий шар - WAF чи правило на рівні проксі, що блокує типові службові шляхи (/phpinfo, /.env, /_ignition, /telescope) для всіх, крім адміністраторів. Але це доповнення до закритих інструментів, а не заміна.

Докладніше в документації: Laravel: розгортання

Застосунок часто підключається до бази під обліковим записом з усіма правами - root у MySQL чи postgres у PostgreSQL. Поки все працює, різниці не видно. Різниця з'являється, коли щось іде не так.

Що дає зловмиснику всемогутній користувач:

  • через SQL-ін'єкцію - не лише читання даних, а DROP DATABASE, читання й запис файлів сервера (LOAD DATA, COPY ... TO PROGRAM у PostgreSQL для суперкористувача), створення нових користувачів;
  • через вкрадений .env - повний контроль над усіма базами на сервері, а не лише над даними застосунку.

Принцип найменших прав - кожен обліковий запис має лише те, що йому справді потрібно:

Користувач Права
застосунок SELECT, INSERT, UPDATE, DELETE на свою базу
міграції (деплой) + CREATE, ALTER, DROP, INDEX на свою базу
аналітика, звіти лише SELECT, бажано на репліці
бекап читання й блокування, без зміни даних
-- PostgreSQL
CREATE ROLE app LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE shop TO app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app;

Інші правила:

  • база не доступна з інтернету: слухає лише внутрішню мережу чи localhost, порт закритий фаєрволом;
  • окремі користувачі для кожного застосунку на спільному сервері бази - витік одного не відкриває інші;
  • з'єднання з TLS, якщо база на іншому сервері;
  • довгі випадкові паролі, різні для кожного оточення;
  • журнал підключень і помилок автентифікації.

Практичний компроміс у Laravel: міграції часто запускають тим самим користувачем, що й застосунок. Окреме підключення для міграцій (--database=migrations) з ширшими правами - кращий варіант для продакшену.

Докладніше в документації: OWASP: безпека баз даних

У Laravel-проєкті кореневий каталог сайту (document root) - лише public/. Там лежать index.php і зібрані ассети. Усе інше - код, .env, storage/, vendor/, .git/ - має бути поза доступом вебсервера.

Типова помилка - document root вказує на корінь проєкту:

root /var/www/shop;          # неправильно
root /var/www/shop/public;   # правильно

Тоді за прямими посиланнями доступні:

  • /.env - паролі до бази, APP_KEY, ключі API. Сканери перевіряють цей шлях на мільйонах сайтів щодня;
  • /.git/ - з каталогу .git інструменти на кшталт git-dumper відновлюють увесь вихідний код і історію комітів, включно з колись видаленими секретами;
  • /storage/logs/laravel.log - стеки помилок, інколи персональні дані;
  • /composer.json, composer.lock - точні версії залежностей для пошуку відомих вразливостей;
  • резервні копії й дампи (backup.sql, .env.bak, index.php~), залишені в публічних каталогах.

Як захиститися:

  • document root = public/ - головне правило;
  • у Nginx - заборона прихованих файлів, крім .well-known (потрібен для сертифікатів Let's Encrypt і security.txt):
location ~ /\.(?!well-known).* {
    deny all;
}
  • не класти в public/ нічого, крім того, що має бути публічним. Файли користувачів - через storage:link лише для публічного диска, приватні - через контролер з перевіркою прав;
  • не деплоїти .git на сервер, якщо він не потрібен (збірка артефакту в CI);
  • регулярна перевірка ззовні: curl -I https://example.com/.env має повертати 404 чи 403. Такі перевірки варто додати в моніторинг.

WAF-правила (наприклад, у Cloudflare), що блокують запити до .env, .git, wp-admin, phpmyadmin, - корисний додатковий рубіж, але не заміна правильному document root.

Докладніше в документації: Laravel: налаштування Nginx

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

Правило 3-2-1:

  • 3 копії даних (основна + дві резервні);
  • на 2 різних носіях чи типах сховищ;
  • 1 копія поза основною інфраструктурою - інший провайдер, регіон, обліковий запис.

Захист від вимагачів і зловмисників:

  • зловмисник, що отримав доступ до сервера, видалить і бекапи, якщо сервер має права на їх видалення. Тому:
    • сервер має лише право дописувати нові копії, але не видаляти чи перезаписувати старі;
    • незмінні копії - Object Lock в S3-сумісних сховищах (режим compliance), версіонування бакетів;
    • окремий обліковий запис для бекапів, до якого немає доступу з продакшену;
  • офлайн- чи ізольовані копії - CISA рекомендує тримати хоча б одну копію поза мережею.

Шифрування: бекапи містять усі персональні дані й секрети. Вони мають бути зашифровані, а ключ - зберігатися окремо від бекапів (і не лише на тому самому сервері).

Що бекапити:

  • базу даних (дамп чи фізична копія + журнали для відновлення на момент у часі);
  • файли користувачів (storage/app, бакет S3);
  • конфігурацію й секрети (зашифрованими) - без них відновлений застосунок не запуститься.

Найважливіше - перевірка відновлення. Бекап, який жодного разу не відновлювали, - лише надія. Регулярне (автоматизоване) відновлення в окреме оточення з перевіркою:

  • дамп розгортається без помилок;
  • ключові таблиці мають очікувану кількість записів;
  • застосунок запускається на відновлених даних.

Так заодно вимірюється реальний час відновлення - і він часто неприємно дивує.

Моніторинг: сповіщення, якщо бекап не створився чи його розмір різко змінився (і раптове зменшення - ознака проблеми).

Докладніше в документації: CISA: посібник з протидії програмам-вимагачам

php artisan env:encrypt шифрує файл оточення, щоб його можна було безпечно зберігати в репозиторії - разом з кодом, з історією змін і рев'ю, але без відкритих секретів.

php artisan env:encrypt --env=production
# створює .env.production.encrypted, виводить ключ (показується один раз)
  • шифр за замовчуванням - AES-256-CBC (можна змінити через --cipher);
  • ключ генерується автоматично або передається через --key;
  • --prune - видалити вихідний відкритий файл після шифрування;
  • --readable - шифрувати кожне значення окремо, залишаючи назви змінних відкритими: у diff видно, яка змінна змінилася, без розкриття значення.

Розшифрування на сервері під час деплою:

php artisan env:decrypt --env=production --key="$LARAVEL_ENV_ENCRYPTION_KEY"

Ключ береться з опції --key або змінної оточення LARAVEL_ENV_ENCRYPTION_KEY. Тобто на сервер (чи в CI) потрапляє один секрет замість десятків.

Що це дає:

  • секрети версіонуються разом з кодом - відомо, хто й коли змінив конфігурацію;
  • новий член команди отримує доступ до конфігурації оточення через один ключ;
  • немає «живих» .env, що пересилаються в месенджерах.

Обмеження й ризики:

  • увесь захист - в одному ключі. Його витік відкриває всі секрети з усієї історії репозиторію. Ключ зберігають у менеджері секретів CI чи хостингу, а не поруч з кодом;
  • ротація секретів все одно потрібна - шифрування файлу не скасовує заміну скомпрометованих значень;
  • історія: зашифровані версії лишаються в Git назавжди. Якщо ключ колись витік, старі версії з тоді чинними секретами теж розкриті;
  • доступ «все або нічого»: будь-хто з ключем бачить усі секрети оточення. Для розмежування доступу краще повноцінний менеджер секретів (Vault, AWS Secrets Manager, Doppler) чи SOPS з ключами для різних людей.

Добрий компроміс для невеликих команд: зашифрований .env.production у репозиторії, ключ - у змінних CI/хостингу, розшифрування на етапі деплою.

Докладніше в документації: Laravel: шифрування файлів оточення

Сертифікати - найчастіше від Let's Encrypt (безкоштовні, на 90 днів) через протокол ACME. Короткий термін - свідомий вибір: компрометований сертифікат швидше перестає діяти, а автоматизація стає обов'язковою.

Автоматизація замість ручного оновлення:

  • Caddy чи FrankenPHP отримують і оновлюють сертифікати автоматично, без жодних налаштувань;
  • Certbot з таймером systemd для Nginx/Apache;
  • хмарні балансувальники й CDN (Cloudflare, AWS) керують сертифікатами самі.

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

Конфігурація TLS - не вигадувати власну, а брати перевірену: Mozilla SSL Configuration Generator генерує налаштування для Nginx, Apache, HAProxy під профіль:

  • Modern - лише TLS 1.3 (сучасні клієнти);
  • Intermediate - TLS 1.2 і 1.3 з надійними шифрами (рекомендований для більшості сайтів);
  • Old - для дуже старих клієнтів (уникати).

Що має бути в налаштуваннях:

  • вимкнені SSL 3.0, TLS 1.0 і 1.1;
  • шифри з прямою секретністю (ECDHE) і AEAD (AES-GCM, ChaCha20-Poly1305);
  • HSTS (Strict-Transport-Security) після того, як HTTPS працює стабільно;
  • редирект з HTTP на HTTPS для сайту (для API - краще відмова);
  • OCSP stapling - якщо CA його підтримує (Let's Encrypt відмовляється від OCSP на користь коротших сертифікатів і CRL).

Перевірка: SSL Labs Server Test (оцінка A/A+), testssl.sh для внутрішніх серверів.

Внутрішній трафік: TLS, що закінчується на балансувальнику чи Cloudflare, лишає незашифрованим шлях до сервера застосунку. Для Cloudflare - режим Full (strict) з сертифікатом на сервері (зокрема безкоштовним Origin CA), а не Flexible, де трафік до origin іде по HTTP.

Приватні ключі - доступні лише процесу вебсервера, не в репозиторії, не в бекапах без шифрування.

Докладніше в документації: Mozilla SSL Configuration Generator

Контейнер - не віртуальна машина: він ділить ядро з хостом, і помилки конфігурації можуть дати вихід за його межі. Основні правила:

1. Не працювати від root.

FROM php:8.5-fpm-alpine
# ...
RUN addgroup -S app && adduser -S app -G app
USER app

Вразливість у застосунку, що дає виконання коду, в контейнері від root - це root у контейнері, і значно ближче до хоста.

2. Мінімальний образ. Alpine чи slim-варіанти, без компіляторів, curl, git і пакетів «на всяк випадок». Менше програм - менше вразливостей і інструментів для зловмисника. Багатоетапне збирання (multi-stage) - залежності й ассети збираються в одному етапі, а в фінальний образ потрапляє лише результат.

3. Жодних секретів в образі. COPY .env . чи ARG API_KEY лишаються в шарах образу назавжди - будь-хто з доступом до образу їх дістане (docker history, розпакування шарів). Секрети - змінними оточення чи змонтованими файлами під час запуску; для збирання - --mount=type=secret. Плюс .dockerignore з .env, .git, node_modules.

4. Файлова система лише для читання, де можливо (read_only: true у Compose), з окремими томами для storage/ і тимчасових файлів.

5. Обмеження прав контейнера:

  • cap_drop: [ALL] і лише потрібні можливості;
  • no-new-privileges: true;
  • ніколи privileged: true і монтування /var/run/docker.sock у контейнер застосунку - це фактично root на хості.

6. Оновлення й сканування:

  • закріплені версії базових образів (а ще краще - дайджест) і регулярне перезбирання з оновленою базою;
  • сканування вразливостей образів у CI: Trivy, Grype, Docker Scout.

7. Мережа й ресурси: контейнер бази не публікує порт назовні, ліміти пам'яті й CPU (захист від виснаження ресурсів).

8. Health check і журнали в stdout - щоб оркестратор бачив стан, а журнали не накопичувалися в контейнері.

Пастка закріплення: «плаваючий» тег (php:8.5-fpm) може раптово принести несумісні зміни, а закріплений назавжди - перестати отримувати виправлення безпеки. Рішення - закріплена версія + автоматичні PR оновлень (Renovate, Dependabot).

Докладніше в документації: OWASP: безпека Docker

Сервер, щойно отримавши публічну IP-адресу, за лічені хвилини починає отримувати спроби підбору паролів SSH і сканування портів. Базовий захист:

1. SSH:

  • вхід лише за ключами, паролі вимкнено (PasswordAuthentication no);
  • заборонено вхід root (PermitRootLogin no) - окремий користувач з sudo;
  • сучасні ключі (Ed25519), захищені парольною фразою;
  • доступ до SSH - лише з певних адрес чи через VPN/bastion (WireGuard, Tailscale), або хоча б fail2ban проти перебору.

Mozilla публікує рекомендовану конфігурацію OpenSSH з безпечними алгоритмами.

2. Фаєрвол - відкрито лише потрібне:

ufw default deny incoming
ufw allow 22/tcp        # або лише з адреси VPN
ufw allow 80,443/tcp
ufw enable

База даних, Redis, панелі моніторингу - не слухають публічний інтерфейс. Окрема пастка Docker: опубліковані порти (ports: 5432:5432) обходять ufw, бо Docker сам керує правилами iptables. Порти служб - лише на 127.0.0.1 або без публікації.

3. Оновлення: автоматичні оновлення безпеки (unattended-upgrades в Ubuntu/Debian), регулярне оновлення ядра з перезавантаженням.

4. Принцип найменших прав:

  • застосунок працює від окремого непривілейованого користувача;
  • права на файли: код - лише для читання процесом PHP, запис - лише в storage/ і bootstrap/cache/;
  • без chmod 777 «щоб запрацювало».

5. Мінімум служб: видалити непотрібне ПЗ, не встановлювати phpMyAdmin і подібні панелі на продакшені.

6. Журнали й моніторинг: спроби входу, sudo, зміни в системних файлах, незвичне навантаження. Журнали - на окремий сервер, щоб зловмисник не міг їх стерти.

7. Інфраструктура як код: налаштування через Ansible, cloud-init чи образи - відтворювано й перевірено, а не «руками колись налаштований сервер», про який ніхто не пам'ятає подробиць.

Керовані платформи (Laravel Cloud, Forge, Ploi, контейнерні платформи) беруть частину цього на себе - але відповідальність за доступи, секрети й оновлення застосунку лишається за командою.

Докладніше в документації: Mozilla: рекомендації для OpenSSH

Значення за замовчуванням у PHP розраховані на зручність розробки. Для продакшену частину треба змінити.

Не розкривати інформацію:

expose_php = Off          ; без заголовка X-Powered-By: PHP/8.5.x
display_errors = Off      ; помилки не виводяться користувачу
display_startup_errors = Off
log_errors = On           ; але записуються в журнал
error_reporting = E_ALL

Точна версія PHP у заголовку підказує зловмиснику, які відомі вразливості перевіряти. Виведені помилки розкривають шляхи файлів і фрагменти коду.

Обмежити ресурси (захист від виснаження):

memory_limit = 256M
max_execution_time = 30
max_input_time = 60
post_max_size = 20M
upload_max_filesize = 10M
max_input_vars = 1000

Вимкнути небезпечне:

allow_url_include = Off   ; include за URL - класичний шлях до виконання чужого коду
allow_url_fopen = Off     ; якщо застосунку не потрібно (HTTP-клієнти працюють через curl)

disable_functions - вимкнути функції виконання команд (exec, shell_exec, system, passthru, proc_open, popen), якщо застосунок їх не використовує. Обережно: Laravel-черги, Horizon, деякі пакети й команди artisan можуть потребувати proc_open - перевіряти, а не вимикати наосліп. Для CLI і FPM можна мати різні налаштування.

open_basedir обмежує каталоги, доступні PHP. Корисно на спільному хостингу, але це не межа безпеки (історично обходилося) і помітно сповільнює роботу з файлами. У контейнерах і на виділених серверах ізоляцію краще робити засобами ОС.

Сесії (якщо використовується нативна сесія PHP):

session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
session.cookie_samesite = Lax

Laravel має власну систему сесій з налаштуваннями в config/session.php, тож тут важливіше перевірити її.

OPcache для продакшену: opcache.validate_timestamps = 0 (швидкість), але тоді після деплою OPcache треба скидати - інакше працюватиме старий код, включно з невиправленою вразливістю.

Актуальна версія PHP - найважливіше налаштування: версії, що не отримують виправлень безпеки, мають відомі невиправлені вразливості.

Докладніше в документації: OWASP: конфігурація PHP

Протокол SMTP не перевіряє відправника: будь-хто може надіслати лист з From: security@your-app.com. Три DNS-записи дозволяють поштовим сервісам відрізнити справжній лист від підробки.

SPF - які сервери мають право відправляти пошту домену:

your-app.com.  TXT  "v=spf1 include:mailgun.org include:_spf.google.com -all"

Отримувач перевіряє IP-адресу сервера-відправника. Обмеження: SPF перевіряє технічну адресу конверта (Return-Path), а не видимий користувачу From, і ламається при пересиланні листів.

DKIM - криптографічний підпис листа:

mg._domainkey.your-app.com.  TXT  "k=rsa; p=MIGfMA0..."

Поштовий сервіс підписує заголовки й тіло приватним ключем, отримувач перевіряє підпис публічним ключем з DNS. Лист не можна змінити дорогою, а підпис прив'язаний до домену.

DMARC - політика, що зв'язує все разом:

_dmarc.your-app.com.  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc@your-app.com; adkim=s; aspf=s"
  • вимагає вирівнювання (alignment): домен у видимому From має збігатися з доменом, що пройшов SPF чи DKIM. Саме це захищає від підробки адреси, яку бачить користувач;
  • політика: p=none (лише звіти), p=quarantine (у спам), p=reject (відхилити);
  • rua - щоденні агреговані звіти: хто й скільки листів відправляє від імені домену.

Чому це питання безпеки, а не лише доставки:

  • фішинг від вашого імені: без DMARC зловмисник надсилає «Скиньте пароль за посиланням» з вашого домену, і листи виглядають справжніми;
  • листи скидання пароля й коди входу, що потрапили в спам чи відхилені, - користувачі не можуть увійти й звертаються в підтримку, де їх легше обдурити соціальною інженерією;
  • з 2024 року Gmail і Yahoo вимагають від масових відправників (понад 5000 листів на день до Gmail) SPF, DKIM і DMARC, а від усіх відправників - щонайменше SPF чи DKIM; без них листи потрапляють у спам або відхиляються.

Як впроваджувати без зламаної пошти:

  1. SPF і DKIM для кожного сервісу, що відправляє від вашого домену (Mailgun, SES, CRM, служба підтримки);
  2. DMARC з p=none і звітами - кілька тижнів дивитися, хто ще відправляє;
  3. поступово quarantine, потім reject.

Окремий піддомен для транзакційної пошти (mg.your-app.com) ізолює її репутацію від маркетингових розсилок.

Домени без пошти теж треба захистити: v=spf1 -all і p=reject, інакше їх використають для підробки.

Докладніше в документації: RFC 7489: DMARC

WAF (Web Application Firewall) - фільтр HTTP-запитів перед застосунком. Хмарні WAF (Cloudflare, AWS WAF) працюють на краю мережі, ще до вашого сервера.

Що він добре робить:

  • поглинає DDoS-атаки мережевого рівня й об'ємні HTTP-флуди - ваш сервер їх не бачить;
  • блокує відомі шаблони атак (керовані правила для SQL-ін'єкцій, XSS, відомих CVE популярного ПЗ) - захист «на час», поки вразливість не виправлена в коді;
  • відсікає ботів і сканери: запити до /.env, /wp-admin, /phpmyadmin, підозрілі User-Agent, перевірки на кшталт Managed Challenge;
  • обмеження частоти на рівні краю - для логіну, пошуку, API;
  • геоблокування, списки IP, правила за ASN.

Чого WAF не робить:

  • не виправляє вразливостей у логіці застосунку: BOLA (чужий id в URL), відсутня авторизація, бізнес-логіка - для WAF це звичайні коректні запити;
  • не бачить усього: зашифровані дані в тілі, нестандартні формати, атаки, розтягнуті в часі;
  • обходиться кодуванням і варіаціями payload-ів - сигнатурний захист не буває повним.

WAF - додатковий шар, а не заміна безпечному коду.

Головна пастка - відкритий origin. Якщо IP-адреса сервера відома (історичні DNS-записи, піддомен без проксі, заголовки листів, сертифікати в журналах Certificate Transparency), зловмисник іде напряму на сервер, оминаючи WAF і захист від DDoS.

Як закрити origin:

  • фаєрвол приймає 80/443 лише з діапазонів IP Cloudflare (список публікується й змінюється - його треба оновлювати);
  • Authenticated Origin Pulls (mTLS між Cloudflare і сервером) - сервер перевіряє клієнтський сертифікат Cloudflare;
  • Cloudflare Tunnel - сервер узагалі не має відкритих вхідних портів, з'єднання ініціює агент з сервера;
  • нова IP-адреса після підключення проксі, якщо стара вже «засвітилася»;
  • усі піддомени, що ведуть на той самий сервер, - через проксі.

Справжній IP клієнта: за проксі Laravel бачить IP Cloudflare. Потрібно довіряти заголовкам лише від проксі (trustProxies з діапазонами Cloudflare) і брати CF-Connecting-IP - інакше ліміти частоти й журнали працюватимуть з неправильними адресами.

Докладніше в документації: Cloudflare: захист origin-сервера

Класичний підхід: у секретах CI лежать статичні ключі хмарного облікового запису (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) з правами на деплой.

Проблеми статичних ключів:

  • живуть роками - їх рідко ротують, бо це болісно;
  • витікають: у журналах збирання, через скомпрометовану залежність чи action у пайплайні, через неуважний echo, через форк з доступом до секретів;
  • працюють звідусіль - вкрадений ключ можна використати з будь-якого комп'ютера;
  • часто мають завеликі права («адміністратор, щоб точно запрацювало»).

OIDC (OpenID Connect) - без збережених секретів:

  1. на кожен запуск пайплайну CI-платформа (GitHub Actions, GitLab CI) видає короткоживучий підписаний токен з даними про запуск: репозиторій, гілка, середовище, workflow;
  2. хмарний провайдер (AWS, GCP, Azure) довіряє CI-платформі як провайдеру ідентичності й обмінює цей токен на тимчасові облікові дані на хвилини;
  3. довіра обмежена умовами: «лише репозиторій org/shop, лише гілка main, лише оточення production».
permissions:
  id-token: write   # дозволити запуску отримати OIDC-токен
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/deploy-shop
      aws-region: eu-central-1

Що це дає:

  • нема чого красти - у секретах CI немає довгоживучих ключів;
  • облікові дані живуть хвилини і прив'язані до конкретного запуску;
  • точні умови: пул-реквест з форку чи інша гілка роль не отримають.

Що важливо налаштувати правильно:

  • умови довіри (claims) мають бути вузькими - лише repo без гілки чи середовища дозволить будь-якій гілці (зокрема з необережного PR) деплоїти в продакшен;
  • права ролі - найменші: деплой конкретного сервісу, а не адміністратор облікового запису;
  • захист оточень у CI (обов'язкове затвердження для production).

Решта гігієни CI: закріплення сторонніх actions за хешем коміту (а не тегом), мінімальні permissions для GITHUB_TOKEN, обережність з pull_request_target, секрети не потрапляють у журнали.

Докладніше в документації: GitHub Actions: OpenID Connect

У хмарі застосунок отримує права не через ключі в .env, а через роль, прикріплену до сервера чи контейнера (IAM-роль інстансу, workload identity). Облікові дані видає сервіс метаданих за внутрішньою адресою 169.254.169.254.

Чому це зручно: немає статичних ключів, тимчасові облікові дані оновлюються автоматично.

Чому це небезпечно без захисту: будь-яка вразливість, що дозволяє змусити сервер зробити HTTP-запит (SSRF - «завантажити зображення за URL», генерація PDF з HTML, вебхук на адресу користувача), дає зловмиснику доступ до метаданих:

http://169.254.169.254/latest/meta-data/iam/security-credentials/app-role

і облікових даних ролі. Кілька відомих масштабних витоків даних почалися саме так.

Захист сервісу метаданих:

  • IMDSv2 обов'язковий (AWS): спершу PUT-запит за сесійним токеном, потім запити з заголовком токена. Типова SSRF дозволяє лише GET без власних заголовків - і токен отримати не може. Для нових інстансів IMDSv2 варто вимагати явно (HttpTokens=required);
  • hop limit = 1 - відповідь сервісу метаданих не проходить через додатковий мережевий «стрибок»: контейнер без host-мережі не дістане метадані хоста;
  • у Kubernetes - IRSA/workload identity замість ролі вузла: кожен под отримує власну роль.

Принцип найменших прав для ролі:

  • лише потрібні дії й ресурси: s3:PutObject і s3:GetObject на arn:aws:s3:::shop-uploads/*, а не s3:* на *;
  • окремі ролі для веб-застосунку, воркерів черг, задач бекапу - компрометація одного не відкриває решту;
  • жодних прав на IAM (створення користувачів і ролей) у застосунку;
  • умови: обмеження за VPC, тегами, мережею;
  • регулярний аудит невикористаних прав (IAM Access Analyzer та аналоги).

Захист від SSRF у коді лишається обов'язковим: перевірка URL і IP після розв'язання DNS, заборона приватних діапазонів і 169.254.0.0/16, вимкнені редиректи.

Виявлення: журнали хмарного провайдера (CloudTrail) з підозрілими діями від імені ролі застосунку (наприклад, виклики з невідомих IP) - сигнал до негайного реагування.

Докладніше в документації: AWS: налаштування сервісу метаданих (IMDS)

Ротація секретів потрібна регулярно (обмежити шкоду від непоміченого витоку) і негайно після підозри на компрометацію. Складність - не зламати роботу під час заміни.

Загальний принцип - період, коли діють обидва ключі:

  1. створити новий секрет;
  2. налаштувати систему приймати і старий, і новий;
  3. перевести всіх, хто використовує секрет, на новий;
  4. відкликати старий.

Так працює ротація ключів API партнерів, паролів до бази (два користувачі чи дві паролі в PostgreSQL), підписів вебхуків.

APP_KEY у Laravel шифрує cookie (зокрема сесійну), зашифровані атрибути моделей (каст encrypted), дані Crypt::encrypt(). Проста заміна ключа:

  • розлогінить усіх користувачів - старі cookie неможливо розшифрувати;
  • зламає зашифровані в базі дані - їх більше неможливо прочитати.

М'яка ротація через APP_PREVIOUS_KEYS:

APP_KEY="base64:новий..."
APP_PREVIOUS_KEYS="base64:старий..."
  • шифрування - завжди новим ключем;
  • розшифрування - спершу новим, потім по черзі попередніми.

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

Але дані в базі самі не перешифруються. Значення, зашифровані старим ключем, лишаються такими, доки їх не перезапишуть. Перед тим як прибрати старий ключ із APP_PREVIOUS_KEYS, потрібна міграція даних: прочитати й зберегти кожне зашифроване значення (команда, що проходить по моделях частинами). Інакше видалення старого ключа - знову втрата даних.

Що ще залежить від APP_KEY: підписані URL (URL::signedRoute) - старі посилання стануть недійсними, якщо підпис не перевіряється попередніми ключами; знімки стану Livewire підписані ключем застосунку - відкриті сторінки отримають помилку при наступній дії.

Організаційно:

  • менеджер секретів (Vault, AWS Secrets Manager, Doppler, хмарні сховища) - центральне місце, журнал доступу, автоматична ротація для баз даних;
  • перелік, де використовується кожен секрет - без нього ротація перетворюється на пошук, що зламалося;
  • регламент реагування: хто і як ротує секрети при витоку, скільки часу це займає - відпрацьоване заздалегідь, а не вперше під час інциденту.

Докладніше в документації: Laravel: м'яка ротація ключів шифрування