Почему мы вообще решили делать своё хранилище
До Vault пароли и доступы у команды жили где придётся: в заметках на телефоне, в текстовых файлах, в переписках. Постоянно происходило одно и то же — кто-то не мог вспомнить, куда записал доступ от нужного сервиса, искал его по чатам и заметкам, а то и вовсе просил заново прислать пароль у коллеги. Мы решили, что это надо один раз собрать в одном месте и закрыть вопрос — так появился Vault.
Готовые менеджеры паролей нам не подошли: либо облако и доверие чужим серверам, либо self-hosted решения с интерфейсом и функциями не под наш процесс. Нужен был свой инструмент — со своими пользователями, ролями и возможностью безопасно шарить доступы внутри команды.
Как выбирали шифрование
Требование звучало просто, а решалось сложно: даже если базу данных украдут целиком, расшифровать хранящиеся в ней пароли должно быть невозможно.
Мы остановились на AES-256-GCM — он даёт не только конфиденциальность, но и проверку целостности (authentication tag), то есть подмену зашифрованных данных легко обнаружить. Каждая запись шифруется со своим случайным IV, поэтому одинаковые пароли не превращаются в одинаковые шифротексты.
Ключ шифрования нигде не хранится в явном виде — он выводится на лету через PBKDF2-SHA512 с 310 000 итераций из комбинации сразу трёх независимых секретов:
Идея в том, что кражи одного или даже двух из трёх секретов недостаточно для офлайн-подбора пароля. Без ключевого файла даже украденная база и серверные настройки бесполезны для атакующего.
С какими сложностями столкнулись
Смена схемы деривации ключа на лету. Когда мы усилили схему (добавили третий секрет — ключевой файл), встал вопрос: что делать с уже зашифрованными данными первых пользователей? Полная переиндексация базы "на живую" рискованна. Решили сделать миграцию: старая схема деривации осталась в коде только как одноразовый путь для пересчёта, а новые записи используют исключительно усиленную схему.
Где хранить ключевой файл. Тут мы долго спорили. Сначала ключ чуть было не оказался в том же месте, что и само приложение, — поймали на ревью и одумались: если ключ лежит там же, где сервер и база, кража одного места отдаёт атакующему всё сразу. В итоге решили хранить ***.key отдельно от сервера — так, чтобы доступ к приложению и базе сам по себе ещё не давал возможности расшифровать данные.
Шаринг записей внутри команды. Личные пароли шифруются ключом, привязанным к конкретному пользователю, — сервер не может прочитать их напрямую. Но как тогда поделиться записью с коллегой, если ни у кого из вас нет ключа друг друга? Для расшаренных копий мы сделали отдельный механизм, в котором сервер способен их расшифровать. Это осознанный компромисс: общая часть паролей защищена немного слабее личной, зато шаринг работает просто и надёжно — и мы сознательно выбрали такой баланс, а не спрятали это ограничение.
Что дальше
Сейчас в Vault уже есть двухфакторная аутентификация, генератор паролей и оценка их надёжности. Дальше в планах — аудит-лог действий и более гибкие права доступа для команд.
Спасибо, что читаете. Если найдёте дыру — пишите, разберёмся.