Как безопасно хранить API-ключи в проекте: .env, GitHub Secrets, серверные переменные
Основной чат
Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.
В 2024 году компания GitGuardian обнаружила в публичных репозиториях GitHub более 12 миллионов секретов — API-ключей, паролей, токенов. Большинство из них попали туда случайно: разработчик просто забыл убрать ключ перед коммитом.
Последствия бывают разными. Кому-то списывают деньги за чужие запросы к OpenAI. Кому-то взламывают базу данных. Кому-то приходит счёт на несколько тысяч долларов за облачные ресурсы, которые кто-то использовал для майнинга.
Хорошая новость: защититься несложно. Нужно один раз выстроить правильную привычку — и большинство проблем исчезнет.
Почему нельзя хранить ключи в коде
Начнём с самого важного, потому что у новичков часто возникает вопрос: «Ну и что, если ключ в коде? Репозиторий же приватный».
Приватный репозиторий — не защита. Достаточно одной случайной смены видимости, одного взломанного аккаунта участника, одного неверного клика — и всё. Публичные репозитории сканируются ботами автоматически в течение секунд после появления коммита.
Git помнит всё. Даже если вы удалили ключ из файла и сделали новый коммит — в истории он остался. git log покажет все предыдущие версии файла. Удаление из текущего кода не удаляет из истории.
Команда видит ключи. Когда в коде написано const apiKey = "sk-abc123" — этот ключ видит каждый, кто клонирует репозиторий. Даже если вы доверяете всей команде, это плохая практика: ключи смешиваются, непонятно чей ключ используется, ротация превращается в кошмар.
Правило простое: секрет в коде — это уже не секрет.
Переменные окружения: основа основ
Правильное место для секретов — не файлы с кодом, а переменные окружения (environment variables). Это пары «имя=значение», которые операционная система предоставляет запущенному процессу. Код читает их в момент запуска — не хранит их в себе.
Читать переменные окружения просто:
// Node.js
const apiKey = process.env.OPENAI_API_KEY;
const dbUrl = process.env.DATABASE_URL;
# Python
import os
api_key = os.getenv('OPENAI_API_KEY')
db_url = os.getenv('DATABASE_URL')
Откуда берутся эти переменные — зависит от окружения. Локально — из .env файла. На сервере — из настроек хостинга. В CI/CD — из GitHub Secrets. Код одинаков везде, секреты — разные в каждом окружении.
.env файл: для локальной разработки
.env — это простой текстовый файл в корне проекта, где вы перечисляете переменные окружения для локальной работы.
# .env
OPENAI_API_KEY=sk-proj-abc123...
DATABASE_URL=postgresql://localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_xyz789...
TELEGRAM_BOT_TOKEN=123456:ABCdef...
Библиотека dotenv читает этот файл при запуске и загружает переменные:
# Node.js
npm install dotenv
// Первая строка в точке входа (index.js, app.js)
require('dotenv').config();
// Теперь process.env содержит всё из .env
console.log(process.env.OPENAI_API_KEY); // работает
# Python
pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv() # читает .env файл
api_key = os.getenv('OPENAI_API_KEY')
Самое важное: .env в .gitignore
Сразу после создания .env — добавьте его в .gitignore. Это список файлов и папок, которые Git игнорирует и не включает в коммиты.
# .gitignore
.env
.env.local
.env.production
.env.*.local
Проверьте, что файл действительно игнорируется:
git status # .env не должен появляться в списке
git check-ignore -v .env # должен показать: .gitignore:1:.env
.env.example: шаблон для команды
Если .env нельзя коммитить — как другие разработчики узнают, какие переменные нужны? Для этого создают .env.example — файл с теми же именами переменных, но без реальных значений. Его коммитить можно и нужно.
# .env.example — коммитится в репозиторий
OPENAI_API_KEY= # ключ от OpenAI, получить на platform.openai.com
DATABASE_URL= # строка подключения к PostgreSQL
STRIPE_SECRET_KEY= # тестовый ключ Stripe (sk_test_...)
TELEGRAM_BOT_TOKEN= # токен от @BotFather
Новый разработчик клонирует репозиторий, копирует файл и заполняет своими значениями:
cp .env.example .env
# открывает .env и заполняет значения
GitHub Secrets: для CI/CD и деплоя
Когда нужно запустить деплой или тесты через GitHub Actions — переменные окружения нужны уже не на вашем компьютере, а на серверах GitHub. Для этого используются GitHub Secrets.
Как добавить секрет
- Откройте репозиторий на GitHub
- Settings → Secrets and variables → Actions
- Нажмите «New repository secret»
- Введите имя (например
OPENAI_API_KEY) и значение - Сохраните
После этого значение зашифровано и не отображается даже в интерфейсе — только звёздочки.
Использование в GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to server
env:
# Переменные берутся из GitHub Secrets
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
# Здесь ваши команды деплоя
npm install
npm run build
npm run deploy
GitHub Secrets автоматически маскируются в логах — если значение случайно попадёт в вывод команды, оно будет заменено на ***.
Environments: разные секреты для dev и prod
Если у вас несколько окружений (staging, production) — для каждого можно создать отдельный набор секретов через GitHub Environments:
Settings → Environments → New environment → добавить секреты
jobs:
deploy-production:
environment: production # использует секреты из окружения production
steps:
- env:
DATABASE_URL: ${{ secrets.DATABASE_URL }} # production-значение
Серверные переменные: на хостинге и VPS
Когда приложение развёрнуто на сервере — там тоже нужно задать переменные окружения. Способ зависит от платформы.
Timeweb Cloud / VPS с PM2
Если запускаете Node.js через PM2:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: './index.js',
env: {
NODE_ENV: 'production',
// НЕ пишите секреты здесь — этот файл попадёт в репозиторий
},
env_file: '.env.production', // читать из отдельного файла
}]
};
Лучше создать на сервере отдельный .env.production прямо в командной строке:
# На сервере через SSH
nano /var/www/myapp/.env.production
# Вводите значения, сохраняете
# Права только для владельца:
chmod 600 /var/www/myapp/.env.production
Docker и docker-compose
# docker-compose.yml — НЕ пишите секреты напрямую
services:
app:
image: myapp
env_file:
- .env.production # файл на сервере, не в репозитории
Или передавайте через отдельный файл секретов:
# На сервере создаёте файл с секретами
echo "OPENAI_API_KEY=sk-..." >> /etc/myapp/secrets.env
docker run --env-file /etc/myapp/secrets.env myapp
Vercel, Railway, Render и другие PaaS
Все современные платформы для деплоя имеют интерфейс для переменных окружения. Это самый простой и безопасный вариант:
Vercel: Project → Settings → Environment Variables
Railway: Project → Variables
Render: Service → Environment
Переменные шифруются, доступны только в запущенном приложении и нигде не отображаются в открытом виде.
Что нельзя коммитить: полный список
Сохраните этот раздел и добавьте в .gitignore каждого проекта.
# .gitignore — секреты и конфиденциальные файлы
# Файлы переменных окружения
.env
.env.local
.env.development
.env.production
.env.staging
.env.*.local
# Ключи и сертификаты
*.pem
*.key
*.p12
*.pfx
id_rsa
id_ed25519
*.cert
# Конфиги с секретами (часто забывают)
config/secrets.yml
config/database.yml # если содержит пароли
secrets.json
credentials.json # Google Cloud credentials
service-account.json # Firebase / GCP service account
# Кошельки и ключи криптовалют (если работаете с Web3)
*.keystore
wallet.json
Помимо очевидного .env, особое внимание обратите на:
credentials.json и service-account.json — файлы сервисных аккаунтов Google Cloud и Firebase. Очень часто утекают, дают полный доступ к проекту.
*.pem и *.key — приватные ключи SSL-сертификатов и SSH. Их компрометация критична.
Конфиги баз данных — в некоторых фреймворках (Ruby on Rails, Laravel) файлы конфигурации содержат реальные пароли к БД.
Как проверить, не утекли ли ключи уже
Перед тем как считать репозиторий чистым — стоит проверить историю коммитов. Ключ мог попасть туда несколько месяцев назад.
git log — быстрый поиск по истории
# Ищем строки, похожие на секреты, во всей истории репозитория
git log -p --all | grep -i "api_key\|secret\|password\|token\|sk-\|Bearer"
truffleHog — автоматический сканер
# Устанавливаем
pip install trufflehog
# или через brew:
brew install trufflehog
# Сканируем репозиторий
trufflehog git file://. --only-verified
truffleHog знает форматы ключей сотен провайдеров — OpenAI, Stripe, AWS, GitHub и других. Он не просто ищет слово «key», а проверяет паттерн реального значения.
gitleaks — ещё один популярный сканер
# Скачать с github.com/gitleaks/gitleaks
gitleaks detect --source . -v
GitHub сам сканирует публичные репозитории
Если репозиторий публичный — GitHub уже ищет в нём секреты автоматически (функция Secret Scanning). При обнаружении вам приходит уведомление на email. Некоторые провайдеры (Stripe, OpenAI, GitHub) получают уведомление и автоматически инвалидируют ключ.
Что делать если ключ уже попал в репозиторий
Плохая новость: удаление файла или строки коммитом не поможет. Ключ остался в истории.
Хорошая новость: есть правильный порядок действий.
Шаг 1: немедленно отозвать ключ. Идите в панель провайдера и отзовите скомпрометированный ключ прямо сейчас. Не через час — сейчас. Если репозиторий был публичным хоть секунду — считайте ключ скомпрометированным.
Шаг 2: проверить логи на подозрительные запросы. Большинство провайдеров показывают историю использования ключа. Посмотрите, не было ли обращений с неизвестных IP.
Шаг 3: сгенерировать новый ключ и добавить в правильное место. Новый ключ — только в переменные окружения, не в код.
Шаг 4: почистить историю git (опционально, сложно). Если репозиторий приватный и вы уверены что никто не успел скопировать — можно переписать историю через git filter-repo. Но это сложная операция, требующая координации всей команды. Проще считать ключ утёкшим и просто сменить его.
# Установить git-filter-repo
pip install git-filter-repo
# Удалить все вхождения строки с ключом из истории
git filter-repo --replace-text <(echo 'sk-abc123...==>REMOVED')
После этого нужно сделать git push --force — что перепишет историю на сервере. Все, кто клонировал репозиторий, должны перекачать его заново.
Повторяем: самое важное — отозвать ключ, а не чистить историю. Смена ключа решает проблему безопасности. Чистка истории — опциональный шаг для порядка.
Дополнительный уровень: Vault и менеджеры секретов
Для небольших проектов .env и GitHub Secrets полностью достаточны. Но если проект растёт — появляются дополнительные инструменты.
HashiCorp Vault — специализированное хранилище секретов с контролем доступа, ротацией ключей и аудитом. Каждое приложение получает только те секреты, которые ему нужны, с ограниченным сроком действия.
AWS Secrets Manager / Parameter Store — аналог для AWS-инфраструктуры. Секреты хранятся в облаке, ротируются автоматически, права настраиваются через IAM.
Doppler, Infisical — более простые SaaS-альтернативы Vault. Синхронизируют переменные окружения между окружениями, интегрируются с CI/CD, есть бесплатные тарифы.
Для большинства вайбкодинг-проектов эти инструменты избыточны — поднимать Vault ради трёх API-ключей не стоит. Но полезно знать, что они существуют.
Быстрый старт: что сделать прямо сейчас
Если вы прочитали статью и хотите сразу привести проект в порядок — вот последовательность:
# 1. Создать .env с реальными значениями
touch .env
# 2. Добавить .env в .gitignore
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
# 3. Создать .env.example с пустыми значениями
cp .env .env.example
# Открыть .env.example и очистить все значения, оставив только имена
# 4. Проверить что .env не отслеживается git
git check-ignore -v .env
# 5. Если .env уже был добавлен в git — удалить из отслеживания
git rm --cached .env
# 6. Заменить все ключи в коде на process.env.НАЗВАНИЕ
# Было: const key = "sk-abc123"
# Стало: const key = process.env.OPENAI_API_KEY
# 7. Просканировать историю
git log -p --all | grep -iE "sk-|api[_-]?key|secret|password|token" | head -50
Чеклист безопасного хранения секретов
Локальная разработка:
☐ .env файл создан в корне проекта
☐ .env добавлен в .gitignore
☐ .env.example с пустыми значениями добавлен в репозиторий
☐ Нет ни одного реального ключа в файлах с кодом
Git-история:
☐ Просканирована история на наличие секретов (git log -p или truffleHog)
☐ Если найдены — ключи отозваны у провайдера
CI/CD:
☐ Секреты добавлены в GitHub Secrets (не в файлы workflow)
☐ В .yml файлах нет реальных значений — только ${{ secrets.NAME }}
Продакшен-сервер:
☐ Переменные заданы через интерфейс хостинга или файл вне репозитория
☐ Файл с секретами имеет права 600 (только владелец)
☐ Для dev и production используются разные ключи
Общее:
☐ Никто кроме нужных людей не имеет доступа к production-ключам
☐ Есть план действий при компрометации: где отозвать, где сменить
Итог
Безопасное хранение секретов — это не про параноию, а про привычку. Один раз настроить .env и .gitignore, добавить GitHub Secrets для CI/CD — и большинство проблем исчезает.
Главные правила, которые стоит запомнить:
Секреты живут в переменных окружения, не в коде. .env есть в .gitignore всегда и в каждом проекте. .env.example с пустыми значениями коммитится — реальный .env никогда. Если ключ попал в репозиторий — сначала отозвать, потом разбираться с историей.
Если хотите проверить себя прямо сейчас — откройте любой свой проект и поищите в коде строки api_key =, secret =, password =. Если нашли реальные значения — пора исправить.
FAQ
Можно ли хранить .env в приватном репозитории? Технически можно, но это плохая практика. Приватный репозиторий может стать публичным по ошибке. Кроме того, все участники репозитория получат доступ к production-ключам, что нежелательно.
Как передать .env новому разработчику в команде? Через защищённый канал — зашифрованное сообщение в мессенджере, 1Password, Bitwarden для команд. Не через email и не через Telegram в открытом виде. В долгосрочной перспективе — менеджер секретов типа Doppler или Infisical.
Нужно ли скрывать ключи во фронтенде (React, Vue)? Да, и это отдельная сложная тема. Любой ключ, который попадает в JavaScript-бандл, виден пользователю через DevTools. Для фронтенда используйте только ключи с ограниченными правами (только чтение, привязанные к домену). Ключи с широкими правами должны быть только на бэкенде.
Что делать если не уверен, утёк ключ или нет? Действовать как будто утёк — отозвать и сгенерировать новый. Это занимает 5 минут. Проверять и сомневаться — проигрышная стратегия.
Переменная окружения задана, но приложение её не видит — почему?
Скорее всего, dotenv загружается после кода, который использует переменную. Убедитесь, что require('dotenv').config() или load_dotenv() — первые строки в точке входа приложения, до любых других импортов.
Июнь 2026.