⚠️ Дисклеймер (YMYL): Безопасность СУБД - ответственная область. Перед применением команд на production-сервере тестируйте в staging-окружении. Для систем, обрабатывающих персональные данные граждан РФ, дополнительно изучите требования Федерального закона № 152-ФЗ и рекомендации ФСТЭК России.
Вы установили PostgreSQL, создали базу, подключили приложение - и считаете это «настройкой».
На самом деле дефолтный PostgreSQL открыт для атак: суперпользователь без пароля, md5-аутентификация, нет аудита, public-схема доступна всем. За 30–60 минут это можно исправить.
В этом руководстве - конкретные команды, примеры политик RLS и готовый чеклист из 30 пунктов.
Материал актуален для PostgreSQL 14–17 и Postgres Pro на 2026 год.
Почему безопасность PostgreSQL - это не опция
PostgreSQL популярен в России в том числе потому, что российская компания Postgres Professional развивает сертифицированный форк Postgres Pro и официальную документацию на русском языке.
Проблема в другом: при стандартной установке PostgreSQL оптимизирован под удобство разработки, а не под безопасность production-среды. Открытый порт 5432 на всех интерфейсах, метод аутентификации md5, суперпользователь postgres с пустым паролем - типичный набор «как после apt install».
Угрозы и последствия
Три сценария, которые чаще всего приводят к инцидентам:
- SQL-инъекция через приложение - если роль приложения имеет избыточные права (UPDATE, DELETE, DROP), одна уязвимость в коде = компрометация всей БД.
- Insider threat - сотрудник с прямым доступом к БД читает или модифицирует чужие записи. Без RLS и аудита это незаметно.
- Privilege escalation через SECURITY DEFINER - функции с этим атрибутом выполняются с правами создателя, а не вызывающего. Злоумышленник может использовать их для повышения привилегий.
Требования 152-ФЗ и ФСТЭК
Если в вашей БД хранятся персональные данные граждан РФ, Федеральный закон № 152-ФЗ обязывает обеспечить их защиту от несанкционированного доступа. ФСТЭК России публикует методические документы по защите информационных систем персональных данных (ИСПДн) - в частности, Приказ № 21. Postgres Pro Enterprise имеет сертификат соответствия ФСТЭК - актуальный статус уточняйте на сайте регулятора.
Авторская ремарка: за три года работы с production PostgreSQL на нескольких SaaS-проектах я ни разу не видел, чтобы новый разработчик самостоятельно закрыл public-схему или заменил md5 на scram. Это не лень - просто документация об этом не кричит.
Роли и привилегии: архитектура доступа
Роль в PostgreSQL - универсальный объект: может быть пользователем (с LOGIN), группой (без LOGIN) или набором прав. Именно через роли реализуется принцип минимальных привилегий (PoLP - Principle of Least Privilege).
Иерархия ролей: четыре уровня
Рекомендуемая архитектура для production:
-- 1. Группы (без логина) - агрегируют права
CREATE ROLE readonly NOLOGIN;
CREATE ROLE app_user NOLOGIN;
CREATE ROLE migrator NOLOGIN;
CREATE ROLE dba_admin NOLOGIN;
-- 2. Конкретные пользователи - наследуют от групп
CREATE ROLE api_service LOGIN PASSWORD 'str0ngP@ss' CONNECTION LIMIT 50;
GRANT app_user TO api_service;
CREATE ROLE analyst_ivan LOGIN PASSWORD 'str0ngP@ss2';
GRANT readonly TO analyst_ivan;
| Роль | Права | Когда использовать |
|---|---|---|
| readonly | SELECT на нужные таблицы | Аналитики, отчётные инструменты, BI |
| app_user | SELECT, INSERT, UPDATE, DELETE - только на бизнес-таблицы | Приложение в runtime |
| migrator | DDL: CREATE, ALTER, DROP + DML | CI/CD, Flyway, Liquibase |
| dba_admin | Управление ролями, GRANT, конфиг | Только для администратора |
GRANT и REVOKE: выдавать точечно
Не давайте права на всю БД - выдавайте только то, что нужно:
-- Закрываем public-схему (критично!)
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- Права app_user только на бизнес-схему
GRANT USAGE ON SCHEMA app TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_user;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA app TO app_user;
-- Права readonly
GRANT USAGE ON SCHEMA app TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO readonly;
-- Чтобы новые таблицы тоже получали права:
ALTER DEFAULT PRIVILEGES IN SCHEMA app
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA app
GRANT SELECT ON TABLES TO readonly;
⚠️ REVOKE ALL ON SCHEMA public FROM PUBLIC - это не опечатка. По умолчанию каждый пользователь PostgreSQL может создавать объекты в public-схеме. Это открывает атаку через подмену функций.
Опасности: superuser и SECURITY DEFINER
Два правила, которые нарушают почти все:
- Никогда не подключайте приложение под postgres (суперпользователь обходит RLS, GRANT, все ограничения).
- Функции с SECURITY DEFINER - потенциальный вектор privilege escalation. Если без них не обойтись, помещайте их в отдельную схему и закрывайте search_path:
CREATE OR REPLACE FUNCTION secure.get_user_data(uid int)
RETURNS TABLE(name text)
SECURITY DEFINER
SET search_path = secure, pg_catalog -- Фиксируем search_path!
AS $$
SELECT name FROM secure.users WHERE id = uid;
$$ LANGUAGE sql;
Проверить текущие права - запрос к системным таблицам:
SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee NOT IN ('postgres', 'PUBLIC')
ORDER BY grantee, table_name;
Row-Level Security: фильтрация на уровне строк
RLS появился в PostgreSQL 9.5 и работает как невидимый WHERE-фильтр на каждой таблице. Даже если приложение выполняет SELECT * FROM orders, БД возвращает только те строки, для которых политика разрешает доступ текущему пользователю.
Как включить RLS
-- 1. Включаем RLS на таблице
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 2. Для владельца таблицы тоже (иначе он обходит политики)
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
После ENABLE ROW LEVEL SECURITY без определённых политик таблица недоступна никому (Default Deny). Это задокументированное поведение PostgreSQL, не баг.
USING vs WITH CHECK
В CREATE POLICY два ключевых клауза:
- USING - фильтрует строки при SELECT, UPDATE, DELETE (что пользователь видит)
- WITH CHECK - проверяет строки при INSERT и UPDATE (что пользователь пишет)
-- Пользователь видит только свои заказы
CREATE POLICY user_sees_own_orders ON orders
FOR SELECT
USING (user_id = current_user::int);
-- Пользователь может создавать заказы только от своего имени
CREATE POLICY user_inserts_own_orders ON orders
FOR INSERT
WITH CHECK (user_id = current_user::int);
RLS для мультитенантного SaaS
Классический паттерн для SaaS с несколькими арендаторами - столбец tenant_id + RLS:
-- Таблица с данными арендаторов
CREATE TABLE documents (
id bigserial PRIMARY KEY,
tenant_id int NOT NULL,
content text
);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
-- Роль получает tenant_id через настройку сессии
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.current_tenant')::int);
-- Приложение устанавливает перед запросами:
-- SET app.current_tenant = '42';
Авторская ремарка: в реальном проекте я использовал именно этот паттерн - current_setting() в USING-клаузе. Главное - устанавливать переменную сессии до любого запроса к таблице, иначе получите ошибку или пустой результат.
Производительность и подводные камни
- Функции в USING-условии лучше делать STABLE или IMMUTABLE - PostgreSQL может кешировать их результат.
- BYPASSRLS - атрибут роли, который полностью отключает RLS. Его нельзя давать прикладным пользователям.
- Суперпользователь (SUPERUSER) обходит RLS автоматически - ещё один аргумент не подключать приложение под postgres.
Аудит: что, кто и когда делал в БД
Аудит - это не паранойя, а доказательная база. Без него невозможно ни расследовать инцидент, ни отчитаться перед регулятором.
Встроенное логирование через postgresql.conf
Минимальный набор параметров для безопасного логирования:
# postgresql.conf
log_connections = on # Логировать все подключения
log_disconnections = on # Логировать отключения
log_failed_auth = on # Неудачные попытки аутентификации
log_line_prefix = '%m [%p] %q%u@%d ' # Время, PID, пользователь, БД
log_statement = 'ddl' # Логировать DDL-операции (CREATE, ALTER, DROP)
log_min_duration_statement = 1000 # Запросы дольше 1 сек - в лог (аномалии)
После изменения параметров - SELECT pg_reload_conf(); (без рестарта).
pg_audit: полный аудит DDL и DML
Для детальной записи всех операций - расширение pg_audit:
# postgresql.conf - добавить pg_audit в preload
shared_preload_libraries = 'pg_audit'
# После рестарта кластера:
pgaudit.log = 'read, write, ddl, role' # Что аудировать
pgaudit.log_catalog = off # Не спамить системными запросами
pgaudit.log_parameter = on # Логировать параметры запросов
Установка расширения:
CREATE EXTENSION pgaudit;
Лог pg_audit пишется в стандартный PostgreSQL-лог. Формат строки содержит: тип операции, объект, команду, пользователя - это позволяет парсить лог и строить отчёты.
Мониторинг активных сессий
-- Кто сейчас подключён и что делает
SELECT pid, usename, application_name, client_addr,
state, query_start, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;
-- Убить подвисший процесс (осторожно!)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'active' AND query_start < NOW() - INTERVAL '1 hour';
Шифрование: данные в пути и в покое
SSL/TLS для соединений
Включение SSL - минимальное требование для production:
# postgresql.conf
ssl = on
ssl_cert_file = '/etc/postgresql/ssl/server.crt'
ssl_key_file = '/etc/postgresql/ssl/server.key'
ssl_ca_file = '/etc/postgresql/ssl/ca.crt' # Для взаимной аутентификации mTLS
В pg_hba.conf - принудительно требовать SSL:
# pg_hba.conf - только hostssl, не host
hostssl all all 0.0.0.0/0 scram-sha-256
На стороне клиента - sslmode=verify-full (проверяет сертификат сервера + CN):
postgresql://user:pass@db.example.com/mydb?sslmode=verify-full
⚠️ sslmode=require (без verify-full) не защищает от атак «человек посередине» - он шифрует трафик, но не проверяет подлинность сервера.
pgcrypto: шифрование чувствительных полей
Для шифрования конкретных столбцов (пароли, токены, номера карт) - расширение pgcrypto:
CREATE EXTENSION pgcrypto;
-- Шифруем значение перед вставкой
INSERT INTO users (email, secret_token) VALUES (
'user@example.com',
pgp_sym_encrypt('my-secret-token', 'encryption-key-here')
);
-- Расшифровываем при чтении
SELECT email, pgp_sym_decrypt(secret_token::bytea, 'encryption-key-here')
FROM users WHERE id = 1;
-- Хэшируем пароли (bcrypt)
SELECT crypt('user-password', gen_salt('bf', 10));
-- Проверка:
SELECT crypt('user-password', stored_hash) = stored_hash;
⚠️ Точных данных о производительности pgcrypto на высоких нагрузках нет - тестируйте в вашем конкретном окружении.
Шифрование резервных копий
Бэкап без шифрования - это незашифрованная копия всех ваших данных:
# pg_dump + GPG шифрование
pg_dump -h localhost -U backup_user mydb | \
gpg --symmetric --cipher-algo AES256 -o backup_$(date +%Y%m%d).dump.gpg
# Восстановление
gpg -d backup_20260601.dump.gpg | psql -U postgres mydb
Ключ шифрования храните отдельно от бэкапа - в секрет-менеджере (HashiCorp Vault, Yandex Lockbox, AWS Secrets Manager).
Сетевая изоляция и конфигурация сервера
listen_addresses: не слушать лишнее
# postgresql.conf
# Только localhost или конкретный внутренний IP
listen_addresses = '127.0.0.1'
# Для сети: listen_addresses = '10.0.1.5'
# Никогда для production: listen_addresses = '*'
Порт 5432 - закройте на уровне firewall для всех, кроме разрешённых IP:
# iptables: разрешить только приложению (10.0.1.10)
iptables -A INPUT -p tcp --dport 5432 -s 10.0.1.10 -j ACCEPT
iptables -A INPUT -p tcp --dport 5432 -j DROP
pg_hba.conf: правила подключений
# Локальные соединения через Unix-сокет - peer (доверяем ОС)
local all postgres peer
local all all peer
# TCP/IP - только scram-sha-256, только SSL, только нужные подсети
hostssl mydb api_service 10.0.1.0/24 scram-sha-256
hostssl mydb analyst_ivan 10.0.2.5/32 scram-sha-256
# Всё остальное - запрет
host all all 0.0.0.0/0 reject
scram-sha-256 вместо md5
md5 в PostgreSQL - устаревший метод, уязвимый к rainbow-table атакам. Перевод на scram-sha-256 (доступен с PG 10):
# postgresql.conf
password_encryption = scram-sha-256
После смены параметра - обновить пароли всех ролей (иначе старые хэши остаются в pg_authid):
-- Для каждой роли:
ALTER ROLE api_service PASSWORD 'newStr0ngP@ss';
Чеклист: 30 пунктов за 30 минут
🔐 Блок 1. Роли и доступ (10 пунктов)
- R1. Смените пароль суперпользователя postgres: ALTER USER postgres PASSWORD 'newPass';
- R2. Никогда не подключайте приложение под postgres - создайте отдельную роль.
- R3. Создайте иерархию ролей: readonly, app_user, migrator, dba_admin.
- R4. Закройте public-схему: REVOKE ALL ON SCHEMA public FROM PUBLIC;
- R5. Установите CONNECTION LIMIT для всех прикладных ролей.
- R6. Проверьте роли с SUPERUSER: SELECT rolname FROM pg_roles WHERE rolsuper = true;
- R7. Удалите или заблокируйте неиспользуемые роли: ALTER ROLE old_user NOLOGIN;
- R8. Все функции с SECURITY DEFINER - зафиксируйте SET search_path внутри.
- R9. Выдавайте права через DEFAULT PRIVILEGES - чтобы новые объекты наследовали политику.
- R10. Ежеквартально аудируйте права: information_schema.role_table_grants.
🛡️ Блок 2. RLS и политики (5 пунктов)
- L1. Включите RLS на всех таблицах с чувствительными данными: ENABLE ROW LEVEL SECURITY.
- L2. Добавьте FORCE ROW LEVEL SECURITY для защиты даже от владельца таблицы.
- L3. Создайте политику Default Deny (без политик = полный запрет).
- L4. Для мультитенантных таблиц - используйте tenant_id + current_setting().
- L5. Никогда не давайте прикладным ролям атрибут BYPASSRLS.
📋 Блок 3. Аудит и мониторинг (6 пунктов)
- A1. Включите log_connections, log_disconnections, log_failed_auth.
- A2. Установите log_line_prefix = '%m [%p] %q%u@%d '.
- A3. Установите расширение pg_audit, добавьте в shared_preload_libraries.
- A4. Настройте pgaudit.log = 'read, write, ddl, role'.
- A5. Настройте log_min_duration_statement для выявления аномалий.
- A6. Регулярно проверяйте pg_stat_activity на подозрительные соединения.
🔒 Блок 4. Шифрование (5 пунктов)
- E1. Включите ssl = on, настройте пути к сертификатам.
- E2. В pg_hba.conf замените host на hostssl.
- E3. На клиентах установите sslmode=verify-full.
- E4. Установите pgcrypto для шифрования чувствительных полей.
- E5. Бэкапы шифруйте GPG или встроенным шифрованием облачного хранилища.
🌐 Блок 5. Сеть и обновления (4 пункта)
- N1. Установите listen_addresses - только нужные IP, не *.
- N2. Закройте порт 5432 на firewall для всех, кроме разрешённых хостов.
- N3. Переключитесь на scram-sha-256 в pg_hba.conf и postgresql.conf.
- N4. Подпишитесь на рассылку безопасности PostgreSQL и своевременно применяйте патчи.
Альтернативный взгляд
Некоторые архитекторы считают RLS «костылём», которым компенсируют слабую логику разграничения в приложении. Их аргумент: политики строк усложняют отладку, замедляют сложные JOIN-запросы и создают неочевидные зависимости. Для небольших монолитов с надёжным application-layer auth это может быть оправдано. Но в мультитенантных SaaS и системах с несколькими клиентскими приложениями RLS - последний рубеж обороны, когда в одном из слоёв возникает уязвимость.
Нетривиальный факт
Мало кто знает, что PostgreSQL по умолчанию не логирует успешные SELECT-запросы даже при log_statement = 'all' - если они выполняются через extended query protocol (prepared statements). Для полного аудита SELECT-запросов нужен pg_audit с флагом pgaudit.log = 'read'. Без этого целый класс операций чтения остаётся невидимым для аудитора.
FAQ
Что будет, если включить RLS без создания политик?
Таблица полностью закрывается для всех, кроме суперпользователя. Это не баг - это поведение Default Deny, задокументированное в официальной документации PostgreSQL. Сначала создайте политику, затем включайте RLS на production-таблицах.
Можно ли использовать md5 в 2026 году?
Технически работает, но md5 - устаревший метод аутентификации, уязвимый к rainbow-table атакам. С PostgreSQL 10 доступен scram-sha-256 - переходите на него. После смены password_encryption в postgresql.conf обязательно обновите пароли всех ролей через ALTER ROLE.
Влияет ли RLS на производительность?
Да, RLS добавляет overhead: к каждому запросу PostgreSQL автоматически подклеивает условие из политики. Минимизируйте влияние: используйте STABLE/IMMUTABLE функции в USING-условиях (PostgreSQL кеширует их результат), создавайте индексы по полям, которые участвуют в политике (например, tenant_id), проверяйте план через EXPLAIN ANALYZE.
Зачем pg_audit, если есть log_statement?
log_statement = 'all' не фиксирует SELECT-запросы, выполняемые через extended query protocol (prepared statements в JDBC, psycopg2, pgx). pg_audit закрывает этот пробел и пишет структурированный лог: тип операции, схема, объект, команда, параметры - в формате, удобном для парсинга и SIEM-систем.
Что такое BYPASSRLS и кому нельзя его давать?
BYPASSRLS - атрибут роли, полностью отключающий проверку Row-Level Security для этой роли. Нельзя выдавать прикладным пользователям, сервисным аккаунтам и ролям приложений. Допустимо только для DBA при отладке - и только временно, с обязательным отзывом после.
Нужен ли pg_audit для соответствия 152-ФЗ?
152-ФЗ обязывает фиксировать факты доступа к персональным данным. pg_audit с pgaudit.log = 'read, write' обеспечивает нужный уровень детализации. Точные требования к составу журнала аудита зависят от класса ИСПДн - уточняйте в приказах ФСТЭК и документах РКН.
Как проверить, что RLS реально работает?
Подключитесь под прикладной ролью (не postgres) и выполните SELECT * FROM protected_table. Затем проверьте EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM protected_table - в плане должен быть виден фильтр из политики. Суперпользователь RLS не видит по определению - для тестирования используйте SET ROLE app_user.



.svg.webp)





