TLS
OxPHP выполняет завершение TLS нативно. Обратный прокси или внешняя SSL-библиотека не требуются. После настройки сервер принимает HTTPS-соединения и автоматически согласует лучший доступный протокол.
Как это работает
Чтобы включить TLS, задайте TLS_CERT и TLS_KEY, указав ими на файлы сертификата и приватного ключа в кодировке PEM. Как только оба значения заданы, сервер начинает слушать HTTPS-соединения по адресу, указанному в LISTEN_ADDR.
Рукопожатие TLS происходит до любой обработки HTTP:
- На
LISTEN_ADDRприходит TCP-соединение. - Сервер выполняет рукопожатие TLS с использованием настроенных сертификата и ключа.
- Согласование протокола (ALPN) выбирает HTTP/2 (
h2) или HTTP/1.1 в зависимости от поддержки на стороне клиента. - Зашифрованное соединение передаётся на уровень HTTP для обычной обработки запросов.
Когда 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), могут поднять нижнюю границу:
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").
Проверка
# 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 для локальной разработки:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=localhost"Затем настройте OxPHP на использование сгенерированных файлов:
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 при этом не включён. Убедитесь, что заданы обе переменные:
docker exec <container> env | grep TLSОшибка TLS_KEY: no private key found in ... при запуске
Файл ключа пуст, повреждён или содержит только сертификат. Убедитесь, что файл ключа содержит блок -----BEGIN ... PRIVATE KEY-----:
grep "PRIVATE KEY" key.pemЕсли ключ отсутствует, перегенерируйте пару сертификата и ключа.
Сертификат не удаётся загрузить при запуске
Файл сертификата пуст или повреждён, и запуск прерывается с ошибкой от уровня TLS. Убедитесь, что файл сертификата содержит хотя бы один блок -----BEGIN CERTIFICATE-----:
grep "BEGIN CERTIFICATE" cert.pemКлиенты видят ошибку цепочки сертификатов
Сервер отправляет только конечный (leaf) сертификат без промежуточных. Объедините полную цепочку в один PEM-файл:
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
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 во внешних библиотеках.
- Файлы сертификата и ключа читаются только при запуске. Обновление сертификатов на диске требует перезапуска сервера.
Смотрите также
- Справочник по конфигурации — полный список переменных окружения
- Руководство по Docker — монтирование томов и управление сертификатами в Docker