sslmode определяет сразу две разные вещи: шифруется ли соединение с PostgreSQL вообще и проверяет ли клиент, что на другом конце именно тот сервер, к которому он хотел подключиться. Из шести значений на оба вопроса отвечают "да" только два - verify-ca и verify-full, причём только verify-full дополнительно проверяет имя хоста. Значение по умолчанию в libpq, prefer, на первый вопрос отвечает "может быть", на второй - "нет", и делает это молча. Если соединение проходит через любую сеть, которая вам не принадлежит, задайте sslmode=verify-full, передайте клиенту нужный корневой сертификат в sslrootcert и заставьте сервер отклонять всё, что не является TLS, с помощью строк hostssl в pg_hba.conf.
Дальше в статье разобрано, что делает каждый режим на уровне протокола, куда класть файлы сертификатов, как включить TLS на сервере, как одну и ту же настройку понимают разные драйверы (единства между ними нет) и как читать ошибки. Основы подключения извне - строки подключения, порядок правил в pg_hba.conf, команды psql - описаны в статье удалённые подключения к PostgreSQL; здесь мы углубляемся только в часть про TLS.
Шесть режимов и что проверяет каждый#
PostgreSQL начинает TLS-соединение необычным способом. Клиент открывает обычное TCP-соединение на порт 5432 и отправляет небольшой пакет SSLRequest с вопросом, готов ли сервер говорить по TLS. Сервер отвечает одним байтом: S - да, N - нет. Всё, что делает sslmode, происходит вокруг этого байта: как клиент ведёт себя, получив N, и насколько тщательно проверяет всё после начала рукопожатия.
| Режим | Шифрование | Что происходит, если сервер отвечает "нет" |
|---|---|---|
disable | Никогда | Обычное соединение, запрос не отправляется |
allow | Только если сервер требует | Сначала без шифрования, TLS только после отказа |
prefer | Если сервер предлагает | Молча переходит на обычное соединение |
require | Всегда | Соединение завершается ошибкой |
verify-ca | Всегда | Соединение завершается ошибкой |
verify-full | Всегда | Соединение завершается ошибкой |
И второй вопрос, который как раз важен при атаке:
| Режим | Цепочка сертификатов проверяется | Имя хоста проверяется |
|---|---|---|
disable, allow, prefer | Нет | Нет |
require | Нет (см. примечание ниже) | Нет |
verify-ca | Да, по sslrootcert | Нет |
verify-full | Да, по sslrootcert | Да |
Подробнее:
- `disable` никогда не отправляет запрос TLS. Имеет смысл только для Unix-сокета или loopback-соединения на той же машине, где защищать нечего: сети нет.
- `allow` сначала пробует обычное соединение и повторяет попытку с TLS, только если сервер отклонил обычное - как правило, потому что в
pg_hba.confдля вас есть лишь строкиhostssl. Режим оставлен для совместимости, и выбирать его почти никому не стоит. - `prefer` просит TLS, пользуется им, если сервер согласен, и спокойно продолжает по открытому тексту, если сервер отказал или рукопожатие не удалось. Сертификат он не проверяет вообще.
- `require` требует шифрования и завершается ошибкой, если получить его не удалось, но принимает любой сертификат, который предъявит сервер, в том числе самоподписанный, выпущенный кем-то посередине.
- `verify-ca` требует шифрования и проверяет, что сертификат сервера выстраивается в цепочку до CA из корневого файла, которому вы доверяете.
- `verify-full` делает всё перечисленное и дополнительно проверяет, что имя, к которому вы подключались (параметр
host), совпадает с Subject Alternative Name сертификата или с Common Name, если SAN нет.
Почему prefer по умолчанию - слабый вариант#
prefer шифрует, когда может, поэтому кажется безопасным. Проблема в том, что решение о шифровании принимается открытым текстом, до того как в дело вступает какой-либо сертификат. Любой, кто может вмешаться в трафик между вами и базой данных - враждебная Wi-Fi сеть, взломанный маршрутизатор, неверно настроенный прокси, - может ответить на SSLRequest буквой N от имени сервера. Клиент пожмёт плечами и отправит обмен аутентификацией и все запросы открытым текстом. На стороне клиента ничего не попадёт в лог и никакой ошибки не будет.
Даже когда prefer всё же получает TLS-сессию, он не смотрит, чей сертификат ему достался, так что атакующий может так же легко сам завершить TLS и открыть второе соединение с настоящим сервером. Шифрование перед стороной, которую вы не опознали, защищает только от тех, кто пассивно записывает трафик, и больше ни от кого.
Это не теоретическая угроза и для пароля. При аутентификации scram-sha-256 сам пароль по сети не передаётся, что помогает, но последующая сессия - каждая прочитанная и записанная строка - передаётся. А при старом методе md5 или password соединение, переведённое на открытый текст, отдаёт учётные данные, которые можно воспроизвести или подобрать.
Почему же по умолчанию всё ещё prefer? Потому что так соединения работают из коробки и с серверами с TLS, и без него, а смена значения сломала бы огромное число существующих установок. Это выбор в пользу совместимости, а не рекомендация по безопасности, и документация PostgreSQL говорит об этом прямо. Решение простое: никогда на него не полагаться. Указывайте режим в каждой строке подключения или в PGSSLMODE, чтобы поведение было записано, а не унаследовано.
Примерное правило:
| Ситуация | Какой режим использовать |
|---|---|
| Unix-сокет или loopback на той же машине | disable или оставить значение по умолчанию |
| Частная сеть, которую вы полностью контролируете | минимум require |
| Всё, что идёт через интернет | verify-full |
| Сертификат сервера от публичного CA, клиент PG 16+ | verify-full с sslrootcert=system |
Как задать sslmode: URI, ключевые слова и PGSSLMODE#
Клиент на основе libpq берёт режим из трёх мест, и явно указанное значение всегда важнее запасного.
# URI form - parameters go after the ?$ psql "postgresql://appuser@db.example.com:5432/shop?sslmode=verify-full&sslrootcert=/etc/ssl/pg/root.crt"# Keyword form - easier to read in scripts$ psql "host=db.example.com dbname=shop user=appuser sslmode=verify-full sslrootcert=/etc/ssl/pg/root.crt"# Environment variables - used when the string leaves them out$ export PGSSLMODE=verify-full$ export PGSSLROOTCERT=/etc/ssl/pg/root.crt$ psql -h db.example.com -U appuser -d shopС TLS связаны переменные окружения PGSSLMODE, PGSSLROOTCERT, PGSSLCERT, PGSSLKEY и PGSSLCRL. Их читают psql, pg_dump, pg_restore, psycopg и всё остальное, построенное на libpq. Это удобный способ сделать строгой целую машину или контейнер, не правя каждый скрипт, и путаница, когда забытый в профиле оболочки PGSSLMODE=disable заставляет соединение вести себя не так, как та же строка в другом месте. Если поведение на двух машинах различается, первым делом выполните env | grep ^PG. Есть также более старая переменная PGREQUIRESSL, объявленная устаревшей в пользу PGSSLMODE; если заданы обе, побеждает PGSSLMODE.
В URI значения параметров декодируются из процентной записи, как и всё остальное. Путь Windows вроде C:\certs\root.crt проходит как есть, а путь с &, # или ? нужно экранировать, пробелы заменять на %20. Форма с ключевыми словами избавляет от всех этих хлопот.
Чтобы убедиться, что вы действительно получили, посмотрите на баннер при подключении psql. TLS-сессия печатает строку примерно такого вида:
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)Новые клиенты добавляют и другие поля, например протокол ALPN в PostgreSQL 17. Нет такой строки - нет TLS. То же самое говорит \conninfo внутри psql, а из любого клиента можно спросить сам сервер:
SELECT ssl, version, cipherFROM pg_stat_sslWHERE pid = pg_backend_pid();Уберите WHERE и присоедините pg_stat_activity (от роли, которой разрешено видеть чужие сессии), чтобы проверить все соединения сразу - это быстрый способ найти тот самый сервис, который всё ещё подключается открытым текстом.
Файлы сертификатов: sslrootcert, sslcert, sslkey#
Задействованы три файла на стороне клиента, и у каждого в libpq есть расположение по умолчанию:
| Параметр | По умолчанию в Linux и macOS | Назначение |
|---|---|---|
sslrootcert | ~/.postgresql/root.crt | CA, подписавший сертификат сервера |
sslcert | ~/.postgresql/postgresql.crt | Клиентский сертификат, если его запрашивает сервер |
sslkey | ~/.postgresql/postgresql.key | Закрытый ключ клиентского сертификата |
В Windows те же имена лежат в %APPDATA%\postgresql\. Список отзыва можно указать через sslcrl (по умолчанию root.crl), если у вас собственный CA и вы отзываете сертификаты.
sslrootcert нужен всем, кто использует verify-ca и verify-full. Что в него класть, зависит от того, кто подписал сертификат сервера:
- Собственный CA или частный CA хостинга. Файл сертификата CA в формате PEM. Если сервер использует единственный самоподписанный сертификат без CA, этот сертификат сам является корневым и кладётся в этот файл.
- Публичный CA, например Let's Encrypt. В клиентах PostgreSQL 16 и новее
sslrootcert=systemиспользует хранилище доверенных сертификатов операционной системы. Эта настройка также меняет режим по умолчанию наverify-fullи превращает более слабые режимы в ошибку, и это сделано намеренно: доверие ко всем публичным CA без проверки имени хоста позволило бы любому владельцу любого публичного сертификата выдать себя за вашу базу. В старых клиентах укажите вsslrootcertфайл системного набора, например/etc/ssl/certs/ca-certificates.crtв Debian и Ubuntu.
sslcert и sslkey нужны, только если сервер настроен запрашивать клиентский сертификат - либо как способ входа (cert), либо как дополнительную проверку поверх пароля. Файл ключа не должен быть доступен никому другому: libpq отказывается работать с ключом, у которого есть права для группы или остальных на Unix, поэтому chmod 0600 ~/.postgresql/postgresql.key - часть настройки. В PostgreSQL 16 добавлен sslcertmode (disable, allow, require), управляющий тем, отправляет ли клиент сертификат вообще.
В PostgreSQL 17 появилась ещё одна опция подключения, о которой стоит знать: sslnegotiation=direct пропускает обмен SSLRequest и начинает TLS-рукопожатие сразу, экономя один круговой обмен. Ей нужен сервер PostgreSQL 17 и sslmode не слабее require. Значение по умолчанию, postgres, - это классическое согласование, и оно работает везде.
Сторона сервера: ssl = on, сертификаты и hostssl#
Ничто на клиенте не имеет значения, если сервер никогда не предлагает TLS. На сервере, которым вы управляете, TLS включают три параметра в postgresql.conf:
ssl = onssl_cert_file = 'server.crt'ssl_key_file = 'server.key'ssl_min_protocol_version = 'TLSv1.2'Относительные пути отсчитываются от каталога данных. Пакеты Debian и Ubuntu поставляются с ssl = on и указывают на сертификат snakeoil, созданный при установке: его достаточно для require, но для проверки он бесполезен, потому что ему никто не доверяет, а имя в нём - имя машины. ssl_min_protocol_version по умолчанию равен TLSv1.2 в PostgreSQL 13 и новее, так что строка выше лишь фиксирует значение по умолчанию.
Самоподписанный сертификат для тестов по образцу из документации PostgreSQL:
$ openssl req -new -x509 -days 365 -nodes -text \ -out server.crt -keyout server.key -subj "/CN=db.example.com"$ chmod 0600 server.key$ chown postgres:postgres server.key server.crtСервер так же строг к своему ключу, как и клиент: он откажется запускать TLS, если server.key доступен группе или остальным, кроме случая, когда файл принадлежит root и имеет права 0640. В PostgreSQL 10 и новее настройки SSL применяются при перезагрузке конфигурации (SELECT pg_reload_conf();), но ошибка проявляется по-разному в зависимости от способа применения: при reload в лог попадёт SSL configuration was not reloaded, а при перезапуске с ssl = on и нечитаемым сертификатом сервер вообще не запустится. После любого изменения проверяйте лог сервера.
Для всего, что клиенты будут проверять, используйте сертификат, у которого Subject Alternative Name содержит именно то имя хоста, с которым подключаются клиенты. Сертификат для db.example.com не пройдёт verify-full, если приложение подключается по IP-адресу, и наоборот.
Включённый TLS никого не обязывает им пользоваться. Это задача pg_hba.conf:
# TYPE DATABASE USER ADDRESS METHODhostssl shop appuser 0.0.0.0/0 scram-sha-256hostssl shop backup 203.0.113.20/32 certhostnossl all all 0.0.0.0/0 rejectСтроки hostssl соответствуют только TLS-соединениям, hostnossl - только обычным, а host - обоим. Если для удалённых адресов есть только строки hostssl, клиент с prefer, которого принудительно понизили до открытого текста, просто не сможет войти - и сервер становится страховкой от клиентов, которые вы не контролируете. Метод cert аутентифицирует только по клиентскому сертификату, сопоставляя Common Name сертификата с именем роли (или через map). Чтобы требовать действительный клиентский сертификат наряду с паролем, добавьте clientcert=verify-ca или clientcert=verify-full в строку hostssl; тогда серверу понадобится ssl_ca_file, указывающий на CA, который подписывает клиентские сертификаты. Старая форма clientcert=1 удалена в PostgreSQL 14.
В управляемой базе данных всё это обычно изменить нельзя, поэтому вопрос сводится к тому, что предлагает хостинг. Проверьте: psql "...?sslmode=require" либо подключится, либо завершится ошибкой "server does not support SSL", а баннер покажет протокол. Затем спросите у хостинга, какой CA подписал сертификат и какое имя хоста он покрывает, потому что именно это нужно для verify-full. На RE:NODE сервер PostgreSQL доступен по хосту и порту, указанным для тарифа, с паролем суперпользователя, сгенерированным для этого сервера: проверьте TLS-баннер на своём соединении, а не гадайте в ту или другую сторону, и храните нужные приложению файлы сертификатов рядом с его кодом, загружая их через файловый менеджер или по SFTP. Меры защиты, окружающие TLS, собраны в чек-листе безопасности баз данных.
Драйверы: слова одни, поведение разное#
Все клиенты на основе libpq ведут себя так, как описано выше. Драйверы, которые реализуют протокол сами, принимают те же слова sslmode, но не всегда вкладывают в них один и тот же смысл.
psql и psycopg
psql, pg_dump, psycopg 2 и psycopg 3 используют libpq, поэтому режимы, значения по умолчанию, файлы и переменные окружения у них одинаковые. Любой параметр libpq можно передать именованным аргументом:
import osimport psycopgconn = psycopg.connect( host="db.example.com", dbname="shop", user="appuser", password=os.environ["DB_PASSWORD"], sslmode="verify-full", sslrootcert="/app/certs/root.crt",)node-postgres
node-postgres (pg) не использует libpq. Без каких-либо настроек SSL он подключается открытым текстом. В строке подключения его парсер (pg-connection-string) воспринимает prefer, require, verify-ca и verify-full одинаково: "TLS включён, полная проверка по хранилищу доверенных сертификатов Node по умолчанию". Для первых трёх это строже, чем в libpq, и поэтому строка с require, которая работает в psql, в Node падает с self-signed certificate in certificate chain. Если добавить в строку uselibpqcompat=true, будет использоваться трактовка libpq. sslmode=no-verify полностью отключает проверку, что равносильно require в libpq и ничуть не лучше.
Правильное решение - не rejectUnauthorized: false, самая копируемая строка во всей этой области, которая отключает проверку, ради которой TLS и нужен. Вместо этого дайте Node сертификат CA:
import fs from "node:fs";import { Pool } from "pg";const pool = new Pool({ host: "db.example.com", database: "shop", user: "appuser", password: process.env.DB_PASSWORD, ssl: { ca: fs.readFileSync("/app/certs/root.crt", "utf8") },});Одна ловушка: если передать и connectionString с sslmode, и объект ssl, настройки SSL, разобранные из строки, заменяют ваши, а переданный вами ca молча игнорируется. Используйте что-то одно.
JDBC
У драйвера PostgreSQL JDBC (pgjdbc) тоже собственная реализация. В актуальных версиях его sslmode по умолчанию равен prefer, как и в libpq; если указать ssl=true без режима, это означает verify-full. Файлы по умолчанию повторяют libpq, но ключ отличается: он ищет postgresql.pk8 и читает ключи PKCS-12 (.p12, .pfx) или PKCS-8 (.pem, .der), а не традиционный PEM-ключ, который использует libpq. Старые версии драйвера вели себя иначе, поэтому проверьте версию в файле сборки, прежде чем доверять значению по умолчанию.
spring.datasource.url=jdbc:postgresql://db.example.com:5432/shop?sslmode=verify-full&sslrootcert=/app/certs/root.crtsslfactory=org.postgresql.ssl.NonValidatingFactory отключает проверку и является аналогом rejectUnauthorized: false в JDBC. Любую такую копию считайте ошибкой, которую нужно убрать.
У остальных драйверов - pgx для Go, Npgsql для .NET, tokio-postgres для Rust - свои значения по умолчанию и свои написания. Прежде чем выкатывать, прочитайте в документации драйвера всё, что относится к слову sslmode, и храните файл CA в своём развёртывании так же, как другую конфигурацию; где ей место, рассказано в статье переменные окружения и секреты.
Ошибки SSL и что они означают#
`server does not support SSL, but SSL was required` - клиент запросил require или строже, а сервер ответил N. Либо на сервере ssl = off, либо сертификат не загрузился при запуске (ищите в логе сервера), либо вместо PostgreSQL отвечает что-то посередине, например TCP-прокси. node-postgres сообщает об этой же ситуации как The server does not support SSL connections. Не "исправляйте" это понижением режима; чините сервер или спрашивайте хостинг.
`root certificate file "/home/app/.postgresql/root.crt" does not exist` - вы запросили verify-ca или verify-full и не задали sslrootcert, поэтому libpq заглянул в место по умолчанию и ничего там не нашёл. Новые клиенты добавляют подсказку с предложением sslrootcert=system или более слабого режима. Укажите в sslrootcert правильный файл; в контейнерах помните, что домашним каталогом часто служит /root или его нет вовсе.
`SSL error: certificate verify failed` - сертификат сервера не выстраивается в цепочку до CA из sslrootcert. Обычно это не тот файл (серверный сертификат другой машины или набор без промежуточного сертификата) либо публичный сертификат, проверяемый по частному корню. openssl s_client -starttls postgres -connect db.example.com:5432 (OpenSSL 1.1.1 и новее) покажет цепочку, которую сервер отправляет на самом деле.
`server certificate for "db.example.com" does not match host name "10.0.0.5"` - это verify-full делает свою работу. Подключайтесь по имени, которое покрывает сертификат, или выпустите сертификат заново, указав в Subject Alternative Name то имя или адрес, которым вы пользуетесь.
`SSL SYSCALL error: EOF detected` или `SSL SYSCALL error: Connection reset by peer` - TCP-соединение под TLS-сессией закрылось без корректного завершения TLS. Проблема редко в сертификате. Частые причины: процесс сервера был убит (ищите остановку из-за нехватки памяти в логе сервера), сервер перезапустился, либо NAT-шлюз, firewall или балансировщик оборвал простаивающее соединение. Если это происходит с простаивающими соединениями в пуле, задайте таймаут простоя пула меньше, чем у сети, и включите TCP keepalive - настройки разобраны в статье пулы соединений и лимиты.
`private key file "/home/app/.postgresql/postgresql.key" has group or world access` - выполните chmod 0600 для файла. В Docker файлы, смонтированные с хоста Windows или macOS, часто приходят со слишком открытыми правами; скопируйте ключ в образ или том и задайте права там.
`no pg_hba.conf entry for host "...", user "appuser", database "shop", no encryption` - сервер принимает для вас только TLS, а клиент подключился открытым текстом. Что-то задало disable или allow, либо драйвер по умолчанию не использует TLS. Задайте режим явно.
Для сбоев сертификатов в целом - просроченных сертификатов, отсутствующих промежуточных, неверных имён - действуют те же рассуждения, что и для веб-серверов; их разбирает статья ошибки SSL-сертификатов.
FAQ#
Какой sslmode в PostgreSQL по умолчанию?
Для libpq и всего, что на нём построено (psql, psycopg, pg_dump), это prefer: использовать TLS, если сервер его предлагает, иначе перейти на открытый текст, сертификат не проверять никогда. Драйверы с собственной реализацией выбирают собственные значения по умолчанию, а node-postgres вообще не использует TLS, пока ему этого не скажут.
Достаточно ли безопасен sslmode=require?
Он защищает от пассивного прослушивания, и в частной сети, которую вы полностью контролируете, этого может хватить. От активного атакующего он не защищает, потому что принимается любой сертификат. Через интернет используйте verify-full.
В чём разница между verify-ca и verify-full?
Оба проверяют, что сертификат подписан CA, которому вы доверяете. verify-full дополнительно проверяет, что сертификат выпущен для имени хоста, к которому вы подключились. С публичным CA одного verify-ca почти недостаточно, потому что любой может получить у этого CA сертификат для своего домена.
Где взять root.crt?
У тех, кто управляет сервером. Для самоподписанного сервера это сам server.crt сервера; для частного CA - сертификат CA; для публичного CA - системное хранилище через sslrootcert=system в клиентах PostgreSQL 16 и новее.
Действует ли sslmode на соединения через Unix-сокет?
Нет. TLS используется только по TCP. Соединение через /var/run/postgresql игнорирует sslmode, поэтому локальный psql работает даже при заданном PGSSLMODE=verify-full и без единого сертификата.
Можно ли заставить использовать TLS на сервере, а не надеяться на клиентов?
Да. Оставьте в pg_hba.conf для удалённых адресов только строки hostssl и для ясности добавьте строку hostnossl ... reject. Клиент с открытым текстом тогда не сможет войти, а не будет выдавать свою сессию. Это не заменяет verify-full на клиенте, который - единственное, что защищает от подмены сервера.


