← Все гайды AI Release
EN Подписаться на @ai_release1

Как безопасно хранить API-ключи для LLM-сервисов: .env, менеджеры секретов и прокси

Обновлено: 21.10.2026 · AI Release · @ai_release1
Как безопасно хранить API-ключи для LLM-сервисов: .env, менеджеры секретов и прокси

TL;DR

Требования и подготовка

Иллюстрация к разделу «Требования и подготовка»
Иллюстрация к разделу «Требования и подготовка»

Термины, которые понадобятся дальше:

Схема, к которой мы идём:

[приложение] --без ключа--> [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 и т.п.) — конкретные имена пакетов сверьте с документацией ОС.

Почему это работает: все дальнейшие шаги опираются на то, что ключ физически лежит в одном месте, а доступ к нему ограничен правами файловой системы и списком получателей в менеджере секретов.

Шаг 1. Изолируйте ключ в .env и запретите его коммитить

Что сделать: создайте в корне проекта три файла: .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).

Шаг 2. Менеджер секретов и прокси: ключ не попадает в код и в браузер

Иллюстрация к разделу «Шаг 2. Менеджер секретов и прокси: ключ не попадает в код и в браузер»
Иллюстрация к разделу «Шаг 2. Менеджер секретов и прокси: ключ не попадает в код и в браузер»

Что сделать: перенесите значение из .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).

Шаг 3. Проверка работы и финальный результат

Иллюстрация к разделу «Шаг 3. Проверка работы и финальный результат»
Иллюстрация к разделу «Шаг 3. Проверка работы и финальный результат»

Что сделать: прогоните четыре проверки: работоспособность прокси, отсутствие ключа в истории 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 на месте

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

git rm --cached .env
git commit -m "remove .env from index"
git filter-repo --path .env --invert-paths

После перезаписи истории потребуется git push --force в свою ветку; команду согласуйте с владельцем репозитория.

Проблема. nginx не стартует после правки конфига

nginx -t

Ожидаемо: syntax is ok и test is successful. Только после этого — nginx -s reload.

Резюме и следующие шаги

Ключ хранится в менеджере секретов, подставляется прокси и никогда не попадает в git, код или браузер. Дальше настройте ротацию ключа по расписанию и добавьте проверку git check-ignore -v .env в pre-commit хук.

FAQ

Можно ли хранить API-ключ в переменной окружения в проде?

Да, это рабочий минимум, но переменные окружения видны процессам и дампам окружения. Для прода используйте менеджер секретов и выдачу ключа на время жизни процесса.

Чем .env хуже менеджера секретов?

.env — обычный файл на диске: его легко случайно закоммитить или скопировать. Менеджер секретов даёт централизованный доступ, аудит и отзыв.

Что делать, если ключ уже попал в git?

Сначала отозвать ключ у провайдера и выпустить новый, потом чистить историю через git filter-repo. Удаление файла новым коммитом проблему не решает.

Нужен ли прокси, если уже есть менеджер секретов?

Нужен, если ключ использует фронтенд или сторонний сервис: прокси — единственный способ не отдавать ключ клиенту. Если ключ читает только ваш бэкенд, можно ограничиться менеджером секретов.

🤖 Кратко для ИИ