python-dotenv; git; openssl; nginx и envsubst (пакет gettext) — для варианта с прокси; доступ к менеджеру секретов (HashiCorp Vault или облачный Secret Manager — команды и синтаксис сверьте с документацией провайдера); действующий API-ключ LLM-провайдера.
Термины, которые понадобятся дальше:
ИМЯ=значение, который читает приложение при старте. Ключ лежит отдельно от кода.Схема, к которой мы идём:
[приложение] --без ключа--> [nginx 127.0.0.1:8080] --Bearer <ключ>--> [API провайдера]
^ ^
| |
читает .env (dev) ключ подставляет прокси (prod)
Проверьте, что инструменты на месте:
python3 --version
pip --version
git --version
openssl version
nginx -v
envsubst --version
Пример: ожидаемый вывод — по строке на инструмент, например git version 2.x.x и nginx version: nginx/1.x.x. Если какая-то команда отвечает command not found, установите пакет средствами вашего дистрибутива (apt install nginx gettext и т.п.) — конкретные имена пакетов сверьте с документацией ОС.
Почему это работает: все дальнейшие шаги опираются на то, что ключ физически лежит в одном месте, а доступ к нему ограничен правами файловой системы и списком получателей в менеджере секретов.
~/llm-appЧто сделать: создайте в корне проекта три файла: .env (реальный ключ), .env.example (шаблон без значения) и .gitignore.
cd ~/llm-app
printf 'LLM_API_KEY=\nLLM_BASE_URL=http://127.0.0.1:8080/v1\n' > .env.example
cp .env.example .env
sed -i 's|^LLM_API_KEY=.*|LLM_API_KEY=<ваш-ключ>|' .env
printf '.env\n.env.*\n!.env.example\n' > .gitignore
chmod 600 .env
<ваш-ключ> — строка, которую выдал провайдер. chmod 600 оставляет чтение только владельцу файла. Строка !.env.example нужна, чтобы шаблон всё-таки попал в репозиторий.
Зачем: .env в .gitignore — единственная защита от самой частой утечки: случайного git add .. Шаблон .env.example при этом документирует для коллег, какие переменные нужны, не раскрывая значений.
Как проверить:
git check-ignore -v .env
ls -l .env
git status --short
Ожидаемый результат: git check-ignore печатает строку вида .gitignore:1:.env .env; ls -l показывает права -rw-------; git status показывает .env.example и .gitignore, но не .env.
Пример: если git check-ignore -v .env ничего не вывел, файл не игнорируется — не коммитьте ничего, пока не почините .gitignore.
Дополнительно проверьте, что ключ не зашит в коде:
grep -rn "LLM_API_KEY=" --include="*.py" .
Ожидаемо — пусто (ключ должен встречаться только в .env).

Что сделать: перенесите значение из .env в менеджер секретов, а на машине приложения поднимайте прокси, который подставляет ключ на лету.
Загрузка секрета (два варианта, синтаксис сверьте с документацией выбранного инструмента):
# HashiCorp Vault
vault kv put secret/llm api_key=<ваш-ключ>
# AWS Secrets Manager
aws secretsmanager create-secret \
--name llm/api-key \
--secret-string '<ваш-ключ>'
Приложение читает переменные окружения, а не литералы:
import os
from dotenv import load_dotenv
load_dotenv() # в проде .env может отсутствовать
api_key = os.environ["LLM_API_KEY"] # упадёт с KeyError, если переменной нет
Теперь прокси. Создайте шаблон /etc/nginx/llm.conf.template:
server {
listen 127.0.0.1:8080;
location /v1/ {
proxy_pass https://api.<провайдер>.tld/;
proxy_set_header Authorization "Bearer ${LLM_API_KEY}";
proxy_set_header Content-Type "application/json";
}
}
<провайдер>.tld замените на хост API вашего поставщика. Подстановка значения и запуск:
export LLM_API_KEY="$(cat ~/llm-app/.env | grep '^LLM_API_KEY=' | cut -d= -f2-)"
envsubst < /etc/nginx/llm.conf.template > /etc/nginx/conf.d/llm.conf
nginx -t && nginx -s reload
Зачем: envsubst вставляет ключ в конфиг только в момент запуска на сервере, поэтому в git и в образе конфиг лежит с плейсхолдером ${LLM_API_KEY}. Браузер и фронтенд обращаются к 127.0.0.1:8080 и ключа не получают — его добавляет прокси. nginx -t проверяет синтаксис до перезагрузки, чтобы не уронить рабочий процесс.
Как проверить:
grep -c 'Bearer ${LLM_API_KEY}' /etc/nginx/conf.d/llm.conf
grep -c 'Bearer <ваш-ключ>' /etc/nginx/conf.d/llm.conf
Ожидаемо: первая команда — 1, вторая — 0. Если наоборот, шаблон уже был подставлен и ключ осел в файле на диске: пересоздайте конфиг из шаблона и проверьте права (chmod 640 /etc/nginx/conf.d/llm.conf).

