Шифрование данных: HTTPS, шифрование в БД, хранение паролей

Шифрование — не одна кнопка, а три разные задачи для трёх разных состояний данных: в покое (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 method tde_heap, шифрует таблицы, индексы, TOAST и WAL; поддерживает upstream PostgreSQL 16/17/18 без форка.
      • pg_vault_tde — access method encrypted_heap, AES-256-GCM на кортеж, PG 17/18.
      Практический вывод для статьи/прода: «у нас PostgreSQL, поставим TDE одной кнопкой» — неверно. Нужен выбор расширения (все — сторонние, зрелость разная), пересоздание таблиц с новым access method и внешнее хранение ключей. Это не «включили», а «спроектировали».

    Осторожно с обещаниями: даже 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
    <?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)Когда
    Argon2idm=19 MiB, t=2, p=1 (или m=47 MiB/t=1; 12 MiB/t=3; 9 MiB/t=4; 7 MiB/t=5)новый стандарт по умолчанию
    scryptN=2^17 (128 MiB), r=8, p=1 (эквиваленты 2^16/p2 и т.д.)если Argon2id нет
    bcryptwork factor ≥ 10, лимит входа 72 байтаlegacy-системы, где ничего нет
    PBKDF2-HMAC-SHA256600 000 итерацийнужен FIPS-140 / комплаенс

    Зачем «память»: GPU/ASIC быстро считают быстрые хеши (SHA-256 — не для паролей, миллионы за секунду). Память-жёсткие Argon2/scrypt ломают массовый параллельный подбор. Рабочий фактор повышают со временем (железо дорожает → пересчёт дороже), пере-хешируя при следующем входе (см. password_needs_rehash).

    15. PHP: правильный цикл жизни пароля

    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):

    1. При следующем входе пользователя пере-хешировать plaintext в Argon2id и удалить старый хеш; для неактивных — экспайрить и требовать сброс.
    2. Слоить без знания пароля: 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() — это нативный PHP password_hash(PASSWORD_BCRYPT) с pre-hash hash_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 тела запросов — это чувствительные данные, ограничьте доступ).

    Источники

    👤 Зарегистрируйтесь, чтобы добавить материал в закладки.