Шифрование — не одна кнопка, а три разные задачи для трёх разных состояний данных: в покое (at rest — на диске), в передаче (in transit — по сети) и в обработке (in use — в памяти, самое сложное). У каждого состояния свой инструмент, своя модель угроз и свой способ всё испортить. В этой статье — как шифровать осознанно: HTTPS/TLS, шифрование на уровне БД (TDE и поуровневое), и хранение паролей, где шифрование вообще почти всегда неверный ответ, а нужен хеш.
Стек примеров — LEMP (Nginx + PHP-FPM) и PostgreSQL/MySQL. Версии и параметры даны по состоянию на октябрь 2026 и помечены явно: криптография особенно любит устаревать, сверяйтесь при чтении.
Часть I. Три состояния данных и модель угроз
1. Карта состояний
| Состояние | Где | Чем защищаем | От кого защищаем | Чего НЕ защищаем |
|---|---|---|---|---|
| In transit | По сети между серверами | TLS (HTTPS) | Перехват (sniffing), MITM, подмена на канале | Тот, у кого уже есть доступ к конечной точке; сам контент в БД |
| In use | В RAM процесса | Мемшифрование / доверенные среды (TEE/SGX) | Холодная атака на RAM, дампы ядра | Обычный удалённый взлом приложения |
| In at rest | На диске / бэкапах | TDE, шифрование столбцов, шифрование файловой системы | Кража дисков, утечка бэкапов/снапшотов, физ. доступ | SQL-инъекция, скомпрометированный процесс (данные-то расшифрованы) |
Главная мысль, которую пропускают: каждый инструмент закрывает свой вектор, и все они слепы к чужим. TDE не поможет, если взломали приложение и оно само отдаёт расшифрованные строки (это, кстати, тот случай, когда защита на уровне приложения из статьи про SQL-инъекции важнее шифрования диска). HTTPS не спасёт, если сервера украли снапшот диска. Отсюда defense-in-depth: слои складываются, а не заменяют друг друга.
2. Что вообще шифровать, а что нет
Шифруют то, что имеет ценность при утечке и не нужно открытым для нормальных операций БД: персональные данные, платёжные токены, мед-/налоговые данные, API-ключи пользователей, секретные ответы.
Не шифруют (обычно) то, что нужно для запросов, индексов и логики: первичные ключи, FK, статусы, временные метки, содержимое постов (иначе CMS не сможет ни отображать, ни искать). Иначе вы платите производительностью и теряете простоту ради полей, которые не так страшны. Начните с классификации данных по чувствительности — это и определит, где нужен дорогой поуровневый механизм, а где хватит дешёвого TDE на всё.
Часть II. Шифрование в передаче: HTTPS и TLS
3. Версии: 1.3 — стандарт, 1.2 — совместимость, всё ниже — в помойку
TLS 1.3 (2018) — короткий handshake (1-RTT), убранные сломанные примитивы (RSA key transport, CBC-набор, SHA-1, статический DH), обязательный PFS. TLS 1.2 держим для старых клиентов, но без «наследия» ниже него. TLS 1.1/1.0 и SSLv3 — отключаем: они уже не соответствуют PCI DSS и ломаными местами (BEAST, POODLE, Lucky13 — лечатся только отказом).
Важное про профилы. Много лет эталон задавал Mozilla SSL Configuration Generator и его страница Server-Side TLS. На 2026 год это переехало на отдельный community-проект TLSRef: конфигуратор теперь на
tlsref.org/configurator(ssl-config.mozilla.orgредиректит туда), публичные гайды и список cipher suites — на docs.tlsref.org. Источники с «магическим списком шифров от Mozilla» старше 2025 — устарели. Ниже — два канонических профиля по-прежнему в терминах modern/intermediate.
4. Два профиля — Modern и Intermediate
- Modern — TLS 1.3 только. Максимальная безопасность, ломает старые клиенты (IE без 1.3, древние Android). Для новых API и контролируемых клиентов.
- Intermediate — TLS 1.2 + 1.3, AEAD-шифры, ECDHE, кривые без сюрпризов. Компромисс для публичного сайта, где трафик с чего угодно.
Для публичного вебсайта (ваш случай) обычно берут Intermediate:
# /etc/nginx/sites-available/shop — TLS (профиль Intermediate, TLSRef 2026)
server {
listen 443 ssl;
http2 on;
server_name shop.example.com;
ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; # никаких TLSv1/1.1/SSLv3
ssl_prefer_server_ciphers off; # для 1.2 даём клиенту выбрать (консенсус TLSRef)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; # TLS1.2-набор в одну строку: в nginx нет shell-continuation через '\'; 1.3 идёт своим списком
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1; # постквантовый гибрид + X25519 + P-256/P-384 для совместимости (TLSRef Intermediate)
ssl_session_tickets off; # PFS-совместимость (tickets переиспользуют ключ)
ssl_session_timeout 1d; # moderate ticket lifetime
ssl_stapling on; # OCSP stapling — меньше round-trip к CA
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/shop.example.com/chain.pem; # issuer+root; без него verify stapled-OCSP не работает
resolver 1.1.1.1 8.8.8.8 valid=300s; # для stapling
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
}
server {
listen 80;
server_name shop.example.com;
return 301 https://$host$request_uri; # HTTP только редирект
}
Пояснения, где обычно стреляют в ногу:
ssl_prefer_server_ciphers off— для TLS 1.2 это сознательный отказ от «сервер решает лучше клиента»: современные клиенты знают свои быстрые AEAD; server-priority чаще вредит (исторический сдвиг от старой практики). Для TLS 1.3 порядок шифров вообще не задаётся так.ssl_session_tickets off— сессионные tickets переиспользуют ключ, что ослабляет forward secrecy; TLSRef держит их выключенными.- OCSP stapling — сервер прикладывает заверенный ответ статуса сертификата, клиенту не надо отдельно дёргать CA (быстрее и приватнее).
5. HSTS и его ловушки
Strict-Transport-Security говорит браузеру: «к этому хосту всегда по HTTPS». Без него первый HTTP-запрос пользователя можно перехватить и не редиректить.
max-age— в секундах (63072000 ≈ 2 года — типовой компромисс TLSRef).includeSubDomains— и все поддомены (осторожно: если где-то есть поддомен только на HTTP, он станет недоступен).preload+ отправка в браузерный preload-список (hstspreload.org) — жёстко, обратно «выписаться» из Chrome/Firefox нельзя месяцами. Используйтеpreloadосознанно.- Ловушка: HSTS работает только после первого успешного HTTPS-визита и только в браузере. curl без
-L/--hsts, мобильные клиенты, боты — его не соблюдают. Поэтому редирект с 80 на 443 всё равно держим, а на канале, где это реально критично, думаем про Certificate Transparency и monitoring.
6. TLS 1.3, 0-RTT и HTTP/3
- 0-RTT (early data) ускоряет повторные соединения, но в nginx защита от повтора по умолчанию отключена (
ssl_early_dataотдаёт ранние данные в приложение без anti-replay — встроенную защиту OpenSSL nginx принудительно выключает): тот же ранний пакет, отправленный повторно на канале с MITM, дойдёт до приложения дважды. Для идемпотентных GET это обычно безопасно; для POST с побочными эффектами — включатьssl_early_dataнужно с идемпотентностью на уровне приложения или не включать вовсе. - HTTP/3 (QUIC) шифрует больше (в заголовках тоже), но TLS-часть — та же 1.3. Не «проще», а «другое»: проверяйте, что
listen 443 quic+ alt-svc настроены согласованно.
7. Mixed content и «сломанный замок»
Сервер настроили, а браузер ругается: страница по HTTPS тянет скрипт/картинку по http://. Это mixed content: пассивное (картинки) браузер обычно режет в новых браузерах, активное (JS) блокирует. Чинят на уровне приложения — относительные URL или https:// в ассетах, Content-Security-Policy: upgrade-insecure-requests. И проверяйте, что CMS не отдаёт абсолютные http://-ссылки из-за неверного siteurl/заголовка от прокси (типично для CDN/WAF перед origin).
Часть III. Шифрование в покое: данные в БД
8. Два принципиально разных уровня
Есть две несводимые друг к другу стратегии, и выбор между ними — архитектурный.
A. TDE / прозрачное шифрование (encryption at rest на уровне хранилища). Шифруются файлы данных, индексы, иногда WAL. Приложение и запросы не меняются — БД сама расшифровывает на лету для легитимного процесса. Защищает от кражи дисков/файлов/снапшотов/бэкапов на уровне ФС. НЕ защищает от SQL-инъекции, от дампа процесса (данные в RAM открыты) и от администратора БД с доступом.
B. Прикладное/поуровневое шифрование (в колонке, в приложении). Вы шифруете конкретные значения до записи — ключом, который есть у приложения, а не у БД. Это защищает и от того, кто просто украл/прочитал БД-файл, и от того, у кого есть SELECT * к таблице (он видит только ciphertext). Цена: ломается поиск, индексы и WHERE по шифрованному.
9. TDE: MySQL vs PostgreSQL — и вот где сюрприз 2026
- MySQL / MariaDB. InnoDB tablespace encryption — есть: в MySQL Enterprise из коробки (ключи в Keyring plugin/KMS), в MariaDB через
encrypt-key-management. Быстро ставится, но это всё ещё «защита от кражи файла, а не от взлома приложения». - PostgreSQL — TDE в ядро НЕ встроен, и в 2026 не встроен. В PG столетия идёт дискуссия; вместо патча в ядре появились расширения-обёртки через table access method:
- Percona
pg_tde(актуально v2.2.0, май 2026) — шифрует таблицы, индексы и WAL; keyring file или внешний KMS через «Global Key Provider»; ставится как пакеты для Percona Server for PostgreSQL 17/18 (дистрибутив-форк Percona), не шифрует временные файлы и системные таблицы/статистику. open_pg_tde(commandprompt) — access methodtde_heap, шифрует таблицы, индексы, TOAST и WAL; поддерживает upstream PostgreSQL 16/17/18 без форка.pg_vault_tde— access methodencrypted_heap, AES-256-GCM на кортеж, PG 17/18.
- Percona
Осторожно с обещаниями: даже TDE не шифрует всё.
pg_dumpи любые дампы — вне периметра любого TDE (это внешний клиент, шифруйте отдельно); данные вshared_buffersи в памяти процесса — всегда plaintext. У Percona pg_tde временные файлы и системные таблицы/статистика не шифруются. WAL шифруютpg_tdeиopen_pg_tde, но неpg_vault_tde(у него шифруются только кортежи). Лог slow queries — тоже plaintext. Это закрывает один конкретный вектор (утечку носителя), и надо честно говорить об этом.
10. pgcrypto: поуровневое шифрование в PostgreSQL
Для точечного шифрования конкретных колонок (без TDE) — pgcrypto:
CREATE EXTENSION pgcrypto;
-- симметричное шифрование (на passphrase, PGP-совместимо)
INSERT INTO secrets(id, data_enc)
VALUES (1, pgp_sym_encrypt('секретный токен', :'app_key'));
SELECT pgp_sym_decrypt(data_enc, :'app_key') FROM secrets WHERE id = 1;
Ключевые оговорки:
- passphrase в
pgp_sym_encrypt— не «безопасный ключ», если он попадает вpsql-лог или таблицу.:'app_key'— это переменная psql (клиентская подстановка): значение подставляется в текст запроса до отправки и может светиться в логах psql. В приложении используйте серверные prepared parameters, а ключ держите вне текста запроса (Vault/env, не литерал). pgp_sym_encryptиспользует устаревший по сути PGP-контейнер с сессионным ключом и MDC; работает, но если хотите современный AEAD — шифруйте в приложении (Part II кода ниже) и храните какbytea.- Шифрование в БД закрывает от того, кто читает файл/таблицу напрямую, но тот, у кого есть
SELECTи доступ к ключу (приложение) — всё равно расшифрует.
11. Правильный примитив в приложении: AEAD
Когда шифруете в PHP/приложении — берите AEAD (шифр с аутентификацией), а не CBC «голый» (без MAC он даёт padding-oracle/ciphertext malleability):
XSalsa20-Poly1305(дефолтный примитивsodium_crypto_secretbox) — безопасный по умолчанию, 24-байтовый (192-битный) случайный nonce без страха коллизий в объёме. Это не XChaCha20: тот вызывается отдельно (sodium_crypto_secretbox_xchacha20poly1305) и тоже с 24-байтовым nonce.AES-256-GCM(sodium_crypto_aead_aes256gcm_*) — быстрее на AES-NI, но nonce требует дисциплины (96-бит, никогда не повторять с той же ключ-парой).
<?php
// XSalsa20-Poly1305 (secretbox): аутентифицированное шифрование, безопасно по умолчанию.
$key = sodium_crypto_secretbox_keygen(); // 32 байта; в проде берём из vault/секрета, НЕ генерим на месте
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 24 байта, unique на запись
function enc(string $plaintext, string $key): string {
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ct = sodium_crypto_secretbox($plaintext, $nonce, $key);
return $nonce . $ct; // сохраняем nonce рядом с ciphertext
}
function dec(string $blob, string $key): string {
$nl = SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
$nonce = substr($blob, 0, $nl);
$ct = substr($blob, $nl);
$pt = sodium_crypto_secretbox_open($ct, $nonce, $key);
if ($pt === false) {
throw new RuntimeException('MAC mismatch — данные подделаны или биты повреждены');
}
return $pt;
}
// в БД храним blob как bytea/BLOB; nonce хранится рядом с ciphertext (это не секрет).Что важно не испортить:
random_bytes, никогда неmt_randдля nonce/ключей.- nonce — не секрет, но уникален на пару ключ+сообщение (для secretbox 24-байтовый случайный — коллизии реально маловероятны до 2^96 записей).
sodium_crypto_secretbox_open()возвращаетfalseна провале MAC — проверяйте строгоif ($pt === false)(неempty()и не нестрогое==): валидный пустой plaintext обязан проходить, аfalseозначает только криптографический отказ (повреждение/подделка).- Ключ — из внешнего хранилища (см. Часть V), не хардкод.
12. Поиск по шифрованному: blind index и детерминированное шифрование
Классическая боль: поле email зашифровано, и WHERE email='...' не работает (шифр недетерминирован — каждый раз другой ciphertext). Решения:
- Blind index — отдельный столбец с
HMAC(email, search_key). Искомое значение → тот же HMAC →WHERE email_bidx = hmac(:v). Equality-поиск работает, содержимое не видно без ключа. Риск: по биндексу при утечке и слабом ключевом слове можно офлайн-подбирать значения (это отдельный ключ, НЕ тот же, что для шифрования; и длинные значения ок). - Детерминированное шифрование (same plaintext → same ciphertext) — единственный корректный примитив для этого: AES-SIV (abuse-resistant deterministic AEAD; в PHP/libsodium готового SIV нет — его берут в Go/Rust или через статичный nonce у GCM, что хрупко из-за риска reuse и вообще не рекомендуется). Позволяет поиск и группировку, но протекает равенство (два одинаковых значения дают одинаковый ciphertext) — утечка частоты/корреляции. Только когда equality реально нужен и значения высоко-кардинальные.
- Метод «шифрование с сохранением формата» и «searchable encryption» — экзотика, редко оправдана в вебе.
Практика: обычно хешируют (blind index) для поиска, шифруют AEAD для содержимого — два разных ключа, две разные цели.
Часть IV. Пароли: хеш, а не шифр
13. Почему не шифрование
OWASP формулирует жёстко: пароли никогда не хранят в plaintext и не шифруют reversible-шифром. Причина: хеш — односторонняя функция, «расшифровать» её нельзя (только гадать и сверять); шифр — двусторонний, и при компрометации ключа/сервера шифр всех паролей разом превращается в plaintext. Исключение-заглушка — редкий кейс, когда нужен сам plaintext для передачи третьей системе (OIDC legacy); тогда меняют архитектуру, а не шифруют пароли.
Три термина, которые путают: шифрование (обратимо, есть ключ) vs хеширование (необратимо) vs KDF/медленный хеш (специально тормозит подбор). Пароли — третья история.
14. Алгоритмы и параметры (OWASP Password Storage, 2026)
Приоритет для новых систем — медленные, память-жадные хеши. Числа берут прямо из рекомендаций:
| Алгоритм | Минимум параметров (OWASP) | Когда |
|---|---|---|
| Argon2id | m=19 MiB, t=2, p=1 (или m=47 MiB/t=1; 12 MiB/t=3; 9 MiB/t=4; 7 MiB/t=5) | новый стандарт по умолчанию |
| scrypt | N=2^17 (128 MiB), r=8, p=1 (эквиваленты 2^16/p2 и т.д.) | если Argon2id нет |
| bcrypt | work factor ≥ 10, лимит входа 72 байта | legacy-системы, где ничего нет |
| PBKDF2-HMAC-SHA256 | 600 000 итераций | нужен FIPS-140 / комплаенс |
Зачем «память»: GPU/ASIC быстро считают быстрые хеши (SHA-256 — не для паролей, миллионы за секунду). Память-жёсткие Argon2/scrypt ломают массовый параллельный подбор. Рабочий фактор повышают со временем (железо дорожает → пересчёт дороже), пере-хешируя при следующем входе (см. password_needs_rehash).
15. PHP: правильный цикл жизни пароля
<?php
// Хранение: Argon2id с явными параметрами (OWASP-минимум). Дефолт PHP задавать явно надёжнее.
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 19456, // 19 MiB
'time_cost' => 2,
'threads' => 1,
]);
// bcrypt как запасной вариант (без параметров выше — дефолт cost=10; минимум work factor 10):
// $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// Проверка (константный срав внутри, без вашего timing-гварда):
if (password_verify($input, $storedHash)) {
// Успех. Миграция старого work factor / старого алгоритма на лету:
if (password_needs_rehash($storedHash, PASSWORD_ARGON2ID, [
'memory_cost' => 19456, 'time_cost' => 2, 'threads' => 1])) {
$new = password_hash($input, PASSWORD_ARGON2ID, ['memory_cost'=>19456,'time_cost'=>2,'threads'=>1]);
// обновить хеш в БД
}
}
// (пароль в память после использования лучше затирать, а логировать — никогда)Что важно:
- никогда не
md5/sha1/sha256для пароля — эти функции быстрые и без памяти: идеальная мишень для GPU-подбора (и классический источник дампов, которые «берут» за часы). password_verifyсам делает константное сравнение и корректно обрабатывает salt/alg из формата хеша.- bcrypt имеет лимит 72 байта входа (PHP его тихо обрезает) — для длинных passphrase/unicode это ловушка; тогда Argon2/scrypt.
- Не делать «pre-hash + bcrypt» наивно: есть password shucking (проверяют, что
bcrypt(base64(H(pw))) == bcrypt(base64(утёкший_H))). Если вынуждены pre-hash — HMAC с pepper, а не голый SHA.
16. Pepper и соль
- Соль — уникальна на пароль, генерит сама библиотека; нужна, чтобы (а) убить rainbow tables, (б) два одинаковых пароля дали разные хеши, (в) ломать пришлось каждый хеш отдельно. Вы её не трогаете — делает
password_hash. - Pepper — секретный общий «секрет», который добавляют к паролю до/после хеширования и хранят НЕ в БД (env/Vault/HSM). При утечке только дампа БД без пеппера хеши неподносят: атакующему мало таблицы, нужен ещё и секрет приложения. Post-hashing pepper =
HMAC(hash, pepper). Ограничение OWASP: pepper сам по себе не даёт безопасности «в вакууме» (нужен всё равно медленный хеш) и не ротится незаметно — потеря/смена пеппера = сброс паролей всех затронутых пользователей.
17. Миграция с legacy-хешей
Если исторически хранили md5/sha1: не переписывайте хеши задним числом (plaintext вы не знаете). Варианты (оба из OWASP):
- При следующем входе пользователя пере-хешировать plaintext в Argon2id и удалить старый хеш; для неактивных — экспайрить и требовать сброс.
- Слоить без знания пароля:
bcrypt(md5($password))на лету для существующих записей (быстрее/менее строго, чем чистый bcrypt), и заменить на прямой хеш при входе. Помнить про shucking (см. выше) при layering.
Формат хеша со временем меняют на PHC string format ($argon2id$v=19$m=…,t=…,p=…$salt$hash) — тогда алгоритм и work factor лежат в самой строке и миграция становится «нормальной» операцией.
Часть V. Ключи: где они живут и как их не потерять
18. Иерархия и envelope encryption
Главный закон: данные шифрует DEK (data encryption key), а DEK шифрует KEK (key encryption key) из внешнего хранилища. Это envelope encryption — основа KMS.
plaintext --DEK--> ciphertext (DEK в RAM приложения, короткая жизнь)
DEK --KEK--> wrapped DEK (хранится рядом с ciphertext в БД)
KEK — в KMS/HSM/Vault, наружу НЕ выходит
Зачем: ротация KEK не требует пере-шифровки терабайт данных — пере-заворачивают только DEK. А утечка БД даёт атакующему ciphertext + wrapped DEK, без KEK бесполезные.
19. Где хранить ключи (от худшего к лучшему)
- ❌ в коде / git / docker-образе /
wp-config.phpбез защиты — «секрет» доступен всем, кто читает репу/образ/бэкап конфига. - ⚠️ в env-переменных — лучше, чем в коде, но env видно в процессах, дампах,
ps e, иногда в логах/traceback. - ✅ в Vault / KMS / Secrets Manager (HashiCorp Vault, AWS KMS, GCP KMS, Azure Key Vault) — ключ наружу не отдаётся, API просит «зашифруй этим», аудит и ротация встроены.
- ✅ HSM для самых дорогих ключей/подписи (аппаратный, физическая защита).
20. Ротация, отзыв, и — обязательно — бэкап
- Ротация KEK — регулярная и по инциденту (утёк env → ротируем всё, чем этот env шифровал).
- Отзыв/compromise — если есть основания, что DEK/KEK скомпрометированы: ротация KEK + пере-шифровка данных под новым DEK для чувствительного, и сброс/пересоздание паролей при утечке пеппера.
- Бэкап ключей — скучный пункт, который рушит всё: потеря ключа = данные потеряны навсегда (именно поэтому «забыли мастер-пароль vault = все данные зашифрованы в null»). Держите запасные ключи шифрования отдельно от данных (offline, split knowledge), и тестируйте восстановление. Бэкап ключа рядом с бэкапом данных (в одном шифре/хранилище) — это не бэкап, а единая точка отказа.
21. CSPRNG — единственный источник случайности
Любой ключ, nonce, salt, pepper, токен восстановления: только криптографически стойкий ГПСЧ — random_bytes() (PHP), getrandom(2)//dev/urandom (Linux), crypto/rand (Go). Никогда rand(), mt_rand(), uniqid(), время, random.random() — предсказуемый ГПСЧ превращает «шифрование» в «легко угадать ключ».
Часть VI. WordPress: реальность шифрования
- Пароли. С WP 6.8
wp_hash_password()— это нативный PHPpassword_hash(PASSWORD_BCRYPT)с pre-hashhash_hmac('sha384', pw, 'wp-sha384')(сохраняет энтропию паролей >72 байт) и префиксом$wpв хеш-строке; алгоритм и опции меняются фильтрамиwp_hash_password_algorithm/wp_hash_password_options(можно поставить Argon2id). Старые хеши формата$P$— это legacy PHPass (MD5-based), которые WP умеет проверять и прозрачно мигрирует на bcrypt при входе — но сам bcrypt в WP уже не PHPass. Свой хешpassword_hash/Argon2 — при кастоме. Важно: в статье про аутентификацию мы разбиралиwp_check_password/Application Passwords — это про проверку, тут — про то, что реально лежит вwp_users.user_pass. - Шифрование в БД. У WP нет ни TDE, ни шифрования полей по умолчанию.
wp_optionsс секретами (API-ключи плагинов),usermeta— plaintext в таблице. Хотите — на уровне приложения в плагине/своей обёртке, или TDE-расширением PG/MySQL поверх (оно покроет и строки WP, но только «at rest»). Не покупайте «WP Encryption plugin» как «защиту от взлома» — большинство шифруют ключом изwp-config/плагина, т.е. скомпрометированный сайт = скомпрометированный ключ. - Секреты.
DB_PASSWORD,AUTH_KEY/SECURE_AUTH_KEY/LOGGED_IN_KEY/NONCE_KEY(+salt) — это не пароли, а симметричные ключи для signed cookies и крипто WP. Утечка AUTH_KEY-набора = подделка авторизационных cookie (см. XSS/CSRF-статью). Держите уникальными, изwp-cli/ENV, ротируйте (тогда разлогинятся все сессии — штатно). Хранить их в git-репозитории = не хранить. - HTTPS в WP. Ставим не только сертификат, но и правим
siteurl/homeнаhttps://,force_ssl_admin, чтобы CMS не отдавалаhttp://-ассеты (mixed content) и не ломала canonical. Если за CDN/WAF — корректно пробрасыватьX-Forwarded-Proto, иначе WP не поймёт, что клиент на HTTPS. - Бэкапы. WP-бэкапы часто утекают (открытые
backup.zipна сервере, «утёкший» плагин резервных копий). Если БД с PII зашифрована TDE-файлом, ноmysqldumpбэкапа — открытый plaintext, шифрование диска ничего не дало: шифруйте бэкапы отдельно и ключи от них держите в другом месте.
Часть VII. Чек-лист
Передача (HTTPS)
- TLS 1.2 + 1.3; TLS 1.0/1.1/SSLv3 отключены.
- Профиль по TLSRef (Intermediate для публичного, Modern для API).
- HSTS (продуманный max-age, includeSubDomains только если поддомены все на HTTPS).
- OCSP stapling; сессионные tickets off; 0-RTT только с идемпотентностью.
- Нет mixed content; корректный `X-Forwarded-Proto` за прокси.
- Автоматическое обновление сертификатов (ACME), мониторинг истечения.
Покой (БД)
- Классифицированы поля по чувствительности (что реально шифровать).
- Понятно, какой уровень: TDE (защита носителя) или поуровневое (защита и от SELECT).
- TDE ключи — во внешнем KMS/Keyring, а не рядом с файлами БД.
- В PostgreSQL учтено, что TDE — это расширение (pg_tde/open_pg_tde), а не кнопка.
- Поуровневое шифрование — AEAD (ChaCha20-Poly1305/AES-GCM), nonce уникальный, MAC проверяется.
- Для поиска — blind index отдельным ключом; не смешивать ключ шифрования и ключ поиска.
Пароли
- Argon2id (или scrypt; bcrypt/PBKDF2 — только обоснованно), параметры не ниже OWASP-минимума.
- Соль генерит библиотека; pepper — если нужен — во внешнем хранилище и учтена невозможность ротации.
- Миграция legacy-хешей при входе; PHC-формат.
- Никаких md5/sha/reversible-шифра паролей; bcrypt 72-байтовый лимит учтён.
Ключи
- Все случайности — из CSPRNG.
- Envelope/KMS: DEK шифрует данные, KEK шифрует DEK, KEK не покидает хранилище.
- Ротация KEK плановая и по инциденту.
- Бэкап ключей существует, отделён от данных и **проверен восстановлением**.
WordPress
- `AUTH_KEY`/`*_SALT`/`DB_PASSWORD` уникальны, вне git, ротируются осознанно.
- siteurl/home на HTTPS, force_ssl_admin, нет утечки http-ассетов.
- Секреты плагинов и `usermeta` не считаются «шифрованными сами по себе».
- Бэкапы шифруются отдельно от диска с живой БД.
Гигиена
- Шифрование не подменяет исправление уязвимостей (TDE ≠ «SQLi можно не чинить»).
- Модель угроз записана: что, от кого и чем защищено; где дырка (in use / админ БД).
- Нет секретов/паролей в audit/доступ-логах (в [WAF-статье](/waf-i-modsecurity-ustanovka-nastrojka-pravila/) audit-log тела запросов — это чувствительные данные, ограничьте доступ).
Источники
- OWASP Password Storage Cheat Sheet — параметры Argon2id/scrypt/bcrypt/PBKDF2, pepper, shucking, миграция — cheatsheetseries.owasp.org/…/Password_Storage_Cheat_Sheet
- OWASP Cryptographic Storage Cheat Sheet — cheatsheetseries.owasp.org
- TLSRef (продолжение Mozilla Server-Side TLS / SSL Config Generator) — профили и конфиги: docs.tlsref.org · tlsref.org/configurator
- PostgreSQL TDE расширения: Percona pg_tde v2.2.0 — docs.percona.com/pg-tde · open_pg_tde (PG 16/17/18, tde_heap) — github.com/commandprompt/open_pg_tde · pg_vault_tde — postgresql.org/about/news
- libsodium (PHP:
sodium_crypto_secretbox, AEAD) — libsodium.org · PHP manual security - Argon2 / RFC 9106 (параметры, бенчмарк) — rfc-editor.org/rfc/rfc9106
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
