Де секрети живуть і як вони витікають
Laravel читає .env на старті через phpdotenv, файли в config/ беруть звідти значення через env(), а код читає config('services.stripe.secret'). Перший спосіб втратити все: вебсервер дивиться не в public/, а в корінь проєкту. Тоді GET /.env віддає файл цілком, а /.git/config підказує, що можна витягти й історію. У Laravel веб-корінь лише public/, і в ньому немає нічого секретного.
шифрує все, що Laravel шифрує: cookies, Crypt, касти encrypted, і підписує посилання URL::signedRoute. З ним можна розшифрувати й підробити cookie чи підписане посилання. Паролів він не стосується: їх хешує bcrypt. Звичайна заміна ключа розлогінить усіх і зробить зашифровані колонки нечитабельними, тому ротують через : новий ключ шифрує, старі ще розшифровують, поки дані не перешифровано.
Після php artisan Laravel бере конфігурацію з bootstrap/cache/config.php і .env не читає. env() поза config/ тоді повертає null для всього, що було лише в .env, і код тихо працює без ключа. Правило просте: env() тільки у файлах config/, усюди інше config(). Сам кешований файл містить усі секрети відкритим текстом, тож він живе там, де .env, і в git не потрапляє.
Секрет витікає і там, де його ніхто не друкував навмисно. =true на продакшені показує сторінку помилки з кодом, заголовками запиту разом із cookies, тілом запиту як є, включно з паролем, і SQL з підставленими значеннями. Log::info(..., $request->all()) пише паролі й токени в лог, а помилка HTTP-клієнта несе URL разом з ?api_key=. Атрибут з PHP 8.2 ховає значення аргументу в стек-трейсі.
Секрет, який потрапив у git, лишається в історії, клонах, форках і кешах CI. Видалити коміт мало: ключ треба спершу відкликати чи змінити в провайдера, а вже потім чистити історію через git filter-repo. Щоб такого не було, ставлять сканер секретів: gitleaks у CI чи push protection на GitHub, а секрети деплою зберігають у secrets CI. Laravel ще вміє : php artisan env:encrypt створює .env.encrypted, який можна комітити, а ключ до нього лежить лише на сервері.
Три кроки від коміту до чужого рахунку
- Розробник створює .env.
- .env використовує Історія git (push).
- Дані йдуть від Історія git до Сканер секретів.
- Сканер секретів надсилає подію ваш ключ, її отримує API провайдера.
Секрет з оточення, у трейсі й у лозі його немає
-
1
рядки 3–7
Справжнє значення живе в .env на сервері чи в secrets CI і в git не потрапляє. zend.exception_ignore_args = On у php.ini для продакшену теж ховає аргументи, але покладатися лише на конфігурацію сервера не варто.
-
2
рядки 14–17
#[\SensitiveParameter] з PHP 8.2: у стек-трейсі замість значення буде Object(SensitiveParameterValue). Звичайна функція plain() показує пароль відкритим текстом.
-
3
рядки 19–26
Порівнюємо дві функції з однаковим кодом. Стек-трейс потрапляє в лог і на сторінку помилки, тож різниця тут саме та, що побачить чужа людина.
-
4
рядки 28–39
Контекст для логу проходить через фільтр за назвами ключів, включно з вкладеними масивами. У Laravel простіше не класти зайве: $request->except('password') або only() з потрібними полями.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
Laravel і Symfony бережуть секрети, крім цих місць
$key = env('STRIPE_SECRET');
Чому це дірка: Після config:cache .env не читається, і env() поза config/ поверне null. Код не впаде, а піде до API без ключа. Читайте config('services.stripe.secret'), а env() лишайте файлам config/.
APP_DEBUG=true
Чому це дірка: На продакшені сторінка помилки покаже заголовки запиту з cookies, тіло запиту як є, включно з полем password, і SQL зі значеннями. Тримайте APP_DEBUG=false і дивіться помилки в логах.
root /var/www/app;
Чому це дірка: Вебсервер дивиться в корінь проєкту, і /.env та /.git/config віддаються як файли. Коренем має бути /var/www/app/public.
git rm --cached .env && git commit
Чому це дірка: Файл зник з наступних комітів, але лишився в попередніх і в кожному клоні. Спершу відкличте й замініть кожен ключ, потім чистіть історію і додайте .env у .gitignore.
php artisan key:generate --force
Чому це дірка: Новий ключ без APP_PREVIOUS_KEYS розлогінить усіх, зламає підписані посилання й зробить касти encrypted нечитабельними. Старий ключ спершу переносять у APP_PREVIOUS_KEYS.
Log::info('login', $request->all());
Чому це дірка: У лог іде пароль відкритим текстом, а логи читає більше людей і сервісів, ніж базу. Логуйте лише потрібні поля: $request->only('email').
run: echo "${{ secrets.DEPLOY_KEY }}" | base64
Чому це дірка: GitHub маскує в логах лише точне значення секрету. Закодоване чи розрізане воно друкується як є. Не виводьте секрети взагалі, навіть для налагодження.
php bin/console secrets:set STRIPE_SECRET
Чому це дірка: Сховище секретів Symfony шифрує значення в config/secrets/prod, і цю теку можна комітити. Файл prod.decrypt.private.php комітити не можна: з ним розшифрується все.
Де тут витікає секрет
Натисніть на рядки, у яких бачите проблему, і перевірте. Тут чотири проблеми.
Вставте у свій шаблон пул-реквесту
- .env і файли з секретами в .gitignore і .dockerignore, у репозиторії лише .env.example з порожніми значеннями.
- env() викликається тільки у файлах config/, код читає config(), на продакшені виконано config:cache.
- Вебсервер дивиться в public/, на продакшені APP_DEBUG=false.
- У логах і текстах помилок немає паролів, токенів і ключів: логуються лише потрібні поля, параметри з паролями позначено #[\SensitiveParameter].
- Ключі API передаються заголовком, а не в query string.
- Витік ключа означає відкликати й замінити його в провайдера, APP_KEY ротується через APP_PREVIOUS_KEYS.
- CI сканує коміти на секрети, а секрети деплою зберігаються в secrets CI і ніде не друкуються.
Як цей сайт тримає секрети
У репозиторії немає жодного справжнього секрету. .gitignore не пускає в git .env, .env.backup, .env.production, auth.json і deploy/.env.docker, а .dockerignore не кладе .env, .env.* і deploy/.env.docker в образ. Розробник бере .env.example, де всі ключі порожні. Продакшен-оточення збирає deploy/make-env.sh з шаблону й окремого файлу секретів і записує результат з правами 600. Шаблон deploy/.env.docker.example має APP_DEBUG=false і рядок «Never commit the real file». Код читає секрети лише через config(), а env() стоїть у файлах config/, як у фрагменті нижче.
.gitignore.env, .env.backup, .env.production, /auth.json, /deploy/.env.docker і /storage/*.key ніколи не потрапляють у коміт..env.exampleСписок усього, що потрібно застосунку, з порожніми значеннями: APP_KEY, ключі входу через Google, LinkedIn і GitHub, доступи до R2.deploy/make-env.shЗбирає deploy/.env.docker із шаблону й файлу секретів (APP_KEY, DB_PASSWORD, CONTENTOPS_TOKEN) і ставить на результат chmod 600.deploy/.env.docker.exampleШаблон продакшен-оточення без значень секретів: APP_ENV=production, APP_DEBUG=false, LOG_LEVEL=warning.- APP_KEY
- Ключ застосунку Laravel: 32 випадкові байти в base64. Ним шифрують cookies, Crypt і касти encrypted, ним підписують signed URL. Генерується php artisan key:generate.
- APP_PREVIOUS_KEYS
- Список старих ключів через кому. Encrypter пробує їх, коли поточний ключ не підходить, а signed URL перевіряються і ними. Дає змінити APP_KEY без масового розлогіну.
- config:cache
- Команда, що збирає всі файли config/ в один PHP-масив у bootstrap/cache/config.php. Прискорює кожен запит, але після неї .env не завантажується.
- APP_DEBUG
- Режим налагодження. true показує детальну сторінку винятку з кодом і даними запиту, false лише загальну сторінку 500. На продакшені завжди false.
- #[\SensitiveParameter]
- Атрибут параметра з PHP 8.2. У стек-трейсі винятку значення такого аргументу замінюється обʼєктом SensitiveParameterValue, тож пароль не потрапить у лог чи на сторінку помилки.
- env:encrypt
- Команда Laravel, що шифрує .env (за замовчуванням AES-256-CBC) у .env.encrypted і друкує ключ. На сервері файл розшифровує env:decrypt з --key або змінною LARAVEL_ENV_ENCRYPTION_KEY.
Питання зі співбесід
Історія git живе у кожному клоні, форку, архіві й кеші CI, а доступ до приватного репозиторію мають підрядники, токени й інтеграції. Видалення файлу пізнішим комітом нічого не прибирає. Комітять .env.example з порожніми значеннями, а справжні секрети живуть на сервері чи в secrets CI.
Код викликає env() поза файлами config/. Після config:cache .env не завантажується, і env() повертає null для значень, яких немає в справжньому оточенні процесу. Треба перенести env() у config/services.php і читати config('services.pay.secret').
З ключем можна розшифрувати й підробити cookies, дані з Crypt і касти encrypted, створити валідне підписане посилання. Паролі під bcrypt не постраждають. Новий ключ ставлять в APP_KEY, старий переносять у APP_PREVIOUS_KEYS: Laravel шифрує новим і пробує старі при розшифруванні. Потім перешифровують дані й прибирають старий ключ, бо доки він у списку, підроблене ним ще приймається.
Справжні значення лише в оточенні процесу чи в менеджері секретів (Vault, AWS Secrets Manager, secrets CI), у репозиторії .env.example. Якщо команді зручніше версіонувати оточення, env:encrypt дає .env.production.encrypted у git, а ключ розшифрування живе лише в CI і на сервері як LARAVEL_ENV_ENCRYPTION_KEY. Окремі ключі для кожного середовища, мінімальні права, сканер секретів у CI і план ротації для кожного ключа, перевірений до витоку, а не після.