Уровни защиты шифрования данных в веб-приложениях
HTTPS шифрует канал между браузером и сервером и не защищает сессию, если cookie с идентификатором уходит без флага Secure. Браузер отправит такую cookie по HTTP при заходе на http-версию сайта, и именно на этом участке она читается открытым текстом. Вторая частая дыра: сервер не объявил HSTS, поэтому первый запрос пользователя идёт по HTTP, где атакующий подменяет ответ и переводит жертву на незашифрованное соединение (SSL stripping).
Защита данных в веб-приложении складывается из пяти слоёв:
- Транспортное шифрование. TLS 1.2 и TLS 1.3 в составе HTTPS с наборами шифров на базе ECDHE и AES-GCM или ChaCha20-Poly1305.
- Политика транспорта. Заголовок HSTS (Strict-Transport-Security), который заставляет браузер обращаться к домену только по HTTPS.
- Защита cookies. Флаги Secure и HttpOnly плюс атрибут SameSite.
- Контроль смешанного контента. Директива CSP upgrade-insecure-requests и отказ от HTTP-ресурсов на HTTPS-странице.
- Шифрование самих данных. На стороне сервера (диски, резервные копии, отдельные поля БД) и на стороне клиента через Web Crypto API.
Первые четыре слоя настраиваются в Nginx и Apache и не требуют правок кода приложения. Пятый слой делится на серверное шифрование, где ключами управляет администратор, и клиентское, при котором открытый текст не покидает браузер.
Дальше: выпуск и автопродление сертификатов Let's Encrypt, конфигурации Nginx и Apache с TLS 1.3, включение HSTS без риска заблокировать сайт, установка флагов Secure и HttpOnly, поиск смешанного контента и примеры шифрования в браузере.
Настройка HTTPS с современными шифрами: пошаговое руководство
Let's Encrypt выдаёт бесплатные TLS-сертификаты на 90 дней с автоматическим продлением. Файлы складываются в каталог /etc/letsencrypt/live/<домен>/, где fullchain.pem и privkey.pem остаются символическими ссылками на версии с меткой времени. В конфигурациях веб-сервера указывайте пути до симлинков: при обновлении сами файлы меняются, а ссылки нет, поэтому перезагрузка конфигурации подхватит новый сертификат без правок.
Выпуск сертификата Let's Encrypt через certbot и acme.sh
Самый предсказуемый способ проверки владения доменом - webroot: ACME-клиент кладёт файл в каталог /.well-known/acme-challenge, а веб-сервер отдаёт его валидатору. Для Nginx команда выглядит так:
certbot certonly --webroot --agree-tos --no-eff-email \ --email admin@example.com \ --webroot-path /usr/share/nginx/html/ \ -d example.com -d www.example.com
В конфигурации Nginx для этого каталога нужен отдельный блок, иначе запросы валидатора уйдут в приложение:
location ^~ /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
default_type "text/plain";
}
После правки конфигурации примените изменения командой nginx -t && nginx -s reload. Для Apache путь проверки обычно /var/www/html/, а сервис перезапускается через systemctl restart httpd или systemctl restart apache2.
Если домен не должен светить IP сервера или порт 80 закрыт, используйте DNS-валидацию: certbot certonly --manual --agree-tos --email admin@example.com --preferred-challenges=dns -d example.com -d www.example.com. Клиент попросит создать TXT-запись с именем _acme-challenge.example.com и покажет её значение. Домен третьего уровня и выше должен проходить проверку на всех уровнях, то есть для sub.example.com запись создаётся по полному имени.
acme.sh закрывает те же задачи и поддерживает DNS API провайдеров, поэтому продление проходит без ручного создания TXT-записей:
acme.sh --issue -d example.com -d www.example.com --webroot /usr/share/nginx/html export CA_EMAIL=admin@example.com acme.sh --issue --dns dns_cf -d example.com
Автоматическое продление ставится в cron. Расписание по понедельникам и четвергам в 00:00 даёт запас времени, если один запуск сорвётся:
0 0 * * 1,4 /usr/bin/certbot renew --noninteractive --deploy-hook "systemctl reload nginx"
certbot продлевает только те сертификаты, до истечения которых осталось менее 30 дней, и обновляет симлинки. Практический нюанс: не ждите 30 дней, продление можно запускать уже через 60 дней после выпуска, тогда окно на исправление ошибок шире. Сервис, который держит сертификат в памяти, получит новый файл только после перезапуска, для этого и нужен deploy-hook вида deploy-hook = systemctl reload nginx в файле обновления.
Использование win-acme для Windows и Docker для автоматизации
На Windows сертификаты выпускает win-acme (wacs.exe). Утилита создаёт каталог проверки, получает сертификат, добавляет привязку на порт 443 в IIS и заводит задание автопродления в планировщике заданий. Для выгрузки в PEM-формат, который понимает Nginx или другой сервис:
wacs.exe --store pemfiles --pemfilespath C:\Certificates --accepttos --emailaddress admin@example.com --host example.com
Если своего веб-сервера нет, а есть Docker, certbot запускается контейнером со встроенным веб-сервером на 80 порту:
docker run -it --rm -p 80:80 \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ certbot/certbot certonly --standalone -d example.com -d www.example.com
Облачную площадку под такой сервер удобно брать готовой: Timeweb Cloud даёт серверы, VDS/VPS, базы данных, хранилище и Kubernetes, а ресурсы меняются по мере роста нагрузки. Это снимает возню с железом и оставляет время на саму настройку TLS.
Конфигурация Nginx и Apache для современных шифров
Сертификат получен, теперь ограничиваем протоколы и шифры. Для Nginx:
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
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;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
Для Apache в файле виртуального хоста:
ServerName example.com SSLEngine on SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem SSLProtocol -all +TLSv1.2 +TLSv1.3 SSLCipherSuite 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 SSLHonorCipherOrder on SSLCompression off SSLSessionTickets off SSLUseStapling on SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
Что реально дают эти строки:
| Протокол | Обмен ключами | Симметричный шифр | Комментарий |
|---|---|---|---|
| TLS 1.3 | ECDHE (X25519, P-256) | AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 | Наборы зафиксированы стандартом, порядок задаётся отдельной директивой |
| TLS 1.2 | ECDHE | AES-GCM, ChaCha20-Poly1305 | Работает при ssl_prefer_server_ciphers on |
| TLS 1.1 и 1.0 | RSA, DHE | CBC-режимы | Отключены в актуальных браузерах |
| SSLv3 и ниже | Нет | CBC | Уязвимы к POODLE, держать включёнными нельзя |
Проверка после перезагрузки: openssl s_client -connect example.com:443 -tls1 -brief должен завершиться ошибкой, а запрос с -tls1_3 показать успешное рукопожатие. Разбор частых ошибок цепочки сертификатов собран в статье Ручная установка SSL-сертификата на Nginx и Apache, а расширенные настройки reverse proxy и обновления сертификатов описаны в полном руководстве по SSL/TLS и HTTPS в Nginx.
Включение HSTS: защита от downgrade-атак и пошаговая настройка
Заголовок Strict-Transport-Security сообщает браузеру, что домен доступен только по HTTPS. После первого ответа с этим заголовком браузер сам переписывает все http-ссылки на https ещё до отправки запроса и блокирует обход ошибок сертификата: кнопки «всё равно продолжить» не будет. Так закрывается SSL stripping, при котором атакующий подменяет первый HTTP-ответ.
Параметры HSTS: max-age, includeSubDomains, preload
- max-age - время в секундах, в течение которого браузер помнит политику. 31536000 равно одному году, 300 и 86400 подходят только для тестов.
- includeSubDomains - распространяет правило на все поддомены. Включайте после проверки, что каждый поддомен имеет валидный сертификат, включая внутренние и служебные хосты.
- preload - добавляет домен в предзагруженный список браузеров, и правило действует даже при первом визите пользователя. Требования к домену: max-age не меньше 31536000, задан includeSubDomains, HTTPS работает на всех поддоменах, порт 80 корректно отвечает редиректом.
С предзагрузкой торопиться не стоит: удаление домена из списка занимает месяцы, поэтому сначала выкатывают политику без preload и наблюдают за логами редиректов.
Настройка HSTS на Nginx и Apache
В Nginx директива ставится внутри HTTPS-сервера, параметр always гарантирует отправку заголовка и для кодов ошибок:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
В Apache:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Порядок безопасного включения: max-age=300, затем 86400, затем 31536000. Заголовок отправляется только в ответах по HTTPS, для http-сервера настраивается редирект 301 на https. Проверка: curl -sI https://example.com | grep -i strict-transport-security. Если после включения HSTS сертификат сломается, пользователи увидят ошибку и обойти её не смогут, поэтому мониторинг срока действия сертификата ставят до, а не после. Дополнительные заголовки безопасности и их связка с HSTS разобраны в материале Nginx и Apache: полная защита веб-серверов.
Флаги Secure и HttpOnly для cookies: защита от перехвата и XSS
Флаг Secure запрещает браузеру отправлять cookie по незашифрованному соединению, флаг HttpOnly закрывает доступ к cookie из JavaScript, поэтому вредоносный скрипт при XSS не заберёт идентификатор сессии. Префиксы имён усиливают правила: cookie с именем __Host- обязана иметь Secure, Path=/ и не иметь Domain, а __Secure- требует только Secure. Эти префиксы отсекают попытки подменить cookie с поддомена.
Установка флагов Secure и HttpOnly на Nginx и Apache
Если cookie ставит приложение за обратным прокси, флаги удобно добавлять на уровне Nginx. Начиная с версии 1.19.3 работает директива proxy_cookie_flags:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_cookie_flags ~ secure httponly samesite=lax;
}
Для старых сборок применяют proxy_cookie_path, но учитывайте побочный эффект: директива переписывает атрибут Path, поэтому cookies с путём, отличным от корня, могут отвязаться.
proxy_cookie_path / "/; Secure; HttpOnly";
В Apache правка заголовка ответа выглядит так:
Header always edit Set-Cookie ^(.*)$ $1;Secure;HttpOnly
Директива добавит флаги ко всем cookies, включая те, где они уже есть, поэтому после применения проверьте ответ curl -sI https://example.com | grep -i set-cookie на дубли вида Secure;Secure. Надёжнее задавать флаги в самом приложении. Для PHP правки в php.ini или в конфиге пула:
session.cookie_secure = 1 session.cookie_httponly = 1 session.cookie_samesite = Lax
После изменения php.ini перезапустите PHP-FPM, иначе старые настройки останутся в пуле воркеров.
SameSite: дополнительный уровень защиты cookies
Атрибут SameSite управляет отправкой cookie при кросс-сайтовых запросах и снижает риск CSRF. Значения: Strict не отправляет cookie при переходе с другого сайта, Lax отправляет только при навигации верхнего уровня методом GET, None отправляет всегда и требует флага Secure. Браузеры уже применяют Lax по умолчанию для cookies без явного атрибута, но полагаться на это в продакшене не стоит. Итоговая строка для сессионной cookie:
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=3600
Для платёжных и OAuth-сценариев, где нужен возврат с внешнего домена, проверяйте поведение SameSite=Lax отдельно: часть редиректов выполняется методом POST и cookie не дойдёт.
Предотвращение утечек через смешанный контент
Смешанный контент (Mixed Content) - это загрузка ресурсов по HTTP со страницы, открытой по HTTPS. Активный контент (скрипты, iframe, стили, XHR-запросы) браузеры блокируют, пассивный (картинки, аудио, видео) автоматически поднимают до HTTPS. Проблема не только в предупреждениях: URL ресурса уходит в открытом виде, а если в нём есть токен или идентификатор, он попадает в логи промежуточных узлов.
Поиск смешанного контента начинается с консоли браузера: строки Mixed Content показывают точный адрес ресурса. Массово проверить шаблоны и собранный фронтенд помогает поиск по исходникам:
grep -rn "http://" ./templates ./public --include="*.html" --include="*.js" --include="*.css"
Жёсткий вариант решения - директива Content-Security-Policy, которая переписывает HTTP-запросы на HTTPS или блокирует их полностью:
# Nginx add_header Content-Security-Policy "upgrade-insecure-requests" always; # Apache Header set Content-Security-Policy "upgrade-insecure-requests"
Директива block-all-mixed-content даёт более строгое поведение и запрещает загрузку HTTP-ресурсов вместо их обновления, но в новых браузерах она считается устаревшей, поэтому основной выбор - upgrade-insecure-requests. CSP стоит вводить постепенно: сначала в режиме Content-Security-Policy-Report-Only с параметром report-uri, чтобы собрать нарушения и не сломать работающий функционал. Практические конфигурации CSP и защита динамического контента от XSS описаны в статье Защита приложений с динамическим контентом.
Отдельно проверьте базовые URL в конфигурации приложения и статические шаблоны: абсолютные http-ссылки в них проще заменить на https, чем ловить их в проде. Протокол-независимые ссылки вида //example.com/cdn.js в новом коде не используйте: они зависят от схемы текущей страницы и легко превращаются в HTTP.
Шифрование на стороне браузера с Web Crypto API
Web Crypto API даёт доступ к криптографии через объект window.crypto.subtle (SubtleCrypto). Методы асинхронные, работают на промисах, а сам интерфейс доступен только в защищённом контексте: HTTPS, localhost или file. Ключи генерируются в браузере, открытый текст не уходит на сервер, что даёт end-to-end шифрование и модель zero-knowledge, при которой сервер хранит только шифротекст.
Пример шифрования данных с AES-GCM
AES-GCM обеспечивает и шифрование, и аутентификацию: изменение байта шифротекста приводит к ошибке проверки тега. Пример генерации ключа, шифрования и дешифрования строки:
async function encryptString(plainText, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plainText);
const cipherBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(cipherBuf))
};
}
async function decryptString(payload, key) {
const iv = new Uint8Array(payload.iv);
const data = new Uint8Array(payload.data);
const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
return new TextDecoder().decode(plainBuf);
}
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
const payload = await encryptString("секретные данные", key);
const restored = await decryptString(payload, key);
Ключ из пароля пользователя получают через PBKDF2 с солью и большим числом итераций, иначе перебор по словарю займёт минуты:
const salt = crypto.getRandomValues(new Uint8Array(16));
const baseKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
const key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 600000, hash: "SHA-256" },
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
Для обмена симметричным ключом с сервером используют RSA-OAEP: им шифруют короткий ключ AES, а не сами данные, потому что длина сообщения ограничена размером модуля (для 3072 бит и SHA-256 это около 318 байт).
const pair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 3072,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["encrypt", "decrypt"]
);
Правила, которые нарушают чаще всего: не переиспользуйте IV с одним и тем же ключом, храните IV рядом с шифротекстом, не выводите ключи в localStorage и не считайте AES-GCM заменой TLS. Одна ошибка в управлении ключами обнуляет всю схему, поэтому для критичных данных логику проверяют тестами с фиксированными векторами.
Когда использовать Web Crypto API, а когда серверное шифрование
| Критерий | Клиентское шифрование | Серверное шифрование |
|---|---|---|
| Кто видит открытые данные | Только браузер пользователя | Сервер и администраторы |
| Управление ключами | На пользователе, потеря ключа означает потерю данных | В KMS или HSM, есть ротация и резервные копии |
| Типовые сценарии | Менеджеры паролей, защищённые заметки, мессенджеры | Обычные веб-приложения, платежи, персональные данные |
| Сложность | Высокая: клиентская криптография и восстановление доступа | Ниже: шифрование дисков, бэкапов и полей БД |
Клиентское шифрование дополняет серверное, а не отменяет его. TLS защищает канал, но трафик расшифровывается на балансировщике, и дальше до приложения данные идут уже открытыми: если сервисы в одной сети, внутренний участок тоже стоит шифровать. Секреты и ключи API храните в переменных окружения или в хранилище секретов, а не в коде и не в репозитории. Когда сервис обращается к внешним моделям, число мест с токенами удобно сократить: AiTunnel даёт единый интерфейс к более чем 200 моделям (GPT, Gemini, Claude) с управлением ключами и бюджетами, оплатой в рублях и совместимостью с библиотеками OpenAI.
Чек-лист и типичные ошибки при настройке шифрования
- Выпустить сертификат Let's Encrypt через certbot, acme.sh, win-acme или контейнер certbot и проверить, что в конфигах указаны пути до симлинков.
- Настроить автопродление в cron (0 0 * * 1,4 /usr/bin/certbot renew --noninteractive) с deploy-hook на перезагрузку веб-сервера.
- Ограничить протоколы до TLS 1.2 и TLS 1.3 и задать современные наборы шифров в Nginx и Apache.
- Включить HSTS, начиная с max-age=300, и только после проверки поддоменов добавлять includeSubDomains и preload.
- Установить флаги Secure и HttpOnly для cookies на уровне приложения и веб-сервера, добавить SameSite=Lax.
- Найти и убрать смешанный контент, включить CSP с upgrade-insecure-requests.
- Оценить необходимость клиентского шифрования через Web Crypto API для самых чувствительных данных.
Частые ошибки, которые сводят работу к нулю:
- HSTS включён с preload до проверки поддоменов, и внутренний сервис без сертификата перестал открываться.
- Флаги cookies забыты, и сессия уходит по HTTP при заходе на http-версию сайта.
- TLS 1.0 и 1.1 оставлены ради «старых клиентов», хотя актуальные браузеры их не используют.
- Автопродление не настроено, мониторинг срока действия отсутствует, сертификат истекает в выходные.
- Смешанный контент не проверен, скрипт по HTTP блокируется браузером, часть функционала молча ломается.
- Правка php.ini без перезапуска PHP-FPM, из-за чего новые флаги cookies не применяются.
Проверка результата занимает минуты:
curl -sI https://example.com | grep -i "strict-transport-security" curl -sI https://example.com | grep -i "set-cookie" openssl s_client -connect example.com:443 -tls1_2 2>/dev/null | grep -E "Protocol|Cipher"
Дополнительно прогоните домен через сканер SSL Labs и посмотрите оценку заголовков безопасности, а затем зафиксируйте текущее состояние конфигураций: регулярный аудит по чек-листу из материала Аудит безопасности Nginx: TLS, заголовки и контроль доступа покажет, что изменилось после обновлений веб-сервера. Начните с одного шага: проверьте, есть ли в ответе вашего сайта заголовок Strict-Transport-Security и флаг Secure у сессионной cookie. Эти две проверки чаще всего выявляют реальные дыры.