TLS

OxPHP выполняет завершение TLS нативно. Обратный прокси или внешняя SSL-библиотека не требуются. После настройки сервер принимает HTTPS-соединения и автоматически согласует лучший доступный протокол.

Как это работает

Чтобы включить TLS, задайте TLS_CERT и TLS_KEY, указав ими на файлы сертификата и приватного ключа в кодировке PEM. Как только оба значения заданы, сервер начинает слушать HTTPS-соединения по адресу, указанному в LISTEN_ADDR.

Рукопожатие TLS происходит до любой обработки HTTP:

  1. На LISTEN_ADDR приходит TCP-соединение.
  2. Сервер выполняет рукопожатие TLS с использованием настроенных сертификата и ключа.
  3. Согласование протокола (ALPN) выбирает HTTP/2 (h2) или HTTP/1.1 в зависимости от поддержки на стороне клиента.
  4. Зашифрованное соединение передаётся на уровень HTTP для обычной обработки запросов.
Note

Когда TLS включён, таймауты заголовков и запроса применяются к каждому запросу после завершения рукопожатия TLS.

Конфигурация

Переменная По умолчанию Описание
TLS_CERT (unset) Путь к файлу сертификата в кодировке PEM. Для включения TLS должны быть заданы оба значения — TLS_CERT и TLS_KEY
TLS_KEY (unset) Путь к файлу приватного ключа в кодировке PEM
TLS_MIN_VERSION 1.2 Минимальная принимаемая версия протокола TLS: 1.2 или 1.3. Любое другое значение вызывает ошибку при запуске
LISTEN_ADDR 0.0.0.0:80 Адрес и порт для прослушивания. При использовании TLS смените на 0.0.0.0:443

Если задан только один из TLS_CERT или TLS_KEY, сервер откажется запускаться: наполовину настроенная пара почти всегда означает опечатку в имени переменной, а молчаливая раздача простого HTTP на порту, предназначенном для HTTPS, привела бы к небезопасному поведению по умолчанию. Пустое значение (TLS_CERT=, как получается при подстановках вида ${TLS_CERT:-}) трактуется как незаданное; когда не задано ни то, ни другое, сервер запускается в режиме простого HTTP.

Поддерживаемые протоколы

Возможность Подробности
Версии TLS TLS 1.2 и TLS 1.3 (нижняя граница настраивается через TLS_MIN_VERSION)
Протоколы ALPN h2 (HTTP/2) и http/1.1, согласуются именно в этом порядке
Клиентские сертификаты Не поддерживаются (нет взаимного TLS)

Минимальная версия протокола

По умолчанию сервер принимает TLS 1.2 и TLS 1.3. Развёртывания, которые обязаны отклонять TLS 1.2 (области действия PCI-DSS, внутренние политики, требующие использования только версии 1.3), могут поднять нижнюю границу:

bash
TLS_MIN_VERSION=1.3

С нижней границей на уровне 1.3 сообщение ClientHello по TLS 1.2 отклоняется во время рукопожатия с алертом protocol_version; на клиентов TLS 1.3 это не влияет.

Недопустимое значение (1.1, 1.0 или опечатка) — это жёсткая ошибка при запуске, а не молчаливый откат: неверно указанная граница безопасности должна приводить к явному сбою, а не тихо работать с более слабой конфигурацией. Значение проверяется при запуске, даже если сам TLS не включён, и oxphp config --check сообщает о той же ошибке ещё до любого перезапуска. Пустое значение (TLS_MIN_VERSION=, как получается при подстановках вида ${TLS_MIN_VERSION:-}) трактуется как незаданное. TLS 1.0 и 1.1 не поддерживаются вовсе и не могут быть включены.

Наборы шифров не настраиваются — так задумано

Встроенная реализация TLS поставляется только с современными наборами шифров AEAD (AES-GCM и ChaCha20-Poly1305 с обменом ключами ECDHE). В ней нет RC4, нет наборов в режиме CBC, нет экспортных шифров, которые можно было бы отключить, — поэтому классической «крутилке» ограничения слабых шифров попросту нечего убирать. TLS_MIN_VERSION задаёт только нижнюю границу протокола; она не меняет криптографический провайдер и не является переключателем соответствия FIPS.

HTTP/2

