Data encryption deep dive: TLS in-transit + mTLS east-west, envelope encryption (DEK wrapped by KMS/HSM CMK), TDE + field-level encryption for PII, E2EE Signal-style (X3DH + Double Ratchet), and an ADR on when E2EE is worth its UX cost.
Любая данная в твоей системе живёт в трёх состояниях: на проводе (между сервисами), на диске (БД, объектное хранилище, бекапы) и в памяти (пока процесс её считает). Если хоть одно из них незащищено, остальные защиты — театр.
История взломов это подтверждает. Adobe в 2013 положили 153M паролей с MD5 без соли — взломали тривиально. Equifax 2017: TLS-inspection сертификат протух на 19 месяцев, traffic перестал инспектироваться, атакующие сидели внутри 76 дней. Capital One 2019: PII в S3 не зашифрован на field-level, SSRF дал credentials, утекли все клиенты. Heartbleed 2014: одна OpenSSL-бага — и любой процесс с уязвимой версией отдавал по 64KB памяти на запрос.
Encryption даёт несколько ортогональных гарантий:
Чем выше уровень — тем сложнее реализация, тем больше операционных компромиссов (нет search, нет admin recovery, нет server-side analytics). Поэтому вопрос «что шифровать и насколько глубоко» — не технический, а threat-modeling.
Базовая троица: at rest / in transit / in use.
Поверх — иерархия ключей envelope encryption:
[Master KEK / CMK] в HSM, никогда не покидает hardware
wraps
[DEK] один на блок данных, ephemeral, в RAM
encrypts
[actual data] лежит зашифрованным в S3/DB/disk
Master ключ никогда не касается твоего кода. Ты просишь KMS «сгенерируй мне DEK» — он отдаёт пару {plaintext_DEK, encrypted_DEK}. Plaintext-версию используешь для AES-GCM локально и сразу затираешь нулями. Encrypted-версию складываешь рядом с ciphertext. Чтобы расшифровать — снова идёшь в KMS с encrypted_DEK, он возвращает plaintext. Ротация мастер-ключа = re-wrap всех DEK (дёшево), а не re-encrypt 50TB файлов (дорого).
Алгоритмический минимум на 2026:
И главный закон: не пиши свою криптографию. libsodium, ring, Tink, BoringSSL — это люди, которые думают об этом 10 лет, и они всё равно регулярно находят баги.
Топология собрана так, чтобы каждый из четырёх сценариев проходил через свой кусок системы, но все они делят одну инфраструктуру — это ближе к реальной картине, чем рисовать четыре независимые диаграммы.
browser (типичный HTTPS-клиент) и пара sender/recipient (peer-ы для E2EE-сценария).lb (TLS 1.3 терминируется тут) и mtls (sidecar, который пересоздаёт mTLS-туннель уже внутри LAN). Это граница «снаружи / внутри».api (бизнес-логика), crypto (модуль с AEAD-операциями), memzero (затирание DEK после использования). Crypto вынесен отдельной нодой намеренно — это единственное место, где появляется plaintext DEK.kms (audit-log, политики, API), hsm (физический модуль с FIPS 140-2 L3) и cmk (master-ключ внутри HSM). Стрелка kms → hsm → cmk показывает, что приватная операция всегда «offload» в железо.disk (LUKS/EBS, blocklevel), db (Postgres с TDE, pagelevel), s3 (объектное хранилище с per-object envelope). Три разных at-rest стратегии в одной картинке.Edges — это физические маршруты данных, не направление крипты:
browser → lb → mtls → api — основной запрос пользователя. На browser↔lb работает TLS 1.3, на lb↔mtls — короткий plaintext-кусок внутри edge box, на mtls↔api — mTLS заново (zero-trust east-west).api → crypto → {disk, db, s3} — application-side шифрование перед записью.api → kms → hsm → cmk — иерархия запросов к мастер-ключу.sender ↔ api ↔ recipient — путь E2EE-сообщения через server-as-dumb-pipe.Цветовая разметка групп — не декоративная: синий (client) → зелёный (edge) → оранжевый (app) → фиолетовый (KMS, root of trust) → голубой (storage). По сценариям видно, как trust boundary последовательно сужается: для E2EE она кончается на самом sender-е.
В скрипте пять сценариев. Первые четыре — техника, пятый — ADR.
1. In-transit (TLS + mTLS). Browser шлёт POST, на LB происходит TLS 1.3 handshake (1-RTT, ECDHE X25519, AES-256-GCM, ALPN=h2). LB проверяет CN/SAN против OS trust store, открывает AEAD-туннель. Внутри edge box трафик короткое время живёт plaintext, затем sidecar обворачивает его в mTLS — обе стороны проверяют сертификаты от internal PKI. На api приходит запрос, ответ идёт обратно тем же encrypted tunnel. Защищает от пассивного снифа, MITM, downgrade-атак. Не защищает от compromised endpoint и malicious app — TLS заканчивается на LB, дальше уже твоя ответственность.
2. Envelope encryption (S3 upload). API получает 50MB файл от клиента. Идёт в KMS GenerateDataKey(spec=AES_256, KeyId=arn:cmk:prod-orders). KMS делегирует HSM сгенерировать 256-битный DEK и обернуть его CMK (AES-KW). HSM никогда не выпускает CMK наружу. KMS возвращает API {Plaintext: DEK, CiphertextBlob: encrypted_DEK}. API передаёт DEK crypto-модулю, тот делает AES-256-GCM(file, DEK, nonce=random_96bit) → ciphertext + tag. API кладёт в S3 body=ciphertext, metadata: x-amz-encrypted-dek=base64(encrypted_DEK). Crypto-модуль вызывает memzero (sodium_memzero / explicit_bzero) — DEK перезаписывается нулями в RAM. Чтобы расшифровать обратно, нужно KMS.Decrypt(encrypted_DEK) → DEK → AES-GCM decrypt. Win: ротация CMK = re-wrap всех DEKs (миллисекунды), а не re-encrypt 50MB файлов (часы и пропускная способность сети).
3. TDE + field-level (DB). INSERT users(id, email, ssn, note). Crypto-модуль делает per-column шифрование: ssn → AES-GCM(ssn, DEK_pii) (рандомизированное, не searchable), email → AES-SIV(email, DEK_pii) (детерминированное, searchable, но слабее). DEK_pii получен из KMS и кешируется в RAM на 5 минут. INSERT уходит в Postgres с уже-зашифрованными колонками. Postgres перед flush на диск шифрует DB pages в AES-256-XTS (TDE на data files + WAL). Результат: на диске лежат encrypted pages, внутри них — encrypted columns. Три атаки и ответы: (1) DBA читает таблицу — видит note="VIP", email_enc=base64..., ssn_enc=base64..., PII закрыт; (2) SQL injection — тот же ciphertext, бесполезен без KMS access; (3) украли backup tape — TDE спасает, pages encrypted. Tradeoff: WHERE ssn LIKE '123%' не работает; для email применили deterministic encryption (можно equality search, но слабее против frequency analysis); альтернатива — blind index HMAC(email, secret) отдельной колонкой.
4. E2EE (Signal X3DH + Double Ratchet). Peer A локально генерит identity key + signed prekey + 100 one-time prekeys (X25519), публикует public части на сервере (private никогда не покидают device). B хочет писать A — забирает bundle. Считает shared secret через X3DH: KDF(DH(IK_B, SPK_A) || DH(EK_B, IK_A) || DH(EK_B, SPK_A) || DH(EK_B, OPK_A)). Оба peer независимо выводят одинаковый root_key. message_key = HKDF(root_key, ratchet); ciphertext = AES-GCM(plaintext, message_key). Сервер видит {from, to, opaque_blob} и расшифровать не может. Forward secrecy: после ratchet старые ключи стираются. Подписка / subpoena: сервер отдаёт blobs — useless без device key. Server pwned: дамп БД даёт только blobs. Цена: нет server-side search, нет account recovery без backup, multi-device sync через sealed sender + sender keys — сложно.
5. ADR: когда E2EE стоит UX cost. Контекст: server-side encryption (envelope + TDE + TLS) уже закрывает ~90% threats. Compliance (HIPAA, GDPR) требует at-rest+in-transit, но не требует E2EE. Option A — server-side only: search/sort/aggregate работают, recovery простой, backup штатный, cost низкий. Option B — E2EE: даже мы не видим данные, нужны device-side ключи, сервер становится «dumb pipe». Costs E2EE: нет server-side search (encrypted indexes / ORE / CSFLE слабее), нет admin recovery, multi-device sync сложен, backup либо sealed (user passphrase), либо отказ от E2EE по факту, нет anti-abuse / spam / malware scan на ciphertext. Decision: E2EE применяем только когда threat model явно включает «сервер атакован или принуждён» — messengers, password managers, health records, journalist tools, financial keys. Не применяем — e-commerce orders, blog posts, SaaS analytics, internal tooling. Consequence: выбрали envelope + TDE + TLS + field-level для PII; E2EE отдельным opt-in модулем для secret-sharing. Revisit: каждые 12 месяцев, или сразу при появлении subpoena risk / законов об обязательной выдаче.
| Решение | За | Против |
|---|---|---|
| Только TLS (in transit) | минимум кода, прозрачно для приложения | украденный диск = всё в открытом |
| + Disk encryption (LUKS/EBS) | transparent, default ON в облаке | running app, SQL injection, DBA видят plaintext |
| + TDE (DB-level) | защищает backup, WAL, data files | live query всё ещё отдаёт plaintext |
| + Field-level (per-column) | защищает от DBA, SQLi, leaked backup | WHERE col LIKE ломается, нужны blind indexes / deterministic enc |
| + E2EE (client-side) | даже compromise сервера = data safe | нет search, нет recovery, multi-device pain, нет server-side moderation |
| AES-256-GCM | AEAD, быстро, стандартизировано | nonce reuse = катастрофа (key recovery) |
| ChaCha20-Poly1305 | software-fast, mobile-friendly, нет timing-side-channel | реже встречается в legacy HW-acceleration |
| Symmetric (AES) | быстро, малые ключи | нужен secure channel чтобы обменяться ключом |
| Asymmetric (X25519/Ed25519) | можно публиковать public ключ | медленнее на 2-3 порядка, используется только для key exchange + signatures |
| Envelope encryption | дёшево ротировать CMK, KMS видит только tiny DEK | extra round-trip к KMS при каждом decrypt (mitigate: DEK caching с TTL) |
| Detached MAC (CBC + HMAC) | гибкость, можно AES-CBC | две операции, риск ошибки порядка → padding oracle |
| AEAD (GCM/Poly1305) | один примитив, harder to misuse | nonce management всё ещё на тебе |
| Postgres pgcrypto | в БД, легко начать | ключи часто оказываются в SQL → в логах |
| App-level + KMS | ключи никогда не касаются БД | больше кода, нужен envelope pattern |
| HSM (CloudHSM, YubiHSM) | FIPS 140-3, ключи в железе | $$$, ops complexity, vendor lock |
| Software KMS (Vault Transit) | self-hosted, бесплатно | ты владеешь backup, ротацией, audit |
GenerateDataKey / Decrypt).Лишний слой шифрования бесплатным не бывает. Не добавляй уровни, если threat model их не требует.
localStorage браузера для «секретов» — ключ всё равно в JS, всё равно XSS = game over.tls-handshake (что под капотом TLS 1.3), secrets-management (где жить ключам и токенам приложения).