Безопасное управление секретами в скриптах и приложениях
Фарш обратно не провернёшь
Порой в Bash поделках требуется создать какой-нибудь секрет, для дальнейшей его передачи в CI/CD или куда-то еще. Ежу понятно, это можно сделать «в лоб», но мыж с тобой не волки позорные, поэтому давай сделаем это по всем правилам DevSecOps Best Practice. (* лучшие практики безопасной разработки).
ЧИТАТЬ ПЕРВЫМ В ТЕЛЕГРАМ ЧИТАТЬ ПЕРВЫМ В MAX
Задача: Создать безопасно секрет, чтобы его не спиздили (например из свапа при форензике).
Форензика — это направление информационной безопасности, связанное с анализом и восстановлением данных для расследования инцидентов
Кто-то извращается со shred и т.п. утилитами, но это избыточно, да и на SSD, Btrfs, ZFS и journal FS оно будет работать хуева и не гарантирует физическое уничтожение данных. Лучше на диск вообще ничего не писать.
Наш вариант это — pipe через printf. Можно конечно усложнить и сделать через анонимный файловый дескриптор, но получаются те же грабли только сбоку, смысла усложнять нет.
#!/usr/bin/env bash
set -euo pipefail
SECRET="$(openssl rand -base64 32)"
printf '%s' "$SECRET" | \
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000 \
-in input.txt \
-out output.enc \
-pass stdin
unset SECRET
Что тут происходит?
Ничего необычного, генерим 32 случайных байта, кодируем в base64, получаем пароль высокой энтропии, затем через printf передаем пароль (без \n) через pipe в stdin.
Ну и в конце скармливаем какой-нибудь утилите или в CI/CD, куда нужно передать секрет, в моем случае я передал его в openssl.
По итогу секрет, не попадает в history, не лежит на диске и временно находится в памяти. Но важно понимать, что после unset SECRET переменная удаляется только из таблицы переменных, в памяти эта переменная может по-прежнему храниться и быть уязвима к форензике. Поэтому носи это в голове и по возможности перезатирай память например тем же stress.
А еще оно может попасть в swap и это еще хуже, swap это прям как неочищенная «корзина» с удаленными файлами.
На bash этот момент описать наверное не получится, поэтому покажу как сделать на Сиськах. Будем использовать mlock().
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
int main() {
char secret[32] = "super_secret_password";
if (mlock(secret, sizeof(secret)) != 0) {
perror("mlock failed");
return 1;
}
printf("Secret in locked memory\n");
// Используем секрет...
memset(secret, 0, sizeof(secret)); // затираем
munlock(secret, sizeof(secret));
return 0;
}
Когда этот код будет вызывать mlock(), указанный диапазон памяти закрепляется в RAM и ядро не имеет права выгружать его в swap.
Получается что сначала mlock() блокирует участок памяти, затирает memset() её перед освобождением и с помощью munlock() снимает блокировку.
Кстати mlock() используют GnuPG, OpenSSH, HashiVault, KeePassXX. Так что вариант надежный, можешь не сомневаться. А еще иногда используется mlockall():
mlockall(MCL_CURRENT | MCL_FUTURE);
Блокируется вся текущая и будущая память процесса, активно применяется демонами в HashiVault.
Такие дела.
Накидай еще своих вариантов в комменты, будет интересно ознакомиться.