Расширение pgcrypto даёт PostgreSQL собственные функции для работы с паролями: crypt() считает хеш, gen_salt() генерирует соль. Этого достаточно, чтобы регистрировать пользователей и проверять пароли прямо в SQL, без отдельного слоя в приложении. Минимальный рабочий пример выглядит так:
SELECT crypt('secret', gen_salt('bf', 10));
На выходе получается строка вида $2a$10$..., в которой закодированы алгоритм bcrypt, параметр стоимости 10 и соль. Отдельное поле для соли не нужно: crypt() достаёт её из сохранённого хеша.
У этого удобства есть цена. Пароль попадает в текст SQL-запроса, а текст запроса может оказаться в логе сервера (log_statement = 'all') и в статистике pg_stat_statements. Для публичного сервиса такой путь рискован, для внутреннего инструмента или прототипа он вполне рабочий. Ниже разобраны установка, синтаксис, готовые запросы и критерии выбора между базой и приложением.
Зачем хешировать пароли в PostgreSQL и когда это оправдано
pgcrypto это расширение из стандартной поставки PostgreSQL (contrib). Оно добавляет crypt() и gen_salt() для паролей, digest() для хешей, pgp_sym_encrypt() для симметричного шифрования. Компилировать ничего не нужно, файлы расширения лежат рядом с сервером и подключаются одной командой.
Хеширование паролей в базе закрывает конкретный класс задач: приложение не умеет хешировать (bash-скрипт, ETL-процесс, самописная админка), отдельного бэкенда нет, есть только PostgreSQL и клиент к нему, нужно быстро проверить гипотезу на прототипе. Типовой пример: небольшой внутренний сервис, где вся логика аутентификации выражается парой SQL-запросов.
Что даёт pgcrypto на практике: bcrypt с настраиваемым cost factor, автоматическую соль внутри хеша, единый формат $2a$/$2b$, совместимый с библиотеками bcrypt в приложениях. Последнее важно при миграции: хеши, созданные crypt(), читаются библиотекой bcrypt в PHP, Python или Go, и наоборот.
Ограничение подхода: набор алгоритмов ограничен четырьмя вариантами - bf, xdes, md5, des. Argon2id, scrypt и PBKDF2 недоступны. Для нового публичного сервиса это весомый минус, для служебного инструмента - нет.
Прямой ответ на главный вопрос: pgcrypto подходит для базовой защиты паролей, когда других вариантов нет или они избыточны. Как основа аутентификации публичного продукта с персональными данными это решение не годится.
Установка и настройка pgcrypto в PostgreSQL
Включение выполняется одной командой в той базе, где будут храниться пользователи:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
Нужны права суперпользователя или роль с привилегией CREATE на базу и доверием к расширению. В managed-инстансах (Timeweb Cloud, Amazon RDS, Яндекс Managed PostgreSQL) команда обычно доступна владельцу базы. Если провайдер ведёт белый список расширений, pgcrypto включают через консоль или параметр shared_preload_libraries для отдельных сборок.
Проверка после установки:
SELECT crypt('test', gen_salt('bf'));
Ответ вида $2a$06$... означает, что всё работает. Пустой результат или ошибка "function crypt(text, text) does not exist" говорят о том, что расширение не включено в этой базе, либо подключение идёт к другой базе или к реплике с иным набором расширений. Ошибка "permission denied to create extension" решается ролью с нужными правами.
Расширение создаётся в конкретной базе. Если приложение подключается к другой базе, команду нужно повторить там. На реплике расширение появится, если оно установлено на основном сервере до создания реплики.
Поведение crypt() и gen_salt() стабильно на PostgreSQL 13 и новее: набор алгоритмов и синтаксис не менялись. Дополнительной настройки autovacuum или планировщика расширение не требует.
Функции crypt() и gen_salt(): синтаксис и алгоритмы
gen_salt(type, iter_count) возвращает соль в формате, который понимает crypt(). Поддерживаются четыре типа:
| Тип | Алгоритм | iter_count | Статус |
|---|---|---|---|
| bf | bcrypt (Blowfish) | cost factor, 4-31, по умолчанию 6 | рекомендуется |
| xdes | расширенный DES | число итераций, по умолчанию 8 | устарел |
| md5 | MD5 с солью | не используется | небезопасен |
| des | классический DES | не используется | небезопасен |
crypt(password, salt) возвращает строку хеша, в которой закодированы алгоритм, параметры и соль. Пример на bcrypt с cost 10:
SELECT crypt('mypassword', gen_salt('bf', 10));
-- $2a$10$N9qo8uLOickgx2ZMRZoMye...
Тот же пароль с той же солью даёт одинаковый хеш, с новой солью - другой. На этом строится проверка: при аутентификации crypt() получает соль и параметры из сохранённого хеша.
md5 и des оставлены для совместимости с очень старыми системами. Оба алгоритма ломаются перебором на доступном железе. Новый проект не должен применять эти типы.
Алгоритм bf (bcrypt): параметры и производительность
Cost factor задаёт число раундов и вместе с ним время вычисления. Ориентиры для одного потока на современном серверном x86:
- cost = 10: около 100 мс на хеш;
- cost = 12: около 400 мс;
- cost = 14: порядка 1,5 с.
Рабочий диапазон для продакшена: 10-12. Cost 10 даёт приемлемую задержку входа и делает перебор в тысячи раз дороже одного раунда. Cost 12 усиливает стойкость, но заметен пользователю: почти полсекунды ожидания, а всплеск параллельных входов нагружает CPU базы. Значение выше 12 применяют выборочно, например для административных учётных записей.
bcrypt включает соль в результат crypt(), поэтому поле salt в таблице не требуется. Соль уникальна для каждой записи: gen_salt() при каждом вызове возвращает новое значение.
Как подобрать cost factor под своё железо и закрыть перебор, описано в руководстве по защите хранилища паролей от брутфорса и атак по времени: там есть бенчмарк, настройки rate limiting и чек-лист из 16 пунктов.
Алгоритм xdes: когда ещё используется и почему не рекомендуется
xdes это расширенный DES с восемью итерациями по умолчанию. Он остался ради совместимости: хеши вида _J9..rasmBYk8r9AiWNc из старых установок PostgreSQL и PHP читаются и проверяются до сих пор.
Разница в стоимости перебора нагляднее любых оценок. bcrypt с cost 10 считает хеш около 100 мс, xdes - меньше 1 мс, то есть примерно в 1000 раз быстрее. Ограничение длины пароля в 8 символов у классического DES и слабая стойкость делают xdes непригодным для новых проектов. Материал, где хеши новых пользователей пишутся в xdes, стоит переписать.
Оправданный сценарий для xdes один: миграция legacy-базы. Тогда применяют двойной хеш: проверяют старое значение, при успешном входе пересчитывают пароль в bcrypt и сохраняют результат.
Практические SQL-запросы для регистрации и проверки паролей
Схема таблицы пользователей:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
login TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX users_login_idx ON users (lower(login));
Индекс по login нужен для быстрого поиска при входе, выражение lower(login) закрывает регистронезависимый логин. Дополнительных индексов тут не требуется, кроме случаев, когда на таблицу ложится тяжёлая выборка вместе с аутентификацией. В такой ситуации пригодятся приёмы из материала о медленных запросах и способах их ускорения.
Регистрация пользователя: INSERT с crypt() и gen_salt()
INSERT INTO users (login, password_hash)
VALUES ($1, crypt($2, gen_salt('bf', 10)))
RETURNING id;
$1 и $2 - параметры, передаваемые драйвером. Конкатенация строк в SQL недопустима: она открывает SQL-инъекцию. gen_salt('bf', 10) вызывается на каждую вставку и создаёт новую соль, поэтому одинаковые пароли двух пользователей дают разные хеши.
Две частые ошибки: фиксированная соль в константе для всей таблицы и слишком короткое поле password_hash. Строка bcrypt с префиксом $2a$10$ занимает 60 символов, для xdes достаточно 20, для md5 - 35. Тип TEXT снимает вопрос с запасом.
Проверка пароля: SELECT с crypt() и сравнение
SELECT id FROM users WHERE login = $1 AND password_hash = crypt($2, password_hash);
crypt($2, password_hash) читает соль и параметры из сохранённого значения, пересчитывает хеш от введённого пароля и возвращает строку в том же формате. Равенство строк даёт ответ. При неверном логине или пароле запрос вернёт 0 строк.
Слабое место этого запроса: сравнение в SQL не гарантирует постоянное время выполнения, а планировщик может вычислять условия в произвольном порядке. Теоретическая timing-атака по такому запросу требует точных измерений и большого числа попыток, но в системах с высокими требованиями проверку переносят в приложение: сначала выбирают password_hash по login, затем сверяют хеши функцией с constant-time сравнением (password_verify() в PHP, hmac.compare_digest() в Python) и добавляют rate limiting.
Второй момент: запрос с открытым паролем в тексте попадает в логи и статистику. Подробнее об этом в разделе про риски.
Смена пароля и обновление хеша
UPDATE users
SET password_hash = crypt($1, gen_salt('bf', 10))
WHERE id = $2
AND password_hash = crypt($3, password_hash);
Условие по password_hash проверяет старый пароль в том же запросе. Новая соль генерируется при каждом обновлении, старая не переиспользуется. Ответ UPDATE 1 подтверждает смену, UPDATE 0 означает, что старый пароль не совпал или пользователя нет.
Ту же конструкцию применяют для повышения cost factor: пароль, введённый при плановом входе, пересчитывают с новым значением, а хеш обновляют в фоне.
Хеширование в БД или в приложении: сравнение подходов
| Критерий | pgcrypto в базе | Приложение |
|---|---|---|
| Открытый пароль в логах СУБД | возможен | нет |
| Доступные алгоритмы | bf, xdes, md5, des | Argon2id, bcrypt, scrypt, PBKDF2 |
| Смена параметров cost | SQL на каждого пользователя | в коде, централизованно |
| Правки в приложении | не нужны | нужны |
| Сравнение constant-time | не гарантировано | есть в библиотеках |
Сильная сторона pgcrypto: одна точка правды. Логика аутентификации лежит в схеме базы, приложению достаточно выполнить два запроса, а миграция на новый сервер переносит хеши без изменений. Слабая сторона: открытый пароль проходит через SQL-слой.
Риски хеширования на стороне БД: логи и утечки
Пароль входит в текст запроса, а текст запроса может сохраниться в четырёх местах:
- лог сервера при log_statement = 'all' или 'mod';
- pg_stat_statements, который хранит текст и параметры запроса;
- журнал ошибок: PostgreSQL пишет невыполненный запрос целиком;
- логи slow query и auto_explain при срабатывании log_min_duration_statement.
Одна строка в логе с crypt('secret', gen_salt('bf', 10)) обесценивает любое хеширование: пароль стоит рядом с ней в открытом виде. Ротация логов сокращает срок жизни утечки, но не отменяет её. Проверка логов на такие строки входит в регулярный аудит: набор запросов и проверок есть в руководстве по аудиту безопасности баз данных.
Параметризованные запросы снимают проблему частично: в обычном логе остаётся строка с placeholders, а значения уходят отдельно. Однако pg_stat_statements в ряде конфигураций сохраняет параметры, а auto_explain может записать значения. Полностью безопасный вариант один: хешировать до обращения к базе, чтобы сервер получал готовый хеш.
Когда всё же стоит использовать pgcrypto
Оправданные сценарии:
- прототип или MVP, где скорость сборки важнее строгих требований к защите;
- внутренний инструмент в закрытом контуре, без доступа из интернета;
- приложение, которое нельзя менять (вендорное, замороженное);
- разовые SQL-скрипты миграции: массовый пересчёт хешей при смене алгоритма;
- учебные и тестовые стенды, где нужен предсказуемый результат без кода.
В этих случаях минимизируют логирование паролей и оставляют bcrypt с cost не ниже 10. Если база живёт в облаке, включить pgcrypto помогает провайдер: в Timeweb Cloud расширения managed-базы PostgreSQL доступны из панели, а сам сервер можно разместить рядом с приложением и закрыть доступ по сети.
Для публичного сервиса с персональными данными хеширование переносят в приложение. В PHP это password_hash() с PASSWORD_ARGON2ID, в Python - passlib или argon2-cffi, в Go - golang.org/x/crypto/bcrypt. Пароль не покидает процесс, выбор алгоритма и параметров контролируется в коде, а смена cost factor не требует SQL-миграции.
Ограничения pgcrypto и альтернативные решения
Что стоит учитывать при выборе:
- нет Argon2id и scrypt: лучший доступный вариант - bcrypt с cost 10-12;
- устаревшие md5, des, xdes остаются в наборе и легко применяются по ошибке;
- параметры cost нельзя задать глобально: gen_salt('bf', 10) придётся писать в каждом запросе;
- вычисления идут на CPU сервера базы и конкурируют с обычными запросами: при сотне входов в секунду с cost 12 это уже сотни ядро-секунд нагрузки;
- pgcrypto не управляет жизненным циклом паролей: политики, срок действия, история хешей и сброс остаются на приложении;
- проверка пароля не защищена от timing-атак на уровне SQL.
Альтернативы: библиотеки хеширования в приложении (bcrypt, Argon2id), внешние системы аутентификации (Keycloak, OAuth-провайдеры), а также pgcrypto только для шифрования данных, не для паролей. Шифрование при передаче и хранении, включая TLS для соединений с базой и защиту бэкапов, разбирается в отдельном материале о шифровании данных при передаче и хранении.
Безопасность и аудит: как защитить пароли при использовании pgcrypto
Если выбор сделан в пользу pgcrypto, риски снижают набором конкретных настроек.
- Логирование. Установите log_statement = 'none' или 'ddl'. Значение 'mod' уже создаёт риск, а 'all' неприемлемо для базы с аутентификацией.
- pg_stat_statements. Расширение полезно для разбора медленных запросов, но хранит текст обращений. Проверьте, сохраняются ли параметры в вашей версии, и очищайте статистику по расписанию.
- log_min_duration_statement. Порог в миллисекундах означает, что медленный INSERT с crypt() попадёт в лог вместе с паролем. Для базы с аутентификацией порог либо отключают, либо исключают проблемные запросы.
- Права. Приложению нужны только SELECT, INSERT и UPDATE на таблицу users. Отдельная роль без доступа к pg_authid и без прав на другие схемы уменьшает последствия компрометации.
- Сеть. Даже при хешировании в базе пароль идёт по сети в открытом виде. Соединение приложения с PostgreSQL закрывают TLS: проверяют ssl = on и строку hostssl в pg_hba.conf.
- Проверка логов. Раз в неделю ищите в логах строки с crypt(, gen_salt( и password. Найденное означает утечку и требует смены паролей пользователей.
- Ротация. Срок хранения логов ограничивают, старые архивы шифруют или удаляют.
Отдельно про бэкапы: дамп базы содержит хеши, а не пароли, что уже защищает пользователей. Однако дамп, снятый во время отладки с log_statement = 'all', может притащить лог с открытыми паролями. Проверяйте содержимое архивов перед отправкой на внешнее хранилище.
Итоги: рекомендации по выбору подхода
pgcrypto с crypt() и gen_salt('bf') закрывает базовую задачу: хеширование и проверку паролей без кода в приложении. Для продакшена с внешними пользователями предпочтительнее хеширование в приложении с bcrypt или Argon2id: пароль не попадает ни в SQL, ни в логи, а параметры алгоритма меняются в одном месте.
Если pgcrypto уже используется или выбран осознанно, держитесь правил: алгоритм bf, cost 10-12, новая соль на каждую запись, параметризованные запросы, log_statement = 'none', TLS для соединений, права приложения ограничены таблицей users.
| Сценарий | Рекомендация |
|---|---|
| Публичный сервис, персональные данные | хеширование в приложении, Argon2id или bcrypt |
| Внутренний инструмент, закрытый контур | pgcrypto, bf, cost 10-12 |
| Прототип, MVP | pgcrypto допустим, с планом перехода |
| Legacy с xdes или md5 | двойной хеш и постепенный пересчёт в bcrypt |
| Нужны политики паролей, MFA, SSO | внешний провайдер аутентификации |
Перед переносом запросов в продакшен сверьтесь с разделом pgcrypto официального руководства PostgreSQL для вашей версии сервера: список поддерживаемых типов соли и границы iter_count лучше проверять на месте, а не по памяти.