TLS

OxPHP obsługuje terminację TLS natywnie. Nie jest wymagane żadne reverse proxy ani zewnętrzna biblioteka SSL. Po skonfigurowaniu serwer przyjmuje połączenia HTTPS i automatycznie negocjuje najlepszy dostępny protokół.

Jak to działa

Aby włączyć TLS, ustaw TLS_CERT i TLS_KEY tak, by wskazywały na pliki certyfikatu i klucza prywatnego zakodowane w formacie PEM. Gdy oba są ustawione, serwer nasłuchuje połączeń HTTPS pod adresem określonym przez LISTEN_ADDR.

Uzgadnianie TLS (handshake) odbywa się przed jakimkolwiek przetwarzaniem HTTP:

  1. Na LISTEN_ADDR przychodzi połączenie TCP.
  2. Serwer przeprowadza uzgadnianie TLS z użyciem skonfigurowanego certyfikatu i klucza.
  3. Negocjacja protokołu (ALPN) wybiera HTTP/2 (h2) lub HTTP/1.1 w zależności od tego, co obsługuje klient.
  4. Zaszyfrowane połączenie jest przekazywane do warstwy HTTP w celu normalnej obsługi żądań.
Note

Gdy TLS jest włączony, limity czasu nagłówków i żądań obowiązują dla każdego żądania po zakończeniu uzgadniania TLS.

Konfiguracja

Zmienna Domyślnie Opis
TLS_CERT (unset) Ścieżka do pliku certyfikatu zakodowanego w PEM. Aby włączyć TLS, muszą być ustawione zarówno TLS_CERT, jak i TLS_KEY
TLS_KEY (unset) Ścieżka do pliku klucza prywatnego zakodowanego w PEM
TLS_MIN_VERSION 1.2 Minimalna akceptowana wersja protokołu TLS: 1.2 lub 1.3. Każda inna wartość to błąd przy starcie
LISTEN_ADDR 0.0.0.0:80 Adres i port do nasłuchiwania. Zmień na 0.0.0.0:443 przy korzystaniu z TLS

Jeśli ustawiony jest tylko jeden z TLS_CERT lub TLS_KEY, serwer odmawia startu: niekompletnie skonfigurowana para to niemal zawsze literówka w nazwie zmiennej, a ciche serwowanie zwykłego HTTP na porcie przeznaczonym dla HTTPS oznaczałoby awarię typu fail-open. Pusta wartość (TLS_CERT=, jaką generują podstawienia w stylu ${TLS_CERT:-}) jest traktowana jako nieustawiona; gdy żadna z nich nie jest ustawiona, serwer startuje w trybie zwykłego HTTP.

Obsługiwane protokoły

Możliwość Szczegóły
Wersje TLS TLS 1.2 i TLS 1.3 (dolny próg konfigurowalny przez TLS_MIN_VERSION)
Protokoły ALPN h2 (HTTP/2) i http/1.1, negocjowane w tej kolejności
Certyfikaty klienta Nieobsługiwane (brak wzajemnego TLS)

Minimalna wersja protokołu

Domyślnie serwer akceptuje TLS 1.2 i TLS 1.3. Wdrożenia, które muszą odrzucać TLS 1.2 (zakresy PCI-DSS, polityki wewnętrzne wymagające wyłącznie 1.3), mogą podnieść dolny próg:

bash
TLS_MIN_VERSION=1.3

Przy progu ustawionym na 1.3 ClientHello w wersji TLS 1.2 jest odrzucany podczas uzgadniania z alertem protocol_version; klientów TLS 1.3 to nie dotyczy.

Nieprawidłowa wartość (1.1, 1.0 lub literówka) to twardy błąd przy starcie, a nie ciche zejście do wartości zapasowej: błędnie wpisany próg bezpieczeństwa musi zawieść głośno, zamiast po cichu działać ze słabszą konfiguracją. Wartość jest walidowana przy starcie nawet wtedy, gdy sam TLS nie jest włączony, a oxphp config --check zgłasza ten sam błąd jeszcze przed jakimkolwiek restartem. Pusta wartość (TLS_MIN_VERSION=, jaką generują podstawienia w stylu ${TLS_MIN_VERSION:-}) jest traktowana jako nieustawiona. TLS 1.0 i 1.1 nie są w ogóle obsługiwane i nie można ich włączyć.

Zestawy szyfrów są nieedytowalne, zgodnie z założeniem

Wbudowana implementacja TLS dostarcza wyłącznie nowoczesne zestawy szyfrów AEAD (AES-GCM i ChaCha20-Poly1305 z wymianą kluczy ECDHE). Nie ma tu RC4, żadnego zestawu w trybie CBC ani szyfru eksportowego do wyłączenia, więc klasyczne pokrętło „ogranicz słabe szyfry” nie ma czego usuwać. TLS_MIN_VERSION to wyłącznie próg protokołu; nie zmienia dostawcy kryptografii i nie jest przełącznikiem zgodności z FIPS.