OxPHP обслуживает HTTP/2 и HTTP/1.1 на одном и том же порту. Протокол выбирается для каждого соединения, и настройки для включения или отключения HTTP/2 нет:

  • По TLS протокол согласуется во время рукопожатия через ALPN. OxPHP анонсирует h2, затем http/1.1, так что клиенты с поддержкой HTTP/2 получают HTTP/2, а все остальные прозрачно откатываются к HTTP/1.1.
  • Без TLS (h2c) OxPHP обнаруживает преамбулу соединения HTTP/2 и обслуживает нешифрованный HTTP/2 для клиентов, подключающихся с предварительным знанием (например, curl --http2-prior-knowledge). Клиенты, не поддерживающие HTTP/2, продолжают использовать HTTP/1.1 на том же порту. (Рукопожатие Upgrade: h2c не используется — HTTP/2 поверх открытого текста требует предварительного знания.)

Окна управления потоком HTTP/2 подняты выше значений по умолчанию, заданных протоколом, — 8 МБ на соединение и 4 МБ на поток против 64 КБ по умолчанию, — чтобы избежать простоев на типичных ответах PHP, которые обычно больше одного стандартного окна.

PHP видит согласованный протокол в $_SERVER['SERVER_PROTOCOL'] ("HTTP/2" или "HTTP/1.1").

Проверка

bash
# HTTP/2 over TLS (negotiated via ALPN) curl -k --http2 -I https://localhost/ # Cleartext HTTP/2 (h2c, prior knowledge) curl --http2-prior-knowledge -I http://localhost/

Ищите HTTP/2 200 в строке ответа.

Ограничения на соединение

OxPHP применяет ограничения HTTP/2 на каждое соединение, чтобы ограничить усиление нагрузки, которое одно TCP-соединение может создать на пул воркеров PHP. Каждый принятый поток становится PHP-запросом в очереди, поэтому неограниченное число потоков от одного соединения могло бы само по себе перегрузить пул.

Переменная По умолчанию Описание
H2_MAX_CONCURRENT_STREAMS PHP_WORKERS_MAX × 4 (min 32) Максимальное число одновременно открытых потоков на соединение. Лишние потоки получают REFUSED_STREAM
H2_MAX_PENDING_RESET 20 Максимальное число кадров RST_STREAM в очереди до закрытия соединения (защита от Rapid Reset, CVE-2023-44487)
H2_MAX_HEADER_LIST_BYTES 65536 Максимальный суммарный объём декодированных байтов заголовков на запрос (защита от HPACK-бомбы)
H2_KEEPALIVE_INTERVAL_SECS 20 Секунды между кадрами PING HTTP/2; 0 отключает keepalive
H2_KEEPALIVE_TIMEOUT_SECS 10 Секунды ожидания ответа на PING до закрытия соединения

PHP_WORKERS_MAX — это максимальное число воркеров, заданное через PHP_WORKERS. Для динамического диапазона вида 4:16 используется максимум (16). Значение по умолчанию масштабируется вместе с числом воркеров, чтобы легитимные параллельные загрузки страниц на одном соединении не создавали больше нагрузки на очередь, чем способен поглотить пул.

Компромисс

H2_MAX_CONCURRENT_STREAMS намеренно настраивается под ёмкость пула, а не под поведение мультиплексирования в браузере. Браузеры отправляют десятки запросов на ресурсы по одному соединению HTTP/2; те, что превышают лимит, получают REFUSED_STREAM и автоматически повторяются браузером в следующей партии. Это добавляет небольшую задержку на страницах с интенсивным веерным разветвлением запросов (много ресурсов на соединение), но не даёт одному соединению заполнить очередь запросов PHP. Если на вашем сайте много страниц с крупными ресурсами и значение по умолчанию вызывает заметную задержку, поднимайте H2_MAX_CONCURRENT_STREAMS явно, а не увеличивайте PHP_WORKERS.

Поддерживаемые типы ключей

Файл приватного ключа должен содержать единственный ключ в кодировке PEM в одном из следующих форматов:

  • RSA
  • ECDSA (например, prime256v1, secp384r1)
  • Ed25519

Файл сертификата может содержать один или несколько сертификатов в кодировке PEM. Для использования в продакшене включайте полную цепочку: ваш серверный сертификат, за которым следуют промежуточные сертификаты.

Самоподписанный сертификат для разработки

