Безопасность
Что система гарантирует, чего не гарантирует и почему.
Где проходит граница
В покое — только шифртекст. Значения зашифрованы XChaCha20-Poly1305 на ключ данных окружения, сам ключ лежит зашифрованным под корневым ключом, который живёт в окружении оператора или во внешнем KMS. Украденный дамп MongoDB без корневого ключа бесполезен.
В работе — зависит от пути:
- машинный путь честно клиентский: сервер отдаёт SDK шифртекст и ключ данных, обёрнутый x25519 под ключ конкретной identity. Открытого значения сервер в этом сценарии не производит вовсе;
- запись и показ значения человеку проходят через сервер открытым текстом по TLS.
Поэтому мы не называем систему zero-knowledge. Живой процесс имеет доступ к значениям в момент обработки, и обещать обратное было бы неправдой — именно на такой попытке в своё время споткнулся Infisical, когда дашборду и интеграциям понадобился plaintext.
Аутентификация identity
Ничего вечного: у client secret есть отзыв, у access-токена — TTL в минутах, у enrollment-токена — десять минут и ровно одно использование.
Повторный обмен enrollment-токена означает, что он утёк. Система не просто
отказывает: захват токена атомарный (compare-and-set), проигравшая попытка
поднимает событие enrollment.reuse-detected и вебхук администраторам.
Разбор механизма и действий по тревоге — на странице
Сервисы и identity.
Аудит
Два потока, видимых клиенту: изменения (activity) и доступы (access).
Каждое чтение секрета — человеком или identity при старте — записано.
События связаны цепочкой на уровне организации, поэтому незаметно удалить
или переписать запись нельзя.
Звено считается под ключом, выведенным из ключа аудита, а тот живёт в окружении сервиса, а не в базе. Разница принципиальная: цепочка из обычных хешей проверяется кем угодно — значит, кем угодно и пересчитывается. Тот, кто получил доступ на запись в базу, удалил бы события, пересобрал цепочку от разрыва и переписал её голову, которая лежит там же. С ключом это невозможно: пересчитать цепочку, не имея его, нельзя.
Проверка — команда audit:verify. Она идёт от первого события по звеньям
и сверяет каждое, попутно замечая то, чего сверка хешей по отдельности не
увидит: развилку, обрыв хвоста и события, до которых от начала цепочки не
добраться. Свойство, которое нельзя проверить, свойством не является.
Голова цепочки тоже подписана — переписать её, удалив последние события,
без ключа нельзя. Остаётся один случай, который изнутри базы не отличить:
откат к состоянию, которое когда-то было подлинным, из старой резервной
копии. Против него сервер раз в час пишет якорь — снимок головы — в
свой лог, а он уезжает с хоста. Передайте накопленные якоря проверке
(audit:verify --anchors <файл>), и она убедится, что каждое
зафиксированное звено осталось на своём месте — и что журнал не стёрт
целиком. Без якорей проверка об откате не знает и говорит об этом прямо.
Зернистость равна такту: откат к состоянию новее последнего якоря не виден по построению, окно — час. Сам якорь печати головы не содержит, иначе строка из лога стала бы готовым разрешением усечь цепочку до этой длины.
Якоря, накопленные до версии, в которой у них появилась собственная подпись, недействительны и отмечаются как неподписанные. Это не сбой: принимать старую форму значило бы оставить открытой ровно ту дверь, ради которой подпись и вводилась. Полноценное покрытие начинается со следующего такта якорения.
Значений в журнале нет: только имена ключей и HMAC значения. Этого хватает, чтобы увидеть «значение изменилось», и не хватает, чтобы узнать какое.
Ограничения по частоте
Вход и обмен токенов ограничены — по IP и по субъекту; при отказе сервер
отвечает 429 RATE_LIMITED с заголовком Retry-After:
| Точка | Лимит |
|---|---|
| вход пользователя | 10 попыток за 15 минут |
вход identity (/machine/login) | 30 за 15 минут |
обмен enrollment-токена (/machine/enroll) | 5 за 15 минут |
| регистрация и письма подтверждения | 3 в час |
| обновление токена | 60 за 15 минут |
Периметр
- адрес вебхука проверяется дважды: при сохранении и перед каждой доставкой, внутренние и loopback-адреса отклоняются, иначе сервер стал бы сканером собственной сети;
- размер запроса и значения ограничены и в приложении, и на обратном прокси;
- refresh-токен браузера живёт в httpOnly-cookie, access-токен — только в памяти вкладки: XSS уносит 15 минут, а не 30 дней;
- API отвечает с
Content-Security-Policy: default-src 'none',X-Content-Type-Options: nosniff, запретом фреймов иReferrer-Policy: no-referrer; HSTS — забота обратного прокси, который терминирует TLS.
Ротация корневого ключа
Корневой ключ тоже не вечен:
OLD_ROOT_KEY=... NEW_ROOT_KEY=... bun run db:rekey -- --dry-run
Перешифровываются только ключи окружений и секреты подписи вебхуков — значения не трогаются, поэтому операция стоит порядка числа окружений, а не числа секретов. Прогон возобновляем и останавливается, если хоть один элемент не открывается ни одним из ключей.