Что сделать: прогоните четыре проверки: работоспособность прокси, отсутствие ключа в истории git, реакцию на неверный ключ и отсутствие ключа в артефактах.
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/v1/models
git log --all --oneline -- .env
grep -rn "sk-" --include="*.py" --include="*.json" --include="*.yml" .
docker history <ваш-образ> 2>/dev/null | grep -i key
Зачем: каждая команда закрывает свой канал утечки — сетевой, историю репозитория, исходники и слои контейнера.
Как проверить: ожидаемые результаты по порядку — 200; пустой вывод (файл никогда не коммитился); пустой вывод; пустой вывод. Затем убедитесь, что при неверном ключе запрос падает, а не проходит молча:
LLM_API_KEY=invalid nginx -s reload && curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/v1/models
Ожидаемо — 401. Верните рабочий ключ и перезагрузите nginx повторно. Точный формат тела ответа с ошибкой зависит от провайдера — сверьтесь с его документацией.
Итоговое состояние: ключ существует в двух местах — в менеджере секретов и в памяти процесса nginx; в репозитории, в коде и в браузере его нет.
Проблема. Приложение падает при старте, хотя .env на месте
ModuleNotFoundError: No module named 'dotenv'python-dotenv установлен в другое окружение (системный Python вместо виртуального).cd ~/llm-app && python3 -m venv .venv && source .venv/bin/activate && pip install python-dotenv
python -c "import dotenv; print('ok')"
Ожидаемый вывод — ok.
Проблема. Push в GitHub блокируется, хотя .env в .gitignore
remote: error: GH013: Repository rule violations found for refs/heads/main. (формулировка зависит от платформы — сверьтесь с её документацией).gitignore не удаляет уже закоммиченное.git rm --cached .env
git commit -m "remove .env from index"
git filter-repo --path .env --invert-paths
После перезаписи истории потребуется git push --force в свою ветку; команду согласуйте с владельцем репозитория.
Проблема. nginx не стартует после правки конфига
nginx: [emerg] "proxy_set_header" directive is not allowed here in /etc/nginx/conf.d/llm.conf:5location, например после закрывающей скобки.nginx -t
Ожидаемо: syntax is ok и test is successful. Только после этого — nginx -s reload.
Ключ хранится в менеджере секретов, подставляется прокси и никогда не попадает в git, код или браузер. Дальше настройте ротацию ключа по расписанию и добавьте проверку git check-ignore -v .env в pre-commit хук.
Можно ли хранить API-ключ в переменной окружения в проде?
Да, это рабочий минимум, но переменные окружения видны процессам и дампам окружения. Для прода используйте менеджер секретов и выдачу ключа на время жизни процесса.
Чем .env хуже менеджера секретов?
.env — обычный файл на диске: его легко случайно закоммитить или скопировать. Менеджер секретов даёт централизованный доступ, аудит и отзыв.
Что делать, если ключ уже попал в git?
Сначала отозвать ключ у провайдера и выпустить новый, потом чистить историю через git filter-repo. Удаление файла новым коммитом проблему не решает.
Нужен ли прокси, если уже есть менеджер секретов?
Нужен, если ключ использует фронтенд или сторонний сервис: прокси — единственный способ не отдавать ключ клиенту. Если ключ читает только ваш бэкенд, можно ограничиться менеджером секретов.