Защищаем базы данных на примере 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 [email protected]<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 систему могут быть свои нюансы.

Итог

Чтобы с вашей базой данных всё было хорошо, её безопасностью однозначно нужно заниматься на нескольких уровнях. Постарайтесь учесть все моменты, чтобы данные остались в целости и сохранности.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *