Берегите очко смолоду
Часть 1 — про секреты: ключи, токены, пароли и места, где они тихо вытекают.
Почему это важно для бизнеса#
Секреты — это не только пароли. Это API-ключи, токены ботов, доступы к платёжкам, приватные ключи, строки подключения к базе. Утечка одного такого значения иногда даёт больше доступа, чем взлом админки.
Для команды главный вопрос не «есть ли у нас .env». Важно понимать полный путь секрета: где он создаётся, кто его видит, как он попадает на сервер, как ротируется и что делать, если он уже утёк.
Когда говоришь «утечка секретов», большинство представляет хакера, который что-то взломал и достал ключ из глубин сервера. На практике почти всё прозаичнее. Секрет чаще всего не «крадут» — его сами кладут туда, где его видно. Ниже — набор историй, каждая из которых встречалась мне в том или ином виде, и урок к каждой.
Браузер — это не сейф#
Самая частая и самая обидная ошибка. Есть переменная окружения с ключом доступа к бэкенду. Чтобы «фронт мог ходить в API», её добавляют в публичный слот сборки — что-то в духе префикса NEXT_PUBLIC_*. Дальше сборщик честно запекает значение прямо в клиентский бандл. Открываешь DevTools, вкладку с исходниками — и вот он, ключ, открытым текстом.
// bad: значение уезжает в браузер на этапе сборки
const apiKey = process.env.NEXT_PUBLIC_API_KEY;
// good: секрет живёт только на сервере, клиент ходит через свой бэкенд
// (server route / API handler), который сам подставляет ключ
const apiKey = process.env.API_KEY; // доступно только в серверном коде
Всё, что доезжает до браузера, перестаёт быть секретом в ту же секунду. Аутентификация и авторизация живут только на сервере — точка.
Широкий токен в роли прикладного ключа#
Однажды в тот же «публичный» слот заехал не просто ключ, а мощный персональный токен — из тех, что дают доступ сразу ко всему (представьте личный PAT с правами на репозитории, пакеты и настройки). Его использовали как обычный прикладной ключ «чтобы быстро заработало».
Проблема не в том, что токен утёк — это случается. Проблема в blast radius: один засвеченный персональный токен открывает не одну функцию, а всё, до чего дотягиваются права его владельца.
Урок двойной. Не переиспользуйте широкие персональные токены как прикладные ключи — заводите узкие, с минимальными правами и понятным сроком. А если что-то засветилось — не «понаблюдаем», а немедленно ротируем. Скомпрометированный секрет не лечится молчанием.
Один секрет на всех и секрет, который меняется сам#
Пара противоположных, но одинаково опасных паттернов вокруг JWT.
Первый — захардкоженный fallback прямо в коде:
// bad
const JWT_SECRET = process.env.JWT_SECRET || "supersecret-fallback";
И этим же секретом подписываются и пользовательские, и админские токены. Достаточно один раз увидеть строку — и админский токен подделывается руками, а за ним уезжает вся чувствительная выборка.
Второй паттерн — вроде бы «безопаснее», но ломает всё иначе: если env пуст, секрет генерируется случайно на каждый старт процесса. Звучит «секьюрно», на деле — сессии рассыпаются между инстансами и после каждого рестарта, токены перестают валидироваться, пользователей выкидывает.
// good: fail-fast + раздельные секреты под роли
const userSecret = required("JWT_USER_SECRET");
const adminSecret = required("JWT_ADMIN_SECRET");
function required(name: string): string {
const v = process.env[name];
if (!v || v.length < 32) throw new Error(`Missing/weak ${name}`);
return v;
}
Нет секрета или он слишком короткий — процесс просто не стартует. Разные роли — разные секреты. Никаких «временных заглушек на проде».
Plaintext там, где должно быть зашифровано#
Отдельная категория проблем — чувствительные данные, лежащие открытым текстом: коды подтверждения (OTP), пароли, токены платёжных карт. Открыл таблицу — увидел всё.
Здесь два разных подхода под два разных случая. OTP не нужно «расшифровывать обратно», поэтому его нужно не шифровать, а превращать в проверяемый отпечаток — HMAC с серверным pepper, плюс жёсткий лимит попыток. А то, что действительно нужно потом прочитать (например, чувствительные токены), шифруется at-rest симметрично — AES-256-GCM ключом, который берётся из окружения, а не лежит рядом в коде.
// OTP: храним отпечаток, а не сам код
const otpHash = hmacSha256(pepper, `${userId}:${code}`);
// чувствительные данные: шифруем, ключ — из env
const enc = aes256gcm(process.env.DATA_ENC_KEY!, plaintext);
Секреты в логах#
Секреты особенно часто утекают через debug-логи. В них радостно складывают заголовок Authorization, тела ответов с токенами, «полезный» дамп запроса. Дальше это уезжает в агрегатор логов, к которому доступ часто шире, чем к самой базе.
Отговорка «ну в проде же debug никто не включит» — не защита, а ставка на удачу. Включат: для разбора инцидента, по ошибке, на стейдже, который смотрит наружу. Redaction должен происходить в точке логирования, чтобы секрет физически не попадал в строку.
log.info("incoming", { headers: redact(headers, ["authorization", "cookie"]) });
Права на файл и Docker#
Две инфраструктурные мелочи, которые стоят дорого.
Первая — права на .env. Файл пишется как 0644, то есть world-readable: любой процесс или пользователь на хосте его прочитает. Нужен 0600 и атомарная запись — во временный файл, потом rename, с явным режимом доступа.
Вторая — секреты в Docker. Их прокидывают через ARG на этапе сборки, и значение оседает в слоях образа: достаёшь образ, смотришь историю слоёв — вот они. Плюс .dockerignore часто не исключает .env*, и файл уезжает в контекст сборки целиком.
# bad: ARG с секретом запекается в слой
ARG API_TOKEN
RUN ./build.sh
# good: секрет только в рантайме, через переменную окружения контейнера
# а .dockerignore исключает .env*, .git, node_modules
ARG — это не секрет, а параметр сборки. Секреты подаются только в рантайме и чистятся из контекста.
Эпоха ассистентов#
И свежий пласт проблем. Живые секреты теперь массово оказываются в неожиданных местах: их вставляют прямо в чат с ИИ-ассистентом «чтобы он помог разобраться», в логи CI-сборки, в документацию, в тикеты. Сам по себе ассистент тут ни при чём — проблема в том, что значение покинуло доверенный контур.
Правило простое и не подлежит обсуждению: всё, что вы куда-то вставили глазами наружу, считается скомпрометированным. Перед вставкой — вычищайте реальные значения, после факта — ротируйте. Дешевле сменить ключ, чем потом гадать, кто его видел.
В следующей части — про чужой ввод: почему нельзя верить ни телу запроса, ни заголовку, ни даже вебхуку, который «точно от провайдера».
Если вы не уверены, где в проекте живут ключи, токены и доступы, это уже повод пройтись по ним отдельно. Напишите мне в Telegram или на почту — помогу найти лишние секреты в коде, CI, логах и настройках, а потом выстроить хранение и ротацию без героических ручных действий.



