Питання на співбесіді: Інфраструктура й секрети
Питання з реальних співбесід з відповідями: 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 (навіть на хвилину, навіть у приватний репозиторій):
- вважати всі секрети з файлу скомпрометованими і змінити їх - згенерувати нові ключі API, паролі до бази, новий токен бота. Це головний крок;
- видалити файл з репозиторію й додати в
.gitignore; - за потреби переписати історію (
git filter-repo) - але це вже другорядне: копія могла лишитися у форках, клонах, кешах CI, на GitHub у вигляді доступних за хешем комітів.
Видалення файлу без ротації ключів нічого не захищає - автоматичні сканери знаходять секрети в публічних репозиторіях за лічені хвилини.
Профілактика:
- сканування секретів перед пушем: GitHub secret scanning з push protection,
gitleaksчиtrufflehogу pre-commit або CI; - окремі ключі для кожного оточення (локальне, staging, продакшен) - витік локальних ключів не зачіпає продакшен;
- ключі з найменшими правами - токен, якому потрібно лише читати, не повинен уміти писати;
- для командної роботи - зашифрований
.env(php artisan env:encrypt) чи менеджер секретів, а не пересилання файлу в месенджері.
Інструменти, що допомагають під час розробки, на продакшені стають готовою розвідкою для зловмисника: вони показують запити до бази, змінні оточення, сесії, токени й внутрішню структуру застосунку. Сканери шукають їхні адреси автоматично.
Що перевірити перед запуском:
| Що | Чим небезпечне відкрите | Як закрити |
|---|---|---|
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) для всіх, крім адміністраторів. Але це доповнення до закритих інструментів, а не заміна.
Застосунок часто підключається до бази під обліковим записом з усіма правами - 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) з ширшими правами - кращий варіант для продакшену.
У 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.
Бекап захищає від багатьох сценаріїв: помилкового 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).
Сервер, щойно отримавши публічну 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 - найважливіше налаштування: версії, що не отримують виправлень безпеки, мають відомі невиправлені вразливості.
Протокол 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; без них листи потрапляють у спам або відхиляються.
Як впроваджувати без зламаної пошти:
- SPF і DKIM для кожного сервісу, що відправляє від вашого домену (Mailgun, SES, CRM, служба підтримки);
- DMARC з
p=noneі звітами - кілька тижнів дивитися, хто ще відправляє; - поступово
quarantine, потімreject.
Окремий піддомен для транзакційної пошти (mg.your-app.com) ізолює її репутацію від маркетингових розсилок.
Домени без пошти теж треба захистити: v=spf1 -all і p=reject, інакше їх використають для підробки.
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) - без збережених секретів:
- на кожен запуск пайплайну CI-платформа (GitHub Actions, GitLab CI) видає короткоживучий підписаний токен з даними про запуск: репозиторій, гілка, середовище, workflow;
- хмарний провайдер (AWS, GCP, Azure) довіряє CI-платформі як провайдеру ідентичності й обмінює цей токен на тимчасові облікові дані на хвилини;
- довіра обмежена умовами: «лише репозиторій
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, секрети не потрапляють у журнали.
У хмарі застосунок отримує права не через ключі в .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)
Ротація секретів потрібна регулярно (обмежити шкоду від непоміченого витоку) і негайно після підозри на компрометацію. Складність - не зламати роботу під час заміни.
Загальний принцип - період, коли діють обидва ключі:
- створити новий секрет;
- налаштувати систему приймати і старий, і новий;
- перевести всіх, хто використовує секрет, на новий;
- відкликати старий.
Так працює ротація ключів 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: м'яка ротація ключів шифрування