Загрузка данных


Почему нельзя удалить приватный ключ из памяти Go-приложения
В Go-приложении нельзя гарантированно удалить приватный ключ из памяти процесса после использования. Экспериментальный пакет runtime/secret из Go 1.26 эту задачу тоже не решает. Практическое решение — короткоживущий ключ: его остаток в памяти быстро перестаёт что-либо стоить.
1. Почему удаление невозможно
Ключ неизбежно копируется туда, куда у кода приложения нет доступа. Затереть можно только те копии, которыми код владеет сам, а копии в runtime и стандартной библиотеке остаются до тех пор, пока память не займут другие данные.
• Сборщик мусора не обнуляет память. Освобождённый объект только помечается как свободный, а байты ключа остаются на месте. runtime.GC() и debug.FreeOSMemory() этого не меняют.
• Строки неизменяемы. Если ключ или токен хотя бы раз стал string, затереть его нельзя. Официальный клиент Vault возвращает данные строками, заголовки net/http тоже строки.
• Runtime копирует данные сам. При росте стека горутины он переносится на новое место, а старая копия не очищается. Стеки завершённых горутин уходят в пул без очистки, поэтому отдельная горутина для работы с ключом не помогает.
• Стандартная библиотека делает собственные копии ключа и хранит их в неэкспортируемых полях:
    ◦ crypto/rsa держит внутреннюю копию D, p и q;
    ◦ bigmod и nistec создают временные копии при подписи, asn1 и x509 — при разборе;
    ◦ буферы crypto/tls и net/http хранят расшифрованный ответ Vault с PEM-ключом, а пулы HTTP/2 переиспользуют такие буферы без обнуления.
• Обойти это библиотекой нельзя. Нужен доступ к аллокатору и сборщику мусора, которого у стороннего кода нет. Библиотеки вроде memguard защищают только свой буфер, но как только ключ передан в crypto/tls, он снова копируется в обычную кучу.
Достаточно одной оставшейся копии простого числа p или q, чтобы восстановить весь RSA-ключ.
2. Go 1.26 и runtime/secret — тоже не решение
В Go 1.26 появилась функция secret.Do(f). Она затирает регистры и стек после вызова f, а память в куче, выделенную внутри f, — когда её освободит сборщик мусора. Гарантии удаления ключа она не даёт:
• Это эксперимент. Пакет собирается только с GOEXPERIMENT=runtimesecret и не покрыт обещанием совместимости Go 1. В версии-кандидате Go 1.27 он по-прежнему экспериментальный, и его поведение уже меняется между версиями.
• Не покрывает HTTP-клиент. В Go 1.26 горутины, запущенные внутри f, не защищены. А net/http выполняет TLS handshake с нашим ключом и читает ответ Vault именно в своих горутинах.
• Не защищает долгоживущие данные. Глобальные переменные, пулы соединений и буферов остаются незатёртыми.
• Не даёт момента удаления. Куча затирается только тогда, когда сработает сборщик мусора, и это время программа не контролирует.
К этому вопросу имеет смысл вернуться, когда пакет перестанет быть экспериментальным.
3. Решение: короткоживущий ключ
Раз ключ нельзя удалить из памяти, нужно сделать так, чтобы его остаток быстро становился бесполезным. Vault PKI (pki/issue) выпускает клиентский сертификат и ключ на каждый запуск с TTL в минутах. После истечения TTL etcd не примет такой ключ, даже если он утёк из памяти, дампа или swap.
Для этого нужна роль PKI в Vault с коротким TTL. Решение работает на текущей версии Go и не зависит от её внутреннего устройства.