У любого, кто держит на VPS своего бота, парсера или фонового скрипта, рано или поздно появляется пароль от базы данных, токен Telegram-бота или ключ внешнего API, который нужно куда-то деть. Написать значение прямо в коде проще всего, но ненадолго: рано или поздно этот код попадает в git – его выгружают на GitHub, бэкапят на другой сервер, форкают для друга, и вместе с кодом туда уезжает секрет. Ровно для такой ситуации придуман файл .env. Он выносит то, что меняется от сервера к серверу и не должно быть видно постороннему, в отдельный текстовый файл рядом с кодом.
Сам факт выноса пароля в отдельный файл при этом ничего не гарантирует. .env – это просто текст в формате КЛЮЧ=ЗНАЧЕНИЕ, без шифрования и без встроенной защиты, и операционная система не делает с ним ничего особенного, пока какая-то программа не прочитает файл и не передаст значения процессу. Поэтому если файл лежит в папке с git-репозиторием и никто не проверил .gitignore, он закоммитится вместе с остальным кодом при первом же git add ., а при правах доступа 644 его прочитает любой пользователь на том же сервере, даже без рутовых прав. Дальше в статье – что класть внутрь .env, а чему там делать нечего, как подключить файл к процессу тремя разными способами и какие две ошибки чаще всего сводят всю эту защиту на нет.
Что такое .env и зачем выносить секреты из кода
Официального стандарта у формата .env нет. Это соглашение, которое в 2012 году ввела Heroku, а в 2013-м закрепила библиотека dotenv для Node.js. После этого формат разошёлся почти по всем языкам программирования (по данным dotenv.org). Правила простые: одна пара КЛЮЧ=ЗНАЧЕНИЕ на строку, без кавычек, если значение не содержит пробелов, и решётка для комментариев. Волшебства в самом файле при этом нет, ведь Python, Node.js или systemd не подхватывают его автоматически – нужна отдельная библиотека или директива, которая явно читает файл и кладёт пары в окружение процесса.
Методология Twelve-Factor App объясняет, зачем вообще выносить конфигурацию в переменные окружения: такие значения «легко менять между деплоями без изменения кода», а в отличие от конфигурационных файлов внутри репозитория у переменных окружения «почти нет риска случайно оказаться закоммиченными» (12factor.net). Поэтому пароль от продакшен-базы и пароль от тестовой базы становятся просто двумя разными значениями одной и той же переменной, и код при переходе между окружениями переписывать не нужно.
Что реально должно быть внутри .env
Внутрь идут значения, одновременно секретные и разные от сервера к серверу: пароль и адрес базы данных, токен Telegram-бота, ключ внешнего API, приватный URL вебхука. По принципу Twelve-Factor App каждая такая переменная должна быть независимой (orthogonal) от остальных, из-за этого группировка в именованный набор вроде «prod» или «staging» самой методологии противоречит: с ростом числа серверов такие наборы множатся, и конфигурацию становится сложно поддерживать. На каждом сервере в итоге лежит свой .env с одинаковым набором ключей, но разными значениями, а логика вида «если ENV=prod, то...» внутри кода самой программы не нужна.
Не всё, что можно вынести в .env, стоит туда класть. Значения, которые не меняются между деплоями и не являются секретом (тайм-ауты, названия таблиц, форматы даты), по 12-факторной модели конфигурацией не считаются, потому что это уже часть логики приложения и её нормальное место прямо в коде. У переменных окружения есть и обратная граница. Методичка OWASP по управлению секретами предупреждает, что они доступны любому процессу и могут попасть в лог или в дамп памяти при падении. Поэтому для по-настоящему высокой ставки, вроде платёжного ключа или мастер-токена с правами на всё, следующей ступенью зрелости становится секрет-менеджер, который выдаёт значение процессу по требованию и может отозвать его без переразвёртывания сервера. Для одного бота на своём VPS такой уровень чаще избыточен, но о потолке полезно знать заранее, до того как проект неожиданно вырастет.
Как подключить .env к процессу
Файл сам по себе ничего не запускает: переменные из него должны попасть в окружение процесса, а конкретный способ зависит от того, чем процесс управляется – systemd-сервисом, самостоятельным Python-скриптом или контейнером Docker, и у каждого варианта свой механизм подключения.
systemd – EnvironmentFile
Бот на VPS чаще всего запущен как systemd-сервис, и в этом случае .env-подобный файл может прочитать сам systemd, без единой строчки Python. За это отвечает директива EnvironmentFile= в секции [Service]: она построчно подключает файл в формате КЛЮЧ=ЗНАЧЕНИЕ, с # для комментариев, а значения не требуют кавычек, если не содержат пробелов. Файл при этом парсится буквально, без выполнения шелл-команд вроде $(date) (man7.org – systemd.exec(5)).
[Service]
WorkingDirectory=/opt/mybot
EnvironmentFile=/opt/mybot/.env
ExecStart=/opt/mybot/venv/bin/python bot.py
Дефис перед путём (EnvironmentFile=-/opt/mybot/.env.local) делает файл необязательным, и сервис запустится, даже если такого файла нет – это удобно для локальных переопределений поверх основного набора. После правки .env сервис не подхватывает новые значения сам, потому что переменные читаются один раз при старте процесса, и без systemctl restart mybot изменения не применятся.
Python – python-dotenv
Для бота, который запускается вручную или через cron, без systemd, .env читает сам код – библиотека python-dotenv, реализующая тот же 12-факторный подход. Достаточно двух строк:
from dotenv import load_dotenv
load_dotenv()
По умолчанию load_dotenv() ищет .env в папке скрипта и выше по дереву каталогов, а уже существующие переменные окружения не перезаписывает: если значение задано снаружи, например, тем же systemd, оно и остаётся в приоритете. Документация библиотеки прямо рекомендует добавить .env в .gitignore, особенно если там пароль – это единственная явная рекомендация по безопасности во всём README библиотеки.
Docker – env_file
В Docker Compose роль .env раздваивается, и это частый источник путаницы. Файл .env в корне проекта подставляет значения в сам compose-файл, например, ${TAG} в строке image: myapp:${TAG}. Атрибут env_file: внутри описания сервиса работает иначе. Он передаёт переменные внутрь контейнера и в интерполяции самого compose-файла не участвует (docs.docker.com).
services:
bot:
build: .
env_file:
- .env
Если указать env_file: списком из нескольких файлов, при совпадении имени переменной побеждает значение из файла, который идёт последним в списке, а явно прописанный блок environment: перекрывает оба файла, даже если значение там пустое. Ещё грубее ошибка, когда секрет прописывают через ENV или ARG прямо в Dockerfile: такое значение остаётся в слоях образа навсегда, ведь слои Docker неизменяемы, и docker history --no-trunc вытаскивает его даже после того, как секрет удалили следующей командой RUN rm. Для секретов на этапе сборки в BuildKit есть отдельный флаг --secret, который монтирует значение только на время одной инструкции и не сохраняет его в итоговом образе; разница между этим флагом и обычным ARG подробно разобрана в блоге laplusda.com.
Почему открытый .env находят даже без целенаправленной атаки
Открытый .env-файл далеко не всегда находит тот, кто целился именно в этот сервер. Чаще его вытаскивает автоматическое сканирование по всей сети, без разбора конкретных целей. Такую кампанию описали исследователи Unit 42 в Palo Alto Networks: атакующие просканировали более 230 миллионов целей и скопировали открытые .env-файлы минимум с 110 000 доменов. Из них извлекли свыше 90 000 уникальных комбинаций переменных окружения, и в каждой уже находились ключи доступа или IAM-credentials, то есть весь этот массив состоял из готовых к использованию секретов. Внутри него около 7000 составляли ключи доступа именно облачных сервисов (unit42.paloaltonetworks.com).
Дальше весь сценарий шёл автоматически: найденный ключ AWS сначала проверяли вызовом GetCallerIdentity, затем перечисляли пользователей и бакеты через ListUsers и ListBuckets, и там, где прав хватало, создавали новую IAM-роль с именем lambda-ex и подключали к ней политику AdministratorAccess. Через эту роль разворачивались Lambda-функции, которые сканировали уже следующие цели, а из скомпрометированных S3-бакетов выгружали данные, удаляли их и оставляли на пустом месте бакета записку с требованием оплаты за нераспространение.
Сами исследователи Unit 42 назвали три причины, из-за которых атака вообще стала возможна: открытые переменные окружения, статические ключи вместо временных ролей и отсутствие принципа минимальных привилегий у скомпрометированного доступа. Ни один пункт не требует, чтобы жертву искали специально, ведь сканер находит файл вне зависимости от того, кто его владелец, а масштаб проблемы этим единичным случаем не ограничивается. По данным нескольких независимых источников, только GitHub в 2024 году обнаружил на платформе более 39 миллионов утёкших секретов, а отчёт GitGuardian о состоянии утечек за 2026 год насчитал 28,65 миллиона новых секретов в публичных репозиториях за 2025 год – на 34% больше, чем годом раньше.
Две ошибки, из-за которых секреты утекают чаще всего
Чаще всего секрет утекает до того, как про .env вообще вспомнили: файл просто оказывается в git одним из первых коммитов. .gitignore при этом защищает только файлы, которые ни разу не попадали в индекс. Если .env закоммитили хотя бы раз, а потом спохватились и добавили его в .gitignore, файл всё равно остаётся в истории репозитория, и достать его оттуда командой git log --all --full-history -- .env так же легко, как случайному человеку, склонировавшему репозиторий.
# .gitignore
.env
.env.*
!.env.example
Если утечка уже произошла, начинать стоит со смены самих значений: отозвать старый ключ и выпустить новый. Официальная инструкция GitHub по удалению чувствительных данных из репозитория начинается именно с этого шага, потому что после ротации утёкшая копия для доступа больше не годится, и переписывать историю репозитория целиком иногда вообще не требуется.
Права доступа на файл подводят не реже git-истории. Новый файл на сервере создаётся с правами 644 по умолчанию, а при таких правах прочитать .env может любой локальный пользователь системы – не только владелец. chmod 600 .env закрывает эту лишнюю дверь: чтение и запись остаются только у владельца. Если процесс работает от отдельного пользователя, например, www-data, разумнее переоформить владельца файла на него (chown www-data .env), чем расширять права до 640 на всю группу. По похожему пути в 2026 году пошли и в CLI-инструменте GitLab, добавив проверку, что конфигурационные файлы должны иметь строго 600, хотя раньше их мог менять кто угодно.
Частые вопросы
Что делать, если .env уже попал в git-историю?
Сначала стоит отозвать старый пароль или токен и выпустить новый. Тогда утёкшая копия перестаёт быть рабочим ключом доступа независимо от того, останется она в истории репозитория или нет. Всю историю вычищать нужно не в каждом случае – это касается прежде всего ситуаций, когда репозиторий публичный и его успели форкнуть. Официальный инструмент для этого называется git filter-repo, с флагом --sensitive-data-removal, но после чистки всё равно понадобится форс-пуш и обращение в поддержку GitHub: своими силами убрать файл из чужих клонов не получится.
Чем .env отличается от переменных окружения systemd?
Сам .env – просто текстовый файл, он ничего не запускает и никуда не подгружается сам по себе. Загружает его в процесс сама systemd: директива EnvironmentFile= читает файл и передаёт значения при старте сервиса, минуя любые сторонние библиотеки на стороне приложения. Поэтому если бот управляется systemd, отдельная Python-библиотека для чтения .env вообще не нужна: файл в том же формате подключает сам юнит.
Нужно ли шифровать .env файл?
На сервере, где и так работает процесс, читающий этот файл в открытом виде, шифрование самого файла проблему не решает, потому что секрет всё равно оказывается в памяти процесса расшифрованным в момент использования. Реальную защиту дают права доступа 600, файл вне git и минимальные привилегии у самого ключа – то есть ограничение того, кто и как может добраться до файла и что даёт украденное значение. А вот при передаче файла между серверами или в бэкапе шифрование уже пригождается, потому что расшифровывать его в моменте там не нужно.
Можно ли держать один .env на dev, staging и prod?
Формально можно, но 12-факторная модель рекомендует иначе: конфигурация должна определяться самим деплоем, то есть отдельным сервером со своим файлом. Условие вроде if ENV == "prod" внутри кода превращает конфигурацию в часть логики приложения и ломает эту независимость. Удобнее держать свой .env на каждом сервере с одинаковым набором ключей, но разными значениями – тогда смена пароля в проде не требует трогать код и не рискует случайно утащить продакшен-значение в тестовую ветку.
Заключение
Файл .env решает узкую задачу, отделяя то, что различается между серверами и не должно быть видно постороннему, от кода приложения. Сам формат при этом ничего не гарантирует: гарантию дают привычки вокруг файла. .gitignore нужен до первого коммита, тогда секрет физически не успевает попасть в историю репозитория, а права 600 сразу после создания файла закрывают доступ для всех, кроме владельца или процесса, которому он реально нужен. Способ подключения стоит выбирать по тому, что уже есть в стеке – EnvironmentFile у systemd, python-dotenv в скрипте или env_file в Docker Compose. Тогда поверх готового механизма не добавляется ещё один самодельный слой.
Автор статьи разбирает похожие технические истории с серверами и ботами в своём Telegram-канале – t.me/aipractiq.
Комментарии