HTTP/2

OxPHP serwuje HTTP/2 i HTTP/1.1 na tym samym porcie. Protokół jest wybierany per połączenie i nie ma ustawienia włączającego lub wyłączającego HTTP/2:

  • Przez TLS protokół jest negocjowany podczas uzgadniania za pomocą ALPN. OxPHP ogłasza h2, a następnie http/1.1, więc klienci obsługujący HTTP/2 dostają HTTP/2, a wszyscy pozostali w sposób przezroczysty wracają do HTTP/1.1.
  • Bez TLS (h2c) OxPHP wykrywa preambułę połączenia HTTP/2 i serwuje jawne (cleartext) HTTP/2 klientom łączącym się z uprzednią wiedzą (prior knowledge) (np. curl --http2-prior-knowledge). Klienci, którzy nie mówią HTTP/2, nadal korzystają z HTTP/1.1 na tym samym porcie. (Uzgadnianie Upgrade: h2c nie jest używane — HTTP/2 przez połączenie jawne wymaga uprzedniej wiedzy.)

Okna kontroli przepływu HTTP/2 są podniesione powyżej domyślnych wartości protokołu — 8 MB na połączenie i 4 MB na strumień, wobec domyślnych 64 KB — aby uniknąć zatorów przy typowych odpowiedziach PHP, które są zwykle większe niż jedno domyślne okno.

PHP widzi wynegocjowany protokół w $_SERVER['SERVER_PROTOCOL'] ("HTTP/2" lub "HTTP/1.1").

Weryfikacja

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/

Poszukaj HTTP/2 200 w wierszu odpowiedzi.

Limity połączeń

OxPHP stosuje limity HTTP/2 per połączenie, aby ograniczyć skalę obciążenia, jakie pojedyncze połączenie TCP może wywrzeć na pulę workerów PHP. Każdy przyjęty strumień staje się zakolejkowanym żądaniem PHP, więc nieograniczona liczba strumieni z jednego połączenia mogłaby samodzielnie wysycić pulę.

Zmienna Domyślnie Opis
H2_MAX_CONCURRENT_STREAMS PHP_WORKERS_MAX × 4 (min 32) Maksymalna liczba jednocześnie otwartych strumieni na połączenie. Nadmiarowe strumienie otrzymują REFUSED_STREAM
H2_MAX_PENDING_RESET 20 Maksymalna liczba ramek RST_STREAM w kolejce, zanim połączenie zostanie zamknięte (ochrona przed Rapid Reset, CVE-2023-44487)
H2_MAX_HEADER_LIST_BYTES 65536 Maksymalna łączna liczba zdekodowanych bajtów nagłówków na żądanie (zabezpieczenie przed bombą HPACK)
H2_KEEPALIVE_INTERVAL_SECS 20 Liczba sekund między ramkami PING HTTP/2; 0 wyłącza keepalive
H2_KEEPALIVE_TIMEOUT_SECS 10 Liczba sekund oczekiwania na odpowiedź PING przed zamknięciem połączenia

PHP_WORKERS_MAX to maksymalna liczba workerów ustawiona przez PHP_WORKERS. Dla zakresu dynamicznego, takiego jak 4:16, używana jest wartość maksymalna (16). Domyślna wartość skaluje się wraz z liczbą workerów, tak aby uzasadnione równoległe ładowanie stron w ramach jednego połączenia nie tworzyło większej presji na kolejkę, niż pula jest w stanie wchłonąć.

Kompromis

H2_MAX_CONCURRENT_STREAMS jest celowo dostrojony do pojemności puli, a nie do zachowania multipleksowania przeglądarek. Przeglądarki wysyłają dziesiątki żądań o zasoby przez jedno połączenie HTTP/2; te przekraczające limit otrzymują REFUSED_STREAM i są automatycznie ponawiane przez przeglądarkę w kolejnej partii. Dodaje to niewielki narzut opóźnienia na stronach o intensywnym rozgałęzieniu (fan-out) żądań (wiele zasobów na połączenie), ale zapobiega zapełnieniu kolejki żądań PHP przez pojedyncze połączenie. Jeśli Twoja witryna ma wiele stron z dużymi zasobami i wartość domyślna powoduje mierzalne opóźnienia, podnieś jawnie H2_MAX_CONCURRENT_STREAMS, zamiast zwiększać PHP_WORKERS.

Obsługiwane typy kluczy