Сгенерируйте самоподписанный сертификат ECDSA для локальной разработки:

bash
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ -keyout key.pem -out cert.pem -days 365 -nodes \ -subj "/CN=localhost"

Затем настройте OxPHP на использование сгенерированных файлов:

bash
TLS_CERT=./cert.pem TLS_KEY=./key.pem LISTEN_ADDR=0.0.0.0:443

Устранение неполадок

Сервер запускается, но TLS не активен

OxPHP требует, чтобы были заданы оба значения — TLS_CERT и TLS_KEY. Если задано только одно из них, сервер откажется запускаться (TLS_CERT is set but TLS_KEY is missing — both are required to enable TLS (unset TLS_CERT to serve plain HTTP)), и oxphp config --check сообщит о той же ошибке конфигурации ещё до развёртывания. Если не задано ни одно, простой HTTP является нормальным молчаливым поведением по умолчанию, — но если переменные присутствуют и пусты (подстановка ${VAR:-}, отрендеренная как пустая, например из-за сломанного монтирования секрета), предупреждение при запуске (TLS variable(s) set but empty — TLS disabled, serving plain HTTP) оставит след. Предупреждение также записывается в лог, когда задано TLS_MIN_VERSION=1.3, а TLS при этом не включён. Убедитесь, что заданы обе переменные:

bash
docker exec <container> env | grep TLS
Ошибка TLS_KEY: no private key found in ... при запуске

Файл ключа пуст, повреждён или содержит только сертификат. Убедитесь, что файл ключа содержит блок -----BEGIN ... PRIVATE KEY-----:

bash
grep "PRIVATE KEY" key.pem

Если ключ отсутствует, перегенерируйте пару сертификата и ключа.

Сертификат не удаётся загрузить при запуске

Файл сертификата пуст или повреждён, и запуск прерывается с ошибкой от уровня TLS. Убедитесь, что файл сертификата содержит хотя бы один блок -----BEGIN CERTIFICATE-----:

bash
grep "BEGIN CERTIFICATE" cert.pem
Клиенты видят ошибку цепочки сертификатов

Сервер отправляет только конечный (leaf) сертификат без промежуточных. Объедините полную цепочку в один PEM-файл:

bash
cat cert.pem intermediate.pem > fullchain.pem

Затем задайте TLS_CERT=./fullchain.pem.

Срок действия сертификата истёк

OxPHP читает файлы сертификатов при запуске и держит их в памяти. Обновление сертификата на диске не даёт эффекта, пока сервер не будет перезапущен.

Решение: перезапустите OxPHP после обновления сертификата. Автоматизируйте это с помощью вашего инструмента обновления сертификатов (например, опции --deploy-hook в certbot).

Не удаётся обслуживать HTTP и HTTPS на одном порту

OxPHP слушает единственный порт. Чтобы поддерживать оба протокола одновременно, используйте обратный прокси (Caddy, Traefik, nginx), который обрабатывает перенаправление с HTTP на HTTPS, или запустите второй экземпляр OxPHP на порту 80, выделенный для перенаправления трафика.

Пример для Docker

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.0 ports: - "443:443" environment: LISTEN_ADDR: "0.0.0.0:443" TLS_CERT: "/etc/ssl/oxphp/cert.pem" TLS_KEY: "/etc/ssl/oxphp/key.pem" volumes: - ./app:/var/www/html:ro - ./certs:/etc/ssl/oxphp:ro

Лучшие практики

  • Включайте промежуточные сертификаты в цепочку PEM. Размещайте серверный сертификат первым, за ним по порядку промежуточные, чтобы клиенты могли проверить полный путь доверия.
  • Автоматизируйте обновление сертификатов. Используйте certbot или acme.sh для обновления сертификатов до истечения срока действия, а затем перезапускайте OxPHP для загрузки новых файлов.
  • Используйте обратный прокси для перенаправления с HTTP на HTTPS. OxPHP не обслуживает HTTP и HTTPS на одном порту одновременно.

Примечания

  • OxPHP не зависит от OpenSSL. TLS обрабатывается встроенной реализацией, что устраняет распространённый источник CVE во внешних библиотеках.
  • Файлы сертификата и ключа читаются только при запуске. Обновление сертификатов на диске требует перезапуска сервера.

Смотрите также