Защищаем базы данных на примере PostgreSQL

База данных — это одна из главных целей хакеров. Если у них есть полный доступ к данным, то у вас большущие проблемы. Давайте рассмотрим, как защитить ваши данные:
- на сетевом уровне;
- на транспортном уровне;
- на уровне базы данных.
И сделаем мы это на примере PostgreSQL.
На уровне сети
Фаервол
Чтобы всё было хорошо, сервер с PostgreSQL нужно полностью изолировать: избегать входящих подключений, psql и SSH. Но проблема в том, что PostgreSQL нельзя так настроить по стандарту.
Давайте поступим иначе, и запретим доступ к узлу через порт, с которым работает база данных. Поставим на него фаервол.
PostgreSQL прослушивает TCP-порт 5425 по умолчанию. Для Linux воспользуемся iptables
:
# Make sure not to drop established connections.
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow SSH.
iptables -A INPUT -p tcp -m state --state NEW --dport 22 -j ACCEPT
# Allow PostgreSQL.
iptables -A INPUT -p tcp -m state --state NEW --dport 5432 -j ACCEP
# Allow all outbound, drop everything else inbound.
iptables -A OUTPUT -j ACCEPT
iptables -A INPUT -j DROP
iptables -A FORWARD -j DROP
Если обновляете правила iptables, то советуем пользоваться iptables-apply
— он автоматически откатит изменения, если вы случайно себя заблокируете.
Это правило даёт любому подключиться к порту 5432. Оно может быть строже: например, к порту смогут подключиться определённые IP-адреса или подсети:
# Only allow access to PostgreSQL port from the local subnet.
iptables -A INPUT -p tcp -m state --state NEW --dport 5432 -s 192.168.1.0/24 -j ACCEPT
Если хотим получить идеальный результат, входящему подключению к порту 5432 нужен локальный агент, который будет поддерживать перманентное исходящее соединение с узлами клиента и может проксировать трафик на локальный вариант СУБД.
Этот метод называется обратное туннелирование, и как пример мы будем использовать удалённое перенаправление портов. Выполните эту команду с узла, на котором работает PostgreSQL, чтобы открыть обратный туннель:
ssh -f -N -T -R 5432:localhost:5432 user@<client-host>
<client-host
> должен быть доступным из узла PostgreSQL с активным SSH-демоном. Эта команда ретранслирует порт 5432 на сервере БД на тот же порт на клиентском компьютере, и у вас появляется путь к базе данных через туннель:
psql "host=localhost port=5432 user=postgres dbname=postgres"
Прослушивание адресов
Полезно будет ограничить адреса, которые прослушивают сервер для клиентских подключений. Для этого используем параметр файла конфигурации listen_addresses
.
Если у узла, через который работает PostgreSQL, несколько сетевых интерфейсов, используем вот этот параметр. Так мы убедимся, что сервер прослушивает только интерфейсы, по которым будут подключаться клиенты.
listen_addresses = 'localhost, 192.168.0.1'
Если клиенты подключаются к БД и находятся на одном узле или поде Kubernetes с PostgreSQL, тогда при отключении прослушки сокетов TCP мы убираем сеть из этого кейса. А если запишем пустую строку в параметр прослушиваемых адресов, то сервер будет принимать только соединения сокетов домена Unix:
listen_addresses = ''
На транспортном уровне
Интернет переходит на HTTPS, поэтому хорошее шифрование для соединения с базой данных — необходимость, а не мода. В документации PostgreSQL протокол TLS по причине “так исторически сложилось” называется SSL. Нам важно, что с ним можно аутентифицировать и клиент, и сервер.
TLS сервера
Чтобы аутентифицировать сервер, нужно получить сертификат для подключившихся клиентов. Бесплатно получаем сертификаты X.509 через инструмент CLI certbot от Let’s Encrypt:
certbot certonly --standalone -d postgres.example.com
Важно помнить, что certbot для проверки запросов сертификатов по умолчанию использует вызов HTTP-01 ACME. Ему нужен рабочий DNS для указывающего на узел домена из запроса и открытый порт 80.
Если вы не хотите пользоваться Let’s Encrypt, лучше использовать openssl CLI:
# Make a self-signed server CA.
openssl req -sha256 -new -x509 -days 365 -nodes \
-out server-ca.crt \
-keyout server-ca.key
# Generate server CSR. Put the hostname you will be using to connect to
# the database in the CN field.
openssl req -sha256 -new -nodes \
-subj "/CN=postgres.example.com" \
-out server.csr \
-keyout server.key
# Sign a server certificate.
openssl x509 -req -sha256 -days 365 \
-in server.csr \
-CA server-ca.crt \
-CAkey server-ca.key \
-CAcreateserial \
-out server.crt
Следите, чтобы в продакшене эти сертификаты вовремя обновлялись.
TLS клиента
Что нам даёт аутентификация клиента по сертификату? Так сервер может проверить личность того, кто подключается, и подтвердить подпись сертификата X.509.
Лучше пользоваться разными центрами сертификации для сервера и клиента. Сейчас создадим клиентский центр сертификации и используем его для подписи клиентского сертификата:
# Make a self-signed client CA.
openssl req -sha256 -new -x509 -days 365 -nodes \
-out client-ca.crt \
-keyout client-ca.key
# Generate client CSR. CN must contain the name of the database role you
# will be using to connect to the database.
openssl req -sha256 -new -nodes \
-subj "/CN=alice" \
-out client.csr \
-keyout server.key
# Sign a client certificate.
openssl x509 -req -sha256 -days 365 \
-in client.csr \
-CA client-ca.crt \
-CAkey client-ca.key \
-CAcreateserial \
-out client.crt
Важно, чтобы в поле сертификата клиента CommonName (CN) было имя учётной записи базы данных, к которой подключается клиент. Так сервер PostgreSQL идентифицирует клиента.
Настройка TLS
Собрав все части вместе, теперь вы можете настроить сервер PostgreSQL для приема соединений TLS:
ssl = on
ssl_cert_file = '/path/to/server.crt'
ssl_key_file = '/path/to/server.key'
ssl_ca_file = '/path/to/client-ca.crt'
# This setting is on by default but it’s always a good idea to
# be explicit when it comes to security.
ssl_prefer_server_ciphers = on
# TLS 1.3 will give the strongest security and is advised when
# controlling both server and clients.
ssl_min_protocol_version = 'TLSv1.3'
Дело за малым — осталось обновить файл аутентификации хоста сервера PostgreSQL (pg_hba.conf
). Так мы сможем требовать TLS от всех подключений и аутентифицировать клиентов сертификатами X.509.
# TYPE DATABASE USER ADDRESS METHOD
hostssl all all ::/0 cert
hostssl all all 0.0.0.0/0 cert
Теперь при подключении к серверу пользователи должны показывать подписанный центром сертификации сертификат.
psql "host=postgres.example.com \
user=alice \
dbname=postgres \
sslmode=verify-full \
sslrootcert=/path/to/server-ca.crt \
sslcert=/path/to/client.crt \
sslkey=/path/to/client.key"
По умолчанию psql не будет проверять сертификат сервера, так что для sslmode ставим значение verify-full
или verify-ca
. Выбор зависит от подключения к серверу через имя хоста из поля CN его сертификата X.509.
Пользуйтесь файлом службы подключения PostgreSQL, чтобы сделать размер команд меньше и не вводить пути к сертификатам TLS при подключении к базе данных. Он даст сгруппировать параметры подключения в специальные службы, на которые можно ссылаться в строке подключения.
Давайте создадим ~/.pg_service.conf
с таким содержимым:
[example]
host=postgres.example.com
user=alice
sslmode=verify-full
sslrootcert=/path/to/server-ca.crt
sslcert=/path/to/client.crt
sslkey=/path/to/client.key
Теперь можно будет указывать только имя службы и базы данных при подключении.
psql "service=example dbname=postgres"
На уровне базы данных
Что такое роли?
Сейчас мы разрешили серверу PostgerSQL подключать только авторизированные клиенты, настроили шифрование и убедились, что клиенты и серверы взаимно аутентифицируют друг друга.
Теперь нам нужно понять, что именно пользователи могут делать и какие доступы у них есть после подключения к базе данных и подтверждения личности. Проще говоря, авторизации.
Система прав PostgreSQL строится на ролях. У версий 8.1 и выше роль и пользователь — почти одно и то же. Имя учётной записи базы данных — это роль с атрибутом login
. Он-то и даёт подключаться к БД.
Вот эти команды равнозначны:
CREATE USER alice;
CREATE ROLE alice LOGIN;
У ролей могут быть другие атрибуты, которые дают обходить все проверки прав пользователя (SUPERUSER
), создавать новые базы данных (CREATEDB
), роли (CREATEROLE
) и много чего ещё.
Кроме атрибутов, ролям можно дать права доступа двух видов: участие в других ролях и привилегии на другие базы данных. Сейчас рассмотрим их подробнее.
Даём права доступа для роли
Представим, что мы следим за инвентаризацией серверов:
CREATE TABLE server_inventory (
id int PRIMARY KEY,
description text,
ip_address text,
environment text,
owner text,
);
Настройка PostgreSQL по умолчанию включает роль суперпользователя, чтобы первично загрузить базу данных, и обычно её называют “postgres”. Если вы постоянно пользуетесь этой ролью, то представьте, что всё время делаете всё с root в Linux. Это не лучший вариант.
Сделаем роль без привилегий и дадим ей права доступа по необходимости. Давать привилегии и роли каждому пользователю отдельно не очень удобно, поэтому мы можем сделать групповую роль и добавить туда пользователей.
Допустим, нашим условным Алисе и Бобу нужно разрешить смотреть список серверов без права изменять его:
-- Create a group role that doesn't have ability to login by itself and
-- grant it SELECT privileged on the server inventory table.
CREATE ROLE developer;
GRANT SELECT ON server_inventory TO developer;
-- Create two user accounts which will inherit "developer" permissions upon
-- logging into the database.
CREATE ROLE alice LOGIN INHERIT;
CREATE ROLE bob LOGIN INHERIT;
-- Assign both user account to the "developer" group role.
GRANT developer TO alice, bob;
Если Алиса и Боб подлючатся к базе данных, они получат привилегии групповой роли “разработчик”, чтобы выполнять запросы к таблице инвентаризации сервера.
Привилегия SELECT
действует на все столбцы таблицы по умолчанию, но так не должно быть. Допустим, что вам нужно разрешить Бобу и Алисе смотреть общую информацию, но запретить подключение со скрытым IP-адресом:
CREATE ROLE intern;
GRANT SELECT(id, description) ON server_inventory TO intern;
CREATE ROLE charlie LOGIN INHERIT;
GRANT intern TO charlie;
Популярные привилегии INSERT
, UPDATE
, DELETE
и TRUNCATE
соответствуют SQL выражениям. Весь список привилегий ищите в разделе Privileges документации PostgreSQL.
На уровне строк
Очень полезная функция привилегий PostgreSQL — безопасность на уровне строк. Вы можете предоставлять привилегии подмножеству строк в таблице: как для запрошенных через оператор SELECT
, так и для результатов операторов INSERT
, UPDATE
и DELETE
.
Как пользоваться безопасностью на уровне строк? Вам нужно включить её для таблицы и выбрать политику контроля доступа на уровне строк.
За основу берём прошлый кейс, и попытаемся разрешить пользователям обновить только свои серверы. Включим RLS в таблице:
ALTER TABLE server_inventory ENABLE ROW LEVEL SECURITY;
По умолчанию в PostgreSQL работает политика “запретить”, то есть, кроме владельца никто не имеет доступа к таблице.
Политика безопасности строк выглядит как логическое выражение и применяется PostgreSQL для каждой строки, которую нужно возвратить или обновить. Если строки, которые возвращает оператор SELECT
, должны соответствовать выражению из раздела USING, то обновлённые операторами INSERT
, UPDATE
или DELETE
строки должны соответствовать выражению WITH CHECK
.
Сделаем так, чтобы пользователи видели все серверы, но могли обновлять только свои, и используем поле “владелец”:
CREATE POLICY select_all_servers
ON server_inventory FOR SELECT
USING (true);
CREATE POLICY update_own_servers
ON server_inventory FOR UPDATE
USING (current_user = owner)
WITH CHECK (current_user = owner);
Важно запомнить, что только у владельца есть право создать или обновить политику безопасности строк для таблицы.
Аудит на закуску
Мы разобрались, как улучшить безопасность на нескольких уровнях последовательно, чтобы атакующий погруз в нашей базе данных. Но давайте ещё рассмотрим, как мы можем улучшить безопасность, когда речь идёт о самом сервере PostgreSQL.
Простой, но важный момент — подробное ведение журнала. Чтобы включить его для всех попыток подключения и выполненных операторов SQL, нужно добавить эти параметры в файл настройки сервера:
; Log successful and unsuccessful connection attempts.
log_connections = on
; Log terminated sessions.
log_disconnections = on
; Log all executed SQL statements.
log_statement = all
Из коробки это почти всё, что нам даст сделать PostgreSQL. Но можно использовать расширение pgAudit, которое для self-hosted версии СУБД нужно ставить вручную.
pgAudit структурирует операторы и делает их детальнее. Учтите, что оно работает на журналах, поэтому при отправлении в SIEM систему могут быть свои нюансы.
Итог
Чтобы с вашей базой данных всё было хорошо, её безопасностью однозначно нужно заниматься на нескольких уровнях. Постарайтесь учесть все моменты, чтобы данные остались в целости и сохранности.
Авторские куры по системному администрированию и Devops на itedu.center