Plik klucza prywatnego musi zawierać pojedynczy klucz zakodowany w PEM w jednym z następujących formatów:

  • RSA
  • ECDSA (np. prime256v1, secp384r1)
  • Ed25519

Plik certyfikatu może zawierać jeden lub więcej certyfikatów zakodowanych w PEM. Na potrzeby produkcyjne dołącz pełny łańcuch: certyfikat serwera, a po nim wszelkie certyfikaty pośrednie.

Certyfikat samopodpisany na potrzeby developmentu

Wygeneruj samopodpisany certyfikat ECDSA na potrzeby lokalnego developmentu:

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

Następnie skonfiguruj OxPHP, aby korzystał z wygenerowanych plików:

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

Rozwiązywanie problemów

Serwer startuje, ale TLS nie jest aktywny

OxPHP wymaga ustawienia obu zmiennych: TLS_CERT i TLS_KEY. Jeśli ustawiona jest tylko jedna z nich, serwer odmawia startu (TLS_CERT is set but TLS_KEY is missing — both are required to enable TLS (unset TLS_CERT to serve plain HTTP)), a oxphp config --check zgłasza tę samą błędną konfigurację przed wdrożeniem. Jeśli żadna nie jest ustawiona, zwykły HTTP jest normalnym, cichym ustawieniem domyślnym — ale jeśli zmienne są obecne i puste (podstawienie ${VAR:-}, które wyrenderowało się jako puste, np. uszkodzony mount sekretu), ostrzeżenie przy starcie (TLS variable(s) set but empty — TLS disabled, serving plain HTTP) pozostawia ślad. Ostrzeżenie jest logowane również wtedy, gdy TLS_MIN_VERSION=1.3 jest ustawione, a TLS nie jest włączony. Potwierdź, że obie zmienne są ustawione:

bash
docker exec <container> env | grep TLS
Błąd TLS_KEY: no private key found in ... przy starcie

Plik klucza jest pusty, uszkodzony lub zawiera wyłącznie certyfikat. Sprawdź, czy plik klucza zawiera blok -----BEGIN ... PRIVATE KEY-----:

bash
grep "PRIVATE KEY" key.pem

Jeśli klucza brakuje, wygeneruj ponownie parę certyfikat–klucz.

Certyfikat nie ładuje się przy starcie

Plik certyfikatu jest pusty lub uszkodzony, a start jest przerywany błędem z warstwy TLS. Sprawdź, czy plik certyfikatu zawiera co najmniej jeden blok -----BEGIN CERTIFICATE-----:

bash
grep "BEGIN CERTIFICATE" cert.pem
Klienci widzą błąd łańcucha certyfikatów

Serwer wysyła tylko certyfikat liścia (leaf), bez certyfikatów pośrednich. Połącz pełny łańcuch w jeden plik PEM:

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

Następnie ustaw TLS_CERT=./fullchain.pem.

Certyfikat wygasł

OxPHP odczytuje pliki certyfikatów przy starcie i przechowuje je w pamięci. Odnowienie certyfikatu na dysku nie ma żadnego efektu, dopóki serwer nie zostanie zrestartowany.

Rozwiązanie: Zrestartuj OxPHP po odnowieniu certyfikatu. Zautomatyzuj to za pomocą swojego narzędzia do odnawiania certyfikatów (np. opcji --deploy-hook w certbocie).

Nie można serwować HTTP i HTTPS na tym samym porcie

OxPHP nasłuchuje na pojedynczym porcie. Aby obsługiwać oba protokoły jednocześnie, użyj reverse proxy (Caddy, Traefik, nginx), które zajmie się przekierowaniem z HTTP na HTTPS, albo uruchom drugą instancję OxPHP na porcie 80 dedykowaną do przekierowywania ruchu.

Przykład dla Dockera

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

Dobre praktyki

  • Dołączaj certyfikaty pośrednie do łańcucha PEM. Umieść certyfikat serwera na początku, a po nim certyfikaty pośrednie w kolejności, tak aby klienci mogli zweryfikować pełną ścieżkę zaufania.
  • Automatyzuj odnawianie certyfikatów. Użyj certbota lub acme.sh, aby odnawiać certyfikaty przed wygaśnięciem, a następnie zrestartuj OxPHP w celu wczytania nowych plików.
  • Użyj reverse proxy do przekierowania z HTTP na HTTPS. OxPHP nie serwuje HTTP i HTTPS na tym samym porcie jednocześnie.

Uwagi

  • OxPHP nie zależy od OpenSSL. TLS jest obsługiwany przez wbudowaną implementację, co eliminuje częste źródło CVE w zewnętrznych bibliotekach.
  • Pliki certyfikatu i klucza są odczytywane wyłącznie przy starcie. Aktualizacja certyfikatów na dysku wymaga restartu serwera.

Zobacz również