Требование зашифровать всё подряд обычно рождается из заботы о безопасности и почти всегда заканчивается медленными запросами, сломанным поиском и лишним слоем кода. Шифрование отдельных столбцов в базе это не галочка в отчёте, а точный инструмент с ощутимой ценой. Он окупается там, где утечка конкретной таблицы ведёт к штрафу, ущербу или потере репутации, и только мешает там, где сведения не секретны ни для кого. Граница между этими случаями тоньше, чем кажется, и провести её лучше до первой миграции, а не после первого инцидента.
Реальные сценарии, где шифрование в базе окупается
Начать стоит с честного списка кандидатов, он короче, чем принято думать. Первая группа это платёжные сведения. Стандарт карточных платежей запрещает хранить код подтверждения вовсе, а номер карты разрешает держать только в нечитаемом виде: зашифрованным, урезанным до последних четырёх цифр или заменённым токеном. Вторая группа это персональные сведения, за утрату которых регулятор назначает штрафы: номера паспортов, сведения о здоровье, страховые номера. В Европе верхняя планка штрафа достигает четырёх процентов годового оборота компании, и шифрование при утечке играет роль смягчающего обстоятельства: перехваченный шифротекст без ключа раскрытием не считается.
Третья группа, о которой вспоминают позже всех, это ключи и токены доступа к чужим системам. Долгоживущие токены интеграций, секреты платёжных шлюзов, ключи подписи. Их утечка открывает злоумышленнику не таблицу, а всю инфраструктуру компании. Сюда же относятся сканы документов, зарплатные записи во внутренних сервисах, коммерческие тарифы партнёров, то есть всё, что компания обещала не раскрывать.
Типовая сценка из практики выглядит так. Команда читает новость о штрафе конкурента и решает зашифровать всю базу целиком. Через месяц аналитики не могут собрать отчёты, поддержка не находит клиентов по фамилии, а релизы ходят кругами из-за миграций. Проблема не в шифровании, а в отсутствии границы: под замок попали и то, что действительно стоило спрятать, и то, что никто никогда не похищал.
От какой утечки спасает шифрование, а от какой нет
Честная модель угроз экономит месяцы работы. Шифрование столбцов спасает, когда утекает сама база: скопированный дамп, потерянный бэкап, файлы таблиц, снятые с диска, выгрузка, которую унёс сотрудник с правом чтения. Открытого текста в таких файлах нет, и без ключа содержимое превращается в бессмысленный набор байтов.
Против взлома приложения шифрование в базе не работает. Приложение само держит ключ и само расшифровывает значения для ответа пользователю, значит взломанный сервер приложения отдаст и открытый текст, и ключ. Если ключ передаётся прямо в SQL-запросе из настроек, инъекция, умеющая выполнять произвольные запросы, вытащит и то, и другое. Открытые значения почти всегда остаются ещё и в журналах приложения, в памяти, в кэше шаблонов. Авторы pgcrypto формулируют это без обиняков: их функции исполняются внутри сервера базы, открытый текст и пароли ходят между модулем и клиентом в ясном виде, поэтому нужен шифрованный канал и доверие администратору, а если доверия нет, шифровать следует на стороне приложения.
Отдельный разговор про облачные базы. У поставщика свои администраторы и свои доступы, и шифрование полей на стороне приложения как раз тот приём, который прячет открытый состав даже от персонала площадки.
Из сказанного следует простое правило. Шифрование в базе защищает сведения в покое, и строить его стоит от сценария похищения файлов, а не от сценария взлома веб-приложения. Второй закрывается другими мерами: проверкой входных значений, минимальными правами, журналами.
Инструменты от pgcrypto до прозрачного шифрования на уровне файлов
В PostgreSQL стандартный путь это расширение pgcrypto. Оно устанавливается одной командой, относится к доверенным модулям и не требует прав суперпользователя, хотя без собранной поддержки OpenSSL просто не установится. Для паролей нужна не шифровка, а необратимый хэш, и здесь работают crypt и gen_salt с алгоритмом bf: чем выше второй аргумент, тем дольше считается хэш и тем дороже перебор, значение вроде двенадцати сегодня типично. Для полей служат pgp_sym_encrypt и pgp_sym_decrypt, режим шифра задаётся параметром cipher-algo=aes256. Сырые функции encrypt и decrypt документация прямо не рекомендует: целостность они не проверяют, а вектор инициализации оставляют на совести разработчика.
CREATE EXTENSION pgcrypto;
INSERT INTO payments (user_id, pan_encrypted, last4)
VALUES (42,
pgp_sym_encrypt('4111 1111 1111 1111', :key, 'cipher-algo=aes256'),
'1111');
SELECT pgp_sym_decrypt(pan_encrypted, :key) FROM payments WHERE id = 7;
В составе расширения есть и hmac. Обычный хэш защищает от опечаток, но не от злоумышленника, который подменит значение и хэш вместе, а хэш с ключом такую подмену уже не пропустит, и эта функция ещё пригодится в разговоре о поиске.
В MySQL свои функции AES_ENCRYPT и AES_DECRYPT с одной коварной деталью по умолчанию: системная переменная block_encryption_mode стартует со значения aes-128-ecb, а режим ECB одинаковые блоки входа превращает в одинаковый шифротекст, то есть сама база мягко подталкивает к слабой схеме. Первым делом режим меняют:
SET SESSION block_encryption_mode = 'aes-256-cbc';
В MongoDB шифрование полей на стороне клиента появилось в версии 4.2, а начиная с 7.0 доступно запрашиваемое шифрование: поиск по зашифрованным полям на равенство, и сервер при такой схеме не видит ни ключей, ни открытых значений. Наконец, TDE в SQL Server, Oracle и InnoDB шифрует файлы целиком и прозрачно для запросов: защищает от уноса дисков и бэкапов, но живой администратор базы по-прежнему читает открытый текст. В PostgreSQL встроенного TDE нет, сторонние проекты только набирают зрелость, поэтому там опираются на шифрование столбцов или диска средствами операционной системы.
Чем оборачивается шифрование для поиска и индексов
Вот где начинается расплата за энтузиазм. Формат OpenPGP в pgcrypto при каждом вызове берёт случайный сеансовый ключ, поэтому два одинаковых номера карты дают два разных шифротекста. Обычное сравнение на равенство не находит ничего, индекс по зашифрованному столбцу бесполезен, сортировка и группировка теряют смысл. Частичный поиск по подстроке исчезает полностью: шаблон LIKE по шифротексту не умеет обрабатывать ни один движок.
А как тогда искать? Обходы есть, у каждого своя цена. Детерминированная схема, где одинаковый вход даёт одинаковый выход, возвращает равенство и индексы, но раскрывает, какие строки совпадают, а это уже утечка структуры. Классический приём для почты и логинов выглядит иначе: рядом с зашифрованным столбцом держат столбец с хэшем от ключа и ищут по нему.
-- при вставке считаем хэш с ключом, сам адрес лежит зашифрованным
INSERT INTO users (email_enc, email_hmac)
VALUES (pgp_sym_encrypt(:email, :key), hmac(:email, :hmac_key, 'sha256'));
SELECT id FROM users WHERE email_hmac = hmac(:email, :hmac_key, 'sha256');
Такой хэш одинаков для одинаковых значений и непрозрачен для чужака без ключа, поэтому он даёт точный поиск, но не подбор по словарю, как обычный хэш без ключа. Диапазоны и частичное совпадение всё равно остаются за бортом: запрашиваемое шифрование в первых версиях поддерживает только равенство, диапазоны добирались значительно позже, а для поиска по фамилии с началом строки приходится строить отдельный поисковый контур, что снова расширяет поверхность атаки. Добавьте рост объёма: шифропакет PGP в несколько раз длиннее исходной строки, столбцы превращаются в двоичные, миграции усложняются, и каждая выгрузка для отчёта теперь тащит расшифровку на прикладном слое.
Ключи, которые нельзя держать рядом с замком
Шифрование без управления ключами это декорация. Ключ в том же файле настроек, что и пароль к базе, означает подарок взломщику: одна утечка вскрывает и замок, и отмычку. Ключ, закоммиченный в репозиторий, живёт в истории изменений вечно, даже после удаления. Рабочая схема называется конвертной: сведения шифруются ключом конкретной версии, а сам он заворачивается ключом из внешнего хранилища вроде облачного KMS или Vault. Приложение получает развёрнутый ключ только в памяти и только на время работы.
Ротация закладывается с первого дня, а не когда истёк срок. В каждой строке живёт столбец с версией ключа:
ALTER TABLE payments ADD COLUMN key_version smallint NOT NULL DEFAULT 3;
Новые записи пишутся с новой версией, старые перезаписываются лениво при чтении. Полная ротация в спокойном режиме выглядит так: включается свежая версия, все записи пишутся только с ней, ночной фоновый процесс небольшими порциями перезаписывает старые строки, в конце прежняя версия помечается архивной. Тогда смена ключа не превращается в многочасовой UPDATE по всем таблицам в выходные. Внешнее хранилище заодно ведёт журнал: кто и когда разворачивал ключ. Это ровно та запись, которую аудитору проще всего показать, отвечая на вопрос о доступе к открытым сведениям.
Случаи, когда излишняя защита сведений приносит вред
Перегрев начинается там, где под замок уходят поля без всякой секретности. Каталог товаров, названия городов, курсы валют, статусы заказов: эти значения и так публикуются на сайте. Шифрование суммы заказа ломает отчётность: агрегаты не посчитать на стороне базы, придётся тянуть миллионы строк по сети и расшифровывать на прикладном слое. Временные метки и служебные поля нужны для отладки, сортировки и мониторинга, их закрытие убивает обычные запросы и добавляет работы всему конвейеру обработки.
Отдельная история с почтой и телефонами. Формально это персональные сведения, фактически по ним происходит вход в аккаунт, поиск дубликатов и рассылки. Шифровать их без подготовленной схемы поиска значит заставить каждый вход перебирать таблицу. Зрелое решение выглядит так: значение шифруется, рядом живёт хэш с ключом для точного поиска, а разворачивание допускается лишь для узкого круга задач под журналом доступа.
Вред от перегиба не только в производительности. Часы команды уходят на поддержку ключей, миграции и согласованные выгрузки, а на базовые меры остаются крохи: права ролей, шифрование канала, журнал запросов, проверка бэкапов. Возникает самообман, будто база теперь надёжно закрыта, хотя взломанное приложение и слабые пароли администраторов никого не волновали ни до, ни после внедрения шифра.
И ещё один классический перекос стоит назвать отдельно: попытка шифровать пароли. Шифрование обратимо, значит однажды ключ найдётся, и пароли утекут уже открытыми. Пароли только хэшируют, медленно и с солью, а обратимость там, где она действительно нужна, оставляют для карт и документов.
Порядок действий для команды при решении о шифровании
Рабочая последовательность, которая позволяет не увязнуть:
- Составить реестр сведений и отметить поля, за утечку которых последуют штраф, ущерб или потеря доверия клиентов;
- Для каждого отмеченного поля описать сценарий атаки, от которого шифрование должно спасти, и способы, которыми поле ищется и читается в продукте;
- Выбрать слой защиты: шифрование на стороне приложения, функции в самой базе или прозрачное шифрование файлов, при необходимости их комбинация;
- Заложить внешнее хранилище ключей и столбец версии ключа до первой записи, а не после первой проверки безопасности;
- Решить вопрос поиска заранее: хэш с ключом для равенства, отдельный индекс для частичного совпадения или отказ от шифрования там, где искать придётся слишком активно;
- Прогнать на копии реального объёма восстановление из бэкапов, типовые отчёты и горячие запросы и только затем включать шифрование в рабочем окружении.
Хороший вывод из всего сказанного звучит непарадоксально: шифровать надо узко. Карты, документы, сведения о здоровье, токены интеграций заслуживают шифра, ключей в отдельном хранилище и продуманного поиска. Публичный каталог, суммы для отчётности и временные метки заслуживают нормальных индексов и быстрых запросов. Простая проверка перед каждым столбцом экономит месяцы: сформулировать, какой конкретно инцидент этот шифр предотвращает. Есть внятный ответ, значит столбец шифруется, а ключи едут в хранилище. Ответа нет, значит поле остаётся в покое, и команда тратит освободившийся вечер на права доступа и проверку бэкапов, которые закрывают больше реальных сценариев, чем лишний слой криптографии.