Один мудрец как-то сказал: «Мы сами порождаем своих демонов». Неважно, кто это сказал, но важно, как это может быть интерпретировано в том или ином контексте. Любое действие рождает противодействие — именно об этом хочется поговорить в рамках финальной статьи. На данном этапе уже не хочется рассказывать про то, как инфраструктура масштабировалась в дальнейшем, но хочется поделиться кейсом повышения безопасности одного из самых важных компонентов инфраструктуры — базы данных. В связи с этим проведём одну большую линию и расскажем всё как есть. Надеюсь, статья поможет каждому избежать ошибок в планировании и узнать что-то новое.
Последний рывок?
Приглашаю вас подписаться на наш блог Хабр, TG-канал DevOps FM и познакомиться с YouTube — там вы найдёте много полезного :)
Глава 1: Конец — это тоже часть пути
Рано или поздно в жизни каждого клиента возникает момент, когда ему необходимо внедрение процессов безопасности в самую ключевую часть его инфраструктуры — в базу данных. Подобный случай не обошёл стороной и наш проект.
Одним прекрасным солнечным утром была поставлена задача в jira, в рамках которой звучали страшные слова:
Повышение безопасности базы данных и оформление её инфраструктуры согласно основным требованиям безопасности.
Первое, о чём можно было подумать, что почти любой компонент инфраструктуры, выступающий в роли backend, взаимодействует с БД напрямую, и при малейшем вмешательстве в механизмы работы БД может возникнуть каскад проблем. Они будут связаны как с инфраструктурными инцидентам (например, рост очередей брокеров или ошибки аутентификации пользователей), так и с финансовыми и репутационными потерями самого клиента (например, потеря легитимного трафика пользователей в момент downtime или рост обращений пользователей в Техническую Поддержку).
— Что нам помогает этого избегать?
— Ответ прост — это огромная тестовая среда, полностью дублирующая продуктовое окружение.
Да, сейчас не бывает проектов без тестовых окружений, но время, потраченное на переключение всех эпиков Developers, QA и DevOps-инженеров, будет трудозатратным и, как следствие, приведёт к тому, что новый функционал будет выпускаться медленнее.
Если говорить по порядку, то перед нами стоял ряд задач и шагов по их выполнению. В роли базы данных нашего проекта была PostgreSQL версии 14, так что минимально необходимые требования можно было охарактеризовать так:
Разворачивание новой тестовой БД в виде кластера;
Внедрение библиотек безопасности в работу БД;
Контроль прав доступа к файлам БД;
Внедрение полного механизма логирования процессов взаимодействия с БД;
Настройка процессов шифрования.
Список выглядит немаленьким, так что начнём обсуждение по порядку и остановимся на каждом пункте чуть подробнее.
Процесс разворачивания новой тестовой БД в виде кластера не представляет для нас сложностей, так как для этого нам нужна лишь актуальная ОС и поставленные на ней ранее ПО безопасности и мониторинга событий.
Именно поэтому переходим к перечню дополнительных библиотек PostgreSQL для повышения безопасности, которые должны быть включены в shared_preload_libraries.
passwordcheck – модуль для проверки надёжности пароля, задаваемого пользователям при использовании ALTER ROLE и CREATE USER;
auto_explain – этот модуль не несёт в себе всецельный объём повышения безопасности ваших данных, но он позволяет автоматизировать процесс протоколирования неоптимизированных или долгих запросов. Данный механизм позволяет значительно уменьшить время на проведение анализа.
pg_stat_statements – сбор статистики всех выполненных операторов SQL.
auth_delay – модуль добавляет время задержки перед ответом на любой запрос авторизации, что в процессе усложняет процесс брутфорса, даже если злоумышленник проникнет в закрытую систему.
pgaudit – модуль расширяет возможности логирования PostgreSQL и логировать определённые операторы, описанные в параметре настройки pgaudit.log. В нашем случае необходимыми являются READ (операции SELECT и COPY), DDL (все операторы Data Definition Language, которые не входят в ROLE-класс —, к таким командам относятся CREATE, ALTER, TRUNCATE, DROP) и ROLE (операторы, относящиеся к изменению или выдаче привилегий, к примеру: CREATE ROLE, ALTER ROLE, DROP ROLE GRANT, REVOKE).
Контроль прав доступа до файлов базы данных при установке PostgreSQL происходит самостоятельно. Права на каталоги 700, а права на файлы 600, но при этом маска режима создания пользовательских файлов у пользователя PostgreSQL не соответствует эталону безопасности.
postgres@ubuntu:~$ umask
0002
А это означает, что все файлы, создаваемые из-под пользователя postgres будут иметь недопустимые привилегии для каталогов 775 и для файлов 664. Наш желаемый результат должен соответствовать исходным правам 700 и 600, а значит нам необходимо задать настройки профиля bash для пользователя, создав в его каталоге нужный файл с правами суперпользователя:
root@ubuntu:~$ echo "umask 077" >> /var/lib/postgresql/.bash_profile; chown root: /var/lib/postgresql/.bash_profile
После добавления соответствующих библиотек необходимо пройтись по ряду параметров, отвечающих за логирования. Ключевыми параметрами, влияющими на логи в рамках задачи, являются:
log_filename – формат файла, содержащего логи, с учётом timestamp (пример значения "postgresql-%Y-%m-%d_%H%M%S.log")
log_rotation_age – время жизни файлов логирования (пример значения "7d")
log_rotation_size – максимальный размер файла логирования (пример значения "0")
pgaudit.log – ранее описанный параметр (пример значения "ddl,role,read")
log_directory – путь до директории хранения логов. Здесь важно заметить, что, согласно требованиям безопасности и оптимизации, это должен быть отдельный каталог со смонтированным диском, предназначенным только для файлов postgresql-%Y-%m-%d_%H%M%S.log. Такой подход необходим потому, что если в момент наплыва трафика уровень логирования событий повысится, то исчерпания свободного пространства будет сложно предсказать. Подобная ситуация может привести к остановке работы базы данных.
log_connections – включение регистрации попыток аутентификации к серверу (по умолчанию данный параметр отключен, но его можно включить "on")
Отлично, самое простое уже реализовано. Но тут появляется задача шифрования. Возникает ряд вопросов относительно того, как этот процесс будет влиять на работу микросервисов с БД.
Глава 2: Безопасность ради «безопасности»
Убедившись, что мы уберегли себя от случайных проникновений и повысили скорость анализа потенциальных инцидентов, необходимо переходить ко второй стадии — шифрованию дисков. В этом случае есть несколько способов реализации.
В первую очередь процесс может быть реализован шифрованием поверх файловой системы; во вторую очередь он может быть осуществлен в процессе шифрования блочного устройства. Рассмотрим оба варианта и решим, какой же из механизмов подходит нам лучше всего.
Названия процессов шифрования говорят за себя. Шифрование файловой системы — это метод кодирования отдельных файлов или каталогов системы с последующей записью их на диск. Блочное же шифрование работает на уровень ниже и с помощью ключа шифрует сразу весь диск и все располагаемые на нём файлы. Каждый из вариантов имеет свои плюсы.
Преимущества шифрования файловой системы:
Выборочное шифрование файлов или каталогов;
Возможность контроля объёма зашифрованной информации;
Быстрая скорость шифрования (потому что файлы выборочные);
Высокий уровень безопасности, поскольку этот процесс обеспечивает детальный контроль над шифрованием каждого файла по отдельности с помощью отдельного ключа, что снижает факторы риска в случае взлома диска;
Легкая доступность к другим файлам, не требующим шифрования в системе;
Простота восстановления файлов в случае частичного сбоя дисков.
Преимущества шифрования блочного диска:
Отсутствие необходимости выборочного контроля файлов. Шифруется и дешифруется сразу весь диск;
Любые файлы и папки, созданные пользователем на диске, автоматически шифруются, что позволяет избежать возможных ошибок пользователя;
Данный тип шифрует метаданные файловой системы, такие как структура каталогов, имена файлов, временные метки или размер.
Так как речь у нас идёт про PostgreSQL с управлением через patroni, то вероятнее всего второй вариант будет самым правильным в реализации. Как минимум практика использования отдельно монтированного диска для хранения на него системных файлов БД не является чем-то новым или странным. Перейдём к настройке:
Если мы говорим про исходную нашу ОС в виде Ubuntu, то нам необходимо осуществить установку утилиты шифрования:
apt update; apt install cryptsetup
Производим переключения мастера в случае, если PostgreSQL уже предустановлен и мы работаем на живой инфраструктуре.
patronictl -c /etc/patroni.yml list
patronictl -c /etc/patroni.yml switchover $REPLICA_NAME
Останавливаем patroni на исходной ноде и отмонтируем диск, который смонтирован в pgsql:
systemctl stop patroni
umount /var/lib/pgsql
Создадим зашифрованный раздел luks, после чего добавим ключ шифрования в файл, лежащий на основном диске файловой системы:
cryptsetup luksFormat /dev/$DISK_NAME /root/cfs.key
cryptsetup luksAddKey /dev/$DISK_NAME /root/cfs.key --key-file=/root/cfs.key
Чтобы открыть наш новый раздел нужно применить luksOpen с указанием ключа шифрования и после создать в рамках него файловую систему:
cryptsetup luksOpen /dev/vdb pgsql-data --key-file=/root/cfs.key
mkfs.xfs /dev/mapper/pgsql-data
Последний шаг — добавление нашего нового раздела в файл, монтирование системы и установка корректных прав доступа к каталогу. В дальнейшем, когда сервер будет запущен, patroni сделает инициализацию процесса синхронизации с данными с Master-сервера.
export UUID=$(ls -l /dev/disk/by-uuid | grep "vdb" | awk '{print $9}')
echo "pgsql-data UUID=${UUID} /root/cfs.key luks" >> /etc/crypttab
export UUID=$(ls -l /dev/disk/by-uuid | grep "dm-0" | awk '{print $9}')
echo "UUID=${UUID} /var/lib/pgsql xfs defaults 0 0" >> /etc/fstab
mount -a
chown postgres:postgres /var/lib/pgsql
chmod 700 /var/lib/pgsql
systemctl start patroni
Настройка шифрования прекрасно работает, но в данным момент стоит задуматься: от чего мы защитились? Единственный вариант, когда нам может пригодиться подобное шифрование, это в случае, если кто-то проникнет в офис к серверной стойке и вытащит диск, ведь мы имеем полностью закрытую инфраструктуру с ограниченным доступом к серверам с БД.
Отсюда следует, что наш вариант будет прекрасным решением, но остаются другие потенциальные дыры. К примеру, трафик, поступающий к БД со стороны микросервисов, всё ещё остаётся в не зашифрованном состоянии. Как тогда быть?
Глава 3: Что-то между
Рассмотрим два случая. Первый случай — это потенциальная возможность возникновения MITM (Man-in-the-middle) в рамках нашей инфраструктуры.
Смысл атаки «Человек посередине» заключается в том, что злоумышленник «пропускает» веб-трафик жертвы (возможно, путём изменения параметров DNS-сервера или файла hosts на машине жертвы) «через себя».
Разговоры об этой довольно распространённой атаке не являются новыми, но сами разработчики продукта PostgreSQL давно подготовились к подобным ситуациям и добавили проверку подлинности SSL сертификата между клиентом и сервером.
apt install openssl -y
После чего необходимо сгенерировать запрос CSR (Certificate Signing Request):
cd /root
openssl req -new -nodes -text -out root.csr \
-keyout root.key -subj "/CN=root.yourdomain.com"
chmod og-rwx root.key
И выпустить сам корневой сертификат, подписанный ключом по данному запросу:
openssl x509 -req -in root.csr -text -days 3650 \
-extfile /etc/ssl/openssl.cnf -extensions v3_ca \
-signkey root.key -out root.crt
В финале мы уже имеем возможность выпустить доверенный сертификат, предварительно выпустив запрос на серверный сертификат и ключ:
openssl req -new -nodes -text -out server.csr \
-keyout server.key -subj "/CN=dbhost.yourdomain.com"
chmod og-rwx server.key
Подписываем его на 365 дней нашим корневым центром сертификации:
openssl x509 -req -in server.csr -text -days 60 \
-CA root.crt -CAkey root.key -CAcreateserial \
-out server.crt
И генерируем клиентский сертификат для приложения по остаточному принципу.
openssl req -new -nodes -text -out client.csr \
-keyout client.key -subj "/CN=client.yourdomain.com"
chmod og-rwx client.key
openssl x509 -req -in client.csr -text -days 60 \
-CA root.crt -CAkey root.key -CAcreateserial \
-out client.crt
Что же у нас получается?
Мы сгенерировали сертификаты для базы данных, но она по прежнему не умеет работать с ssl, чтобы решить эту проблему нам понадобится ряд параметров и настроек безопасности.
В настройках основной конфигурации включаем режим работы шифрования трафика у входящих коннектов:
ssl – включаем возможность работы с ssl подключениями (“true”)
ssl_ca_file – путь до файла содержащего сертификат Центра Сертификации, для подтверждения сертификата клиента (например “/root/root.crt”)
ssl_cert_file – путь до файла содержащего ssl сертификат сервера (например “/root/server.crt”)
ssl_key_file – путь до файла содержащего ключ к выпущенному сертификату сервер (например “/root/server.key”)
И запрещаем через конфигурационный файл аутентификации клиентов любые не содержащие ssl-протоколирование подключения со стороны наших внутренних хостов:
hostssl replication all 10.100.10.2/18 scram-sha-256
Хорошо, но как быть со стороны приложения?
Здесь всё так же просто, важно лишь передать микросервису его клиентский сертификат (client.crt), ключ к сертификату (client.key) и корневой сертификат для подтверждения подлинности сертификата со стороны сервера. В дальнейшем при осуществлении коннекта к базе данных со стороны любого приложения, имеющего озвученный выше перечень и выставленный параметр sslmode=verify-full, будет происходить проверка SSL сертификатов с двух сторон, что исключает любое вмешательство в трафик любых потенциальных злоумышленников.
Глава 4: Что-то где-то
Когда администратор наладил все основные механизмы, в его руки поступает последняя задача — вишенка на торте. Бэкапы БД: как с ними быть? В случае, если мы бэкапируем файлы, которые располагаются в пределах нашей инфраструктуры, на какой-то резервный диск, то то, как перешифровать этот диск, мы уже знаем. Но есть один значительный минус: бизнес любит страховаться, а это значит, что количество резервных копий вашей БД даже в сжатом состоянии по размеру может значительно превысить основной объём базы. Вдобавок клиент может захотеть использовать сторонние S3 как хранилище копий, и тогда на помощь приходит механизм walg-backup, способный эффективно, оптимизировано и безопасно осуществлять бэкапирование БД в облако. Здесь мы так же пошли по этому пути.
Для работы walg-backup необходима конфигурация .walg.json и ряд команд, которые в последующем могут быть обернуты в простейший bash script, но начнём по порядку.
.walg.json должен содержать следующие параметры настройки:
WALE_S3_PREFIX – префикс имён архивов в хранилище;
AWS_ACCESS_KEY_ID – S3 access key;
AWS_ENDPOINT – S3 endpoint;
AWS_S3_FORCE_PATH_STYLE – включение адресации в стиле пути при подключении к S3 совместимой службе;
AWS_SECRET_ACCESS_KEY – S3 secret key;
PGDATA – путь до каталога данных БД;
PGHOST – параметр соединения с хостом;
WALG_UPLOAD_CONCURRENCY – количество параллельных потоков для загрузки с сети;
WALG_DOWNLOAD_CONCURRENCY – количество параллельных потоков для скачки;
WALG_UPLOAD_CONCURRENCY – количество параллельных потоков для загрузки;
WALG_UPLOAD_DISK_CONCURRENCY – количество параллельных потоков для загрузки на диск;
WALG_DELTA_MAX_STEPS – определяет, сколько разностных резервных копий может быть между полными резервными копиями;
WALG_COMPRESSION_METHOD – метод сжатия резервных копий;
WALG_LIBSODIUM_KEY – ключ шифрования архивированных копий размером 32 байта (его можно сгенерировать посредством выполнения команды openssl rand -hex 32);
WALG_LIBSODIUM_KEY_TRANSFORM – преобразование, которое будет применено к WALG_LIBSODIUM_KEY для получения требуемого 32-байтового ключа.
Пример
{
"WALE_S3_PREFIX": "s3://postgres-wal",
"AWS_ACCESS_KEY_ID": "default:s3-user",
"AWS_ENDPOINT": "https://storage.yandexcloud.net",
"AWS_S3_FORCE_PATH_STYLE": "true",
"AWS_SECRET_ACCESS_KEY": "
"PGDATA": "/var/lib/pgsql/14/data/",
"PGHOST": "/var/run/postgresql/.s.PGSQL.5432",
"WALG_UPLOAD_CONCURRENCY": "2",
"WALG_DOWNLOAD_CONCURRENCY": "2",
"WALG_UPLOAD_DISK_CONCURRENCY": "2",
"WALG_DELTA_MAX_STEPS": "7",
"WALG_COMPRESSION_METHOD": "brotli",
"WALG_LIBSODIUM_KEY": "" class="formula inline">{YOUR_LIBSODIUM_KEY}",
"WALG_LIBSODIUM_KEY_TRANSFORM": "hex"
}
После создания файла .walg.json, его дальнейшего размещения в /var/lib/pgsql и правильной настройки всех необходимых ключей аутентификации к S3, запуск внутренних команд управления wal-g запустит процесс сбора бэкапа, шифрования и дальнейшей отправки в S3. Но при этом существует важный момент: действия по сбору бэкапов должны производиться с ноды лидера PostgreSQL кластера для консистентности данных в собранных бэкапов:
su - postgres -c "/usr/local/bin/wal-g backup-push /var/lib/pgsql/14/data/ >> ~/walg_backup.log
Глава 5: Пройденный путь
Подходя к концу этой серии статей, я хочу озвучить итоги.
Работы по созданию инфраструктуры с нуля — очень большое и кропотливое делом для каждого инженера. Нельзя учесть все факторы риска и все сложности, которые могут возникнуть на этапах роста инфраструктуры в дальнейшем. Также надо понимать и быть готовым к задачам по изменению текущего функционала инфраструктуры. Запросы со стороны КБ могут поступать регулярно, и не зря в определении кибербезопасности заложено слово «практика»:
Кибербезопасность — это практика защиты сетей, устройств, приложений, систем и данных от киберугроз.
Ведь как говорил один философ: «Практика ведет к совершенству».