Middle: питання на співбесіді з теми «Інфраструктура й секрети»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
6 питань
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, інакше їх використають для підробки.