TLS
OxPHP は TLS 終端をネイティブに処理します。リバースプロキシや外部の SSL ライブラリは必要ありません。設定を済ませれば、サーバーは HTTPS 接続を受け付け、利用可能な最適なプロトコルを自動的にネゴシエートします。
仕組み
TLS を有効にするには、TLS_CERT と TLS_KEY を、PEM 形式でエンコードされた証明書ファイルと秘密鍵ファイルを指すように設定します。両方が設定されると、サーバーは LISTEN_ADDR で指定されたアドレスで HTTPS 接続を待ち受けます。
TLS ハンドシェイクは、あらゆる HTTP 処理の前に行われます。
- TCP 接続が
LISTEN_ADDRに到着します。 - サーバーは設定された証明書と鍵を使って TLS ハンドシェイクを実行します。
- プロトコルネゴシエーション(ALPN)が、クライアントのサポート状況に基づいて HTTP/2(
h2)または HTTP/1.1 を選択します。 - 暗号化された接続が HTTP 層に引き渡され、通常のリクエスト処理が行われます。
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 の一方だけが設定されている場合、サーバーは起動を拒否します。片方だけ設定された状態はほぼ確実に変数名のタイプミスであり、HTTPS 用のポートで黙って平文の HTTP を提供してしまうとフェイルオープンになるためです。空の値(${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 にすると、TLS 1.2 の ClientHello はハンドシェイク中に 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 暗号スイート(ECDHE 鍵交換を伴う AES-GCM と ChaCha20-Poly1305)のみを同梱しています。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 接続プリフェイスを検出し、事前知識(prior knowledge)で接続してくるクライアント(例:
curl --http2-prior-knowledge)に平文の HTTP/2 を提供します。HTTP/2 を話さないクライアントは、同じポートで HTTP/1.1 を使い続けます。(Upgrade: h2cハンドシェイクは使用されません。平文上の HTTP/2 には事前知識が必要です。)
HTTP/2 のフロー制御ウィンドウは、プロトコルのデフォルトより引き上げられています。デフォルトの 64 KB に対して、接続ごとに 8 MB、ストリームごとに 4 MB です。これは、通常デフォルトのウィンドウ 1 つより大きい典型的な 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 上限を適用し、1 本の TCP 接続が PHP ワーカープールに及ぼせる増幅を抑えます。受け付けられた各ストリームはキューに入る PHP リクエストになるため、1 つの接続からのストリーム数が無制限だと、それだけでプールを飽和させてしまいます。
| 変数 | デフォルト | 説明 |
|---|---|---|
H2_MAX_CONCURRENT_STREAMS |
PHP_WORKERS_MAX × 4(最小 32) |
接続ごとに同時に開けるストリームの最大数。超過したストリームは REFUSED_STREAM を受け取ります |
H2_MAX_PENDING_RESET |
20 |
接続が閉じられるまでにキューに入れられる RST_STREAM フレームの最大数(CVE-2023-44487 Rapid Reset 対策) |
H2_MAX_HEADER_LIST_BYTES |
65536 |
リクエストごとにデコードされるヘッダーの合計バイト数の最大値(HPACK ボム対策) |
H2_KEEPALIVE_INTERVAL_SECS |
20 |
HTTP/2 PING フレームの間隔(秒)。0 でキープアライブを無効化します |
H2_KEEPALIVE_TIMEOUT_SECS |
10 |
接続を閉じるまでに PING の応答を待つ秒数 |
PHP_WORKERS_MAX は PHP_WORKERS で設定されるワーカー数の最大値です。4:16 のような動的範囲では、最大値(16)が使われます。デフォルト値はワーカー数に応じてスケールするため、1 つの接続上での正当な同時ページ読み込みが、プールが吸収できる以上のキュー圧力を生むことはありません。
H2_MAX_CONCURRENT_STREAMS は、ブラウザの多重化の挙動ではなく、プールの容量に合わせて意図的にチューニングされています。ブラウザは 1 本の HTTP/2 接続で数十件のアセットリクエストを送りますが、上限を超えたものは REFUSED_STREAM を受け取り、ブラウザによって次のバッチで自動的に再試行されます。これはファンアウトの多いページ(接続あたりのアセットが多いページ)でわずかなレイテンシのペナルティを加えますが、1 本の接続が PHP のリクエストキューを埋め尽くすのを防ぎます。大量のアセットを持つページが多く、デフォルト値が計測可能なレイテンシを引き起こす場合は、PHP_WORKERS を増やすのではなく、H2_MAX_CONCURRENT_STREAMS を明示的に引き上げてください。
サポートする鍵の種類
秘密鍵ファイルには、以下のいずれかの形式の PEM 形式でエンコードされた鍵が 1 つだけ含まれている必要があります。
- RSA
- ECDSA(例:prime256v1、secp384r1)
- Ed25519
証明書ファイルには、PEM 形式でエンコードされた証明書を 1 つ以上含められます。本番環境では、フルチェーンを含めてください。まずサーバー証明書、続いて中間証明書を並べます。
開発用の自己署名証明書
ローカル開発用に自己署名の 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 が有効でないのに TLS_MIN_VERSION=1.3 が設定されている場合にも警告がログに記録されます。両方の変数が設定されているか確認してください。
docker exec <container> env | grep TLS起動時に TLS_KEY: no private key found in ... エラーが出る
鍵ファイルが空、破損している、または証明書しか含まれていません。鍵ファイルに -----BEGIN ... PRIVATE KEY----- ブロックが含まれているか確認してください。
grep "PRIVATE KEY" key.pem鍵が見つからない場合は、証明書と鍵のペアを再生成してください。
起動時に証明書の読み込みに失敗する
証明書ファイルが空または破損しており、TLS 層からのエラーで起動が中止されます。証明書ファイルに -----BEGIN CERTIFICATE----- ブロックが少なくとも 1 つ含まれているか確認してください。
grep "BEGIN CERTIFICATE" cert.pemクライアントに証明書チェーンのエラーが表示される
サーバーが中間証明書を伴わず、リーフ証明書だけを送信しています。フルチェーンを 1 つの PEM ファイルに連結してください。
cat cert.pem intermediate.pem > fullchain.pemその後、TLS_CERT=./fullchain.pem を設定します。
証明書の有効期限が切れている
OxPHP は起動時に証明書ファイルを読み込み、メモリ上に保持します。ディスク上で証明書を更新しても、サーバーを再起動するまで反映されません。
対処法: 証明書の更新後に OxPHP を再起動してください。証明書更新ツール(例:certbot の --deploy-hook オプション)でこれを自動化しましょう。
同じポートで HTTP と HTTPS を提供できない
OxPHP は 1 つのポートで待ち受けます。両方のプロトコルを同時にサポートするには、HTTP から HTTPS へのリダイレクトを処理するリバースプロキシ(Caddy、Traefik、nginx)を使うか、リダイレクト専用の 2 つ目の 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 におけるボリュームマウントと証明書の管理