4. TLS termination

Терминация TLS в Nginx: разгрузка CPU, ускорение TLS 1.3 и mTLS

В современном вебе передача сетевых данных в открытом виде (по протоколу HTTP) — непростительная ошибка с точки зрения безопасности. Протокол HTTPS (HTTP поверх защищенного транспортного уровня TLS) шифрует трафик, обеспечивая строгую конфиденциальность, аутентификацию сервера и целостность передаваемых пакетов.

Однако криптография — это чрезвычайно тяжелая вычислительная математика. Операции асимметричного шифрования при установке защищенного соединения сжигают колоссальное количество процессорных тактов.

TLS Termination (Терминирование TLS) — это архитектурный паттерн, при котором входящие зашифрованные клиентские HTTPS-сессии расшифровываются на граничном шлюзе (Nginx). Дальше внутрь защищенного периметра инфраструктуры (к микросервисам на Go) очищенный трафик передается по легковесному и быстрому протоколу HTTP (или специализированному gRPC h2c). Nginx берет на себя всю криптографическую работу, терминируя защищенный канал на входе.


1. Mechanical Sympathy: Аппаратная цена рукопожатия TLS Handshake

Чтобы осознать, почему выставление Go-сервера напрямую в публичный интернет с TLS — не всегда оптимальное инженерное решение, посмотрим на происходящее глазами процессора.

Процесс установки защищенной сессии (TLS Handshake) опирается на ресурсоемкую асимметричную криптографию (алгоритмы RSA или кривые Диффи-Хеллмана ECDHE). Вычисление модульной экспоненты над 2048/4096-битными числами или скалярное умножение точек на эллиптических кривых требует сотен тысяч тактов CPU на каждое входящее соединение.

Если Go-приложение принимает HTTPS напрямую, его планировщик вынужден постоянно выделять потоки ОС (M) под криптографические операции. Но главная угроза — медленные и нестабильные клиенты:

  • Клиент на слабом мобильном канале передает сертификаты и ключевые пакеты с задержками в сотни миллисекунд.
  • В рантайме Go горутина, обслуживающая такой сокет, висит в памяти, удерживая криптографический контекст, сессионные буферы кучи и блокируя системные ресурсы.
  • При наплыве тысяч таких клиентов куча (Heap) раздувается, заставляя сборщик мусора (Garbage Collector) непрерывно сканировать долгоживущие структуры и просаживать throughput полезного бэкенда.

Почему Nginx эффективнее справляется с TLS:

  1. Асинхронное событийно-ориентированное ядро на Си: Nginx использует системный вызов epoll. Он удерживает десятки тысяч медленных TLS-рукопожатий в компактных C-структурах, не аллоцируя динамические стеки горутин и не создавая работы для сборщика мусора.
  2. Аппаратные векторные инструкции и OpenSSL: Nginx собирается с высокооптимизированными криптографическими библиотеками (OpenSSL, BoringSSL или wolfSSL). Они на уровне ассемблера используют специализированные векторные инструкции CPU — AES-NI (аппаратное шифрование AES на кремнии), CLMUL и AVX-512, выжимая предельный гигабитный трафик при минимальном нагреве кристаллов CPU.
flowchart LR
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    A["Клиент из интернета"]:::entry -->|"1. HTTPS - Тяжелый TLS Handshake"| B["Nginx - Терминация"]
    B -->|"2. HTTP - Быстрый, без шифрования"| C["Go Backend"]
    C -->|"3. HTTP Response"| B
    B -->|"4. HTTPS - Шифрование ответа"| A
  

2. Возобновление сессий TLS: Избавляемся от повторного Handshake

Самое быстрое криптографическое рукопожатие — то, которого удалось полностью избежать. Если клиент совершает серию запросов, повторный полный TLS Handshake (с асимметричным обменом ключами) создает неоправданную задержку в 1–2 сетевых round-trip'а (RTT).

В протоколах TLS предусмотрены механизмы быстрого возобновления сессий (Session Resumption), которые Nginx поддерживает на уровне конфигурации:

1. Session IDs (Кэширование на стороне сервера)

Сервер и клиент сохраняют уникальный 32-байтный идентификатор сессии (Session ID). При повторном подключении клиент передает этот ID. Если Nginx находит его в кэше разделяемой памяти, этап асимметричных вычислений пропускается: стороны мгновенно восстанавливают симметричный ключ шифрования и начинают передачу данных.

NGINX
# Выделяем 10 МБ в разделяемой памяти между воркерами для кэша сессий (~40 000 сессий)
ssl_session_cache shared:SSL:10m; 
ssl_session_timeout 1d;

2. Session Tickets (Stateless-механизм)

Хранение сессий в памяти Nginx плохо масштабируется при балансировке между несколькими серверами (клиент может прийти на другой хост, где нет нужного ID). В механизме Session Tickets (RFC 5077) сервер шифрует состояние сессии секретным симметричным мастер-ключом и отдает его клиенту в виде зашифрованного билета (Ticket). При следующем запросе клиент возвращает билет, Nginx расшифровывает его своим ключом и продолжает диалог.


3. TLS 1.3 и режим 0-RTT: Экстремальная скорость против рисков безопасности

Современный стандарт TLS 1.3 кардинально упростил криптографию в вебе:

  1. Удалены все небезопасные и устаревшие шифры (RC4, 3DES, CBC-режимы AES, MD5, SHA-1).
  2. Полное рукопожатие сокращено с 2-х RTT до ровно 1 RTT — клиент отправляет свои криптографические параметры одновременно с пакетом Client Hello.

Вершиной оптимизации стал режим 0-RTT (Zero Round Trip Time), или Early Data. Если клиент ранее уже устанавливал защищенное соединение с сервером, он может передать первый HTTP-запрос (например, GET /api/v1/profile) внутри первого же криптографического пакета, еще до завершения нового рукопожатия!

Для включения 0-RTT в Nginx используется директива:

NGINX
ssl_early_data on;

4. Передача криптографического контекста в рантайм Go

Поскольку Nginx снимает шифрование, соединение между Nginx и Go происходит по голому протоколу HTTP. Для приложения поле r.TLS в объекте http.Request равно nil.

Если Go-бэкенду необходимо сгенерировать абсолютный редирект, ссылку для подтверждения почты или проверить безопасность куки (Secure), он сформирует префикс http:// вместо https://, что приведет к блокировке браузером или ошибкам авторизации.

Для передачи контекста Nginx настраивается следующим образом:

NGINX
location / {
    proxy_pass http://go_backend:8080;

    # Сообщаем, что клиент пришел по защищенному HTTPS
    proxy_set_header X-Forwarded-Proto $scheme; # Примет значение 'https'

    # Передаем оригинальный внешний порт
    proxy_set_header X-Forwarded-Port $server_port;

    # Транслируем доменное имя
    proxy_set_header Host $host;
}

В Go вы обязаны использовать мидлварь, которая читает эти заголовки и подменяет r.URL.Scheme и r.URL.Host до того, как запрос попадет в бизнес-логику. В популярных фреймворках или в стандартном пакете net/http через обертки это настраивается с указанием TrustForwardHeader:

Go
func HTTPSRedirectOrContextMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        proto := r.Header.Get("X-Forwarded-Proto")
        if proto == "https" {
            r.URL.Scheme = "https"
        }
        if host := r.Header.Get("Host"); host != "" {
            r.URL.Host = host
        }
        next.ServeHTTP(w, r)
    })
}

5. Взаимная аутентификация: Mutual TLS (mTLS)

В корпоративных средах и архитектуре Zero Trust все чаще применяется Mutual TLS (mTLS). В отличие от стандартного HTTPS, где только клиент валидирует подлинность сертификата сервера, при mTLS сервер требует от клиента предъявить его персональный сертификат X.509 и проверяет его по локальному центру сертификации (CA).

Nginx может выступать централизованной точкой проверки mTLS-сертификатов на входе:

NGINX
server {
    listen 443 ssl;
    server_name m2m.example.com;

    ssl_certificate        /etc/ssl/server.crt;
    ssl_certificate_key    /etc/ssl/server.key;

    # Доверенный корневой сертификат для валидации клиентов
    ssl_client_certificate /etc/ssl/ca.pem;

    # Требовать обязательный клиентский сертификат (on) или опционально (optional)
    ssl_verify_client on;

    location / {
        proxy_pass http://go_backend:8080;

        # Передаем статус верификации и метаданные субъекта
        proxy_set_header X-Client-Verify $ssl_client_verify;
        proxy_set_header X-Client-Cert-Dn $ssl_client_s_dn;

        # Передаем весь сертификат в формате PEM для глубокого анализа в Go
        proxy_set_header X-Client-Cert $ssl_client_escaped_cert;
    }
}

Итог

  1. TLS Termination снимает с Go-бэкенда тяжелую криптографическую нагрузку и проблемы с медленными клиентами, перекладывая их на C-ядро Nginx.
  2. Кэширование сессий (Session Cache и Session Tickets) радикально снижает нагрузку на CPU Nginx, устраняя необходимость полных рукопожатий.
  3. TLS 1.3 0-RTT — мощный ускоритель для API, но требует строгой защиты от Replay-атак на уровне Go-кода.
  4. Контекст протокола: Всегда передавайте X-Forwarded-Proto, чтобы Go понимал, что он работает за HTTPS.
  5. mTLS: Nginx может верифицировать клиентские сертификаты и передавать их данные в Go через HTTP-заголовки.

Расшифровав трафик и сбалансировав его, Nginx может сделать еще одну невероятно полезную вещь — кэшировать ответы бэкенда, снижая нагрузку на Go до нуля. В следующей статье мы разберем механику кэширования в Nginx: 5. Кэширование в nginx.