Как защитить PostgreSQL 16: три слоя между базой и интернетом
Базу данных ломают скучно: открытый порт, стандартный логин, подобранный пароль — и пустые логи, в которых некому это заметить. Защита от такого сценария — не один флаг, а три независимых слоя: контроль доступа, шифрование и аудит.
База — не сайт: её вообще не должно быть видно
Классический сценарий компрометации базы данных скучен: порт 5432 открыт всему интернету, логин стандартный postgres, пароль подобран за вечер, трафик не шифруется, а в логах пусто — некому даже заметить перебор. Ни одного «взлома» в голливудском смысле, только настройки по умолчанию, оставленные как есть.
Боты сканируют весь адресный диапазон интернета непрерывно: свежеподнятый Postgres с открытым портом находят за часы, а не за месяцы. Защита от этого сценария — не один флаг, а три независимых слоя: контроль доступа (кто и откуда может подключиться), шифрование (что видно по пути) и аудит (что происходило и кого пора забанить). Ниже — все три на примере PostgreSQL 16.
Слой 1. Кто и откуда: pg_hba.conf
Файл pg_hba.conf — это список правил аутентификации. Правила читаются сверху вниз до первого совпадения, поэтому порядок строк — часть конфигурации, а не оформление. Самая опасная строка выглядит безобидно:
host all all 0.0.0.0/0 trusttrust означает «пускать без пароля», а 0.0.0.0/0 — «кого угодно». Такая строка иногда появляется «на минуту, чтобы отладить» — и остаётся. Правильная конфигурация строится наоборот: сначала специфичные разрешения, в конце — явный отказ всем остальным.
# TYPE DATABASE USER ADDRESS METHOD
# Приложение работает под ОС-пользователем www-data, а в базу ходит
# как app_user — имена не совпадают, поэтому peer не подойдёт.
# Более специфичное правило стоит ПЕРВЫМ.
local all app_user scram-sha-256
# Остальные локальные подключения через Unix-сокет — peer:
# роль в базе сверяется с именем пользователя ОС.
local all all peer
# Сетевые подключения: только по TLS и только из доверенной подсети.
hostssl all all 192.168.1.0/24 scram-sha-256
# Всё остальное — явный отказ. Обе версии протокола IP.
host all all 0.0.0.0/0 reject
host all all ::0/0 rejectТри правила, на которых это держится:
Метод — только scram-sha-256. Старый md5 уязвим к краже и повтору хэша, trust — не метод аутентификации вообще. С 14-й версии scram и так метод по умолчанию, но в конфигах, переживших несколько апгрейдов, md5 дожил до наших дней.
Сетевой тип — hostssl, а не host: он отклоняет подключения без TLS ещё до проверки пароля.
Последняя строка — reject. Даже если фаервол однажды откроют шире, чем задумано, база откажет сама. Слои защиты для того и нужны, чтобы ошибка в одном не открывала всё.
В postgresql.conf базу стоит привязать к внутреннему адресу — тогда наружу она не слушает вовсе:
listen_addresses = '192.168.1.100'
password_encryption = scram-sha-256
scram_iterations = 4096 # значение по умолчанию, можно поднятьИзменения pg_hba.conf применяются без перезапуска — SELECT pg_reload_conf(); — а вот смена listen_addresses требует рестарта СУБД. Проверить, что правила прочитались без ошибок, можно прямо из базы — в 16-й версии у представления появились номер правила и имя файла:
SELECT rule_number, type, database, user_name, address, auth_method, error
FROM pg_hba_file_rules;Непустая колонка error — это правило, которое молча не работает. Заодно в 16-й версии правила можно раскладывать по отдельным файлам через include_dir — удобно, когда конфиг собирается автоматикой.
Слой 2. Что видно по пути: TLS
Трафик между приложением и базой без шифрования читается любым узлом на маршруте — вместе с паролями и данными. Серверная часть включается несколькими строками:
ssl = on
ssl_cert_file = '/etc/postgresql/ssl/server.crt'
ssl_key_file = '/etc/postgresql/ssl/server.key'
ssl_ca_file = '/etc/postgresql/ssl/root.crt' # нужен для клиентских сертификатов
ssl_min_protocol_version = 'TLSv1.2'
ssl_prefer_server_ciphers = onПрава на приватный ключ — не формальность: с ключом, доступным группе или всем, PostgreSQL просто не запустится.
chmod 600 /etc/postgresql/ssl/server.key
chown postgres:postgres /etc/postgresql/ssl/server.keyВторая половина настройки — клиентская, и о ней забывают чаще. По умолчанию клиент подключается с sslmode=prefer: шифрование включится, если сервер предложит, но подлинность сервера никто не проверит — подменивший сервер узел получит и пароль, и данные. Честное подключение выглядит так:
psql "host=db.internal dbname=app user=app_user \
sslmode=verify-full \
sslrootcert=/etc/ssl/certs/internal-ca.crt \
require_auth=scram-sha-256"sslmode=verify-full проверяет и подпись центра сертификации, и совпадение имени хоста. Параметр require_auth появился в 16-й версии и закрывает последнюю щель: сервер-самозванец не сможет предложить клиенту метод попроще и выманить пароль открытым текстом.
Слой 3. Что происходило: логи и Fail2ban
Перебор паролей не оставляет следов ровно до тех пор, пока логи не настроены. Включаем сбор и — главное — добавляем в префикс строки %r: без него в логе не будет IP-адреса клиента, и банить будет некого.
logging_collector = on
log_directory = '/var/log/postgresql'
log_filename = 'postgresql-%Y-%m-%d.log'
log_rotation_age = 1d
log_rotation_size = 100MB
# %r выводит адрес и порт клиента — без него Fail2ban бессилен.
log_line_prefix = '%m [%p] %q%u@%d %r '
log_connections = on
log_disconnections = on
log_statement = 'ddl'Неудачная попытка входа с таким префиксом выглядит в логе так:
2026-07-22 10:14:03.812 UTC [24417] postgres@app 203.0.113.5(54321) FATAL: password authentication failed for user "postgres"Дальше дело за Fail2ban: три неудачи за десять минут — час бана на уровне фаервола.
[postgresql]
enabled = true
filter = postgresql
logpath = /var/log/postgresql/postgresql-*.log
port = 5432
protocol = tcp
banaction = iptables-multiport
# На Debian 12+ / RHEL 9, где бэкенд фаервола по умолчанию nftables:
# banaction = nftables-multiport
maxretry = 3
findtime = 600
bantime = 3600[Definition]
failregex = ^.*\s<HOST>\(\d+\) FATAL:\s+password authentication failed for user .*$
^.*\s<HOST>\(\d+\) FATAL:\s+no pg_hba\.conf entry for host .*$
^.*\s<HOST>\(\d+\) FATAL:\s+role ".*" does not exist.*$
ignoreregex =Фильтр ловит не только неверные пароли: попытка входа под несуществующей ролью и подключение, не подошедшее ни под одно правило pg_hba.conf, — такие же верные признаки перебора.
Чек-лист
| Что | Признак, что всё в порядке |
|---|---|
| pg_hba.conf | Только hostssl + scram-sha-256, замыкающий reject; ни trust, ни md5 |
| listen_addresses | Внутренний адрес, наружу база не слушает |
| TLS | TLSv1.2+, валидные сертификаты, на клиентах verify-full |
| Логи | %r в префиксе, сессии и DDL пишутся |
| Fail2ban | Джейл включён, тестовая неудачная попытка входа попадает в бан |
| Фаервол | Порт 5432 открыт только приложению — на уровне ОС или облачной группы безопасности |
| Привилегии | Приложение ходит не под postgres, а под ролью с минимумом прав |
| Обновления | Минорные релизы ветки 16.x ставятся регулярно |
Про привилегии одна деталь, которую часто тянут из старых инструкций: отзывать право записи у схемы public вручную нужно было до 15-й версии — начиная с неё обычные роли и так не могут создавать в ней объекты. Проверить стоит только базу, пережившую апгрейд со старых версий.
Причём здесь проверка сайта
Ни при чём — и это лучшая новость статьи. Наша проверка смотрит на сайт снаружи, глазами постороннего, и базу данных она не видит: не сканирует порты, не пробует подключиться, не перебирает пароли. Если всё настроено по этой статье, снаружи от вашего PostgreSQL не видно вообще ничего — к этому состоянию и надо стремиться.
Верно и обратное: база, которую видно из интернета, — находка, которую не покажет никакой пассивный отчёт, потому что искать её снаружи — уже не наблюдение, а вторжение. Слой «что видно с улицы» — заголовки, TLS сайта, забытые поддомены со стендами, открытые админки — проверяется за минуту; слой «что за фасадом» — только вот так, руками и конфигами. Нужны оба.