TLS

OxPHP термінує TLS нативно. Ні зворотний проксі, ні зовнішня SSL-бібліотека не потрібні. Після налаштування сервер приймає HTTPS-зʼєднання й автоматично узгоджує найкращий доступний протокол.

Як це працює

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

TLS-рукостискання відбувається до будь-якої обробки HTTP:

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

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

Налаштування

Змінна За замовчуванням Опис
TLS_CERT (не задано) Шлях до файлу сертифіката у форматі PEM. Щоб увімкнути TLS, потрібно задати і TLS_CERT, і TLS_KEY
TLS_KEY (не задано) Шлях до файлу приватного ключа у форматі 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 для клієнтів, які підключаються з попереднім знанням (prior knowledge, наприклад 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 (мін. 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
Клієнти бачать помилку ланцюжка сертифікатів

Сервер надсилає лише кінцевий сертифікат без проміжних. Обʼєднайте повний ланцюжок в один 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.13.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 у зовнішніх бібліотеках.
  • Файли сертифіката й ключа читаються лише під час запуску. Щоб оновлені на диску сертифікати почали діяти, потрібно перезапустити сервер.

Промпти для вашого агента

Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.

prompt
Налаштуй термінування TLS в OxPHP для мого розгортання. Прочитай сторінку `features/tls` через MCP-сервер документації OxPHP. Зʼясуй, звідки зараз беруться мої сертифікати, потім дай мені конфігурацію, яка обслуговує цей репозиторій через HTTPS, і скажи, що зміниться для всього, що стоїть перед ним.
prompt
Моє налаштування TLS в OxPHP узгоджує не те, чого я очікую. Прочитай сторінку `features/tls` через MCP-сервер документації OxPHP. Перевір мою мінімальну версію протоколу, ланцюжок сертифікатів і конфігурацію ALPN на відповідність тому, що описано на сторінці, і скажи, що з цього пояснює те, що я бачу.

Дивіться також