TLS

OxPHP 原生处理 TLS 终止,无需反向代理或外部 SSL 库。一旦配置完成,服务器便会接受 HTTPS 连接并自动协商可用的最佳协议。

工作原理

要启用 TLS,将 TLS_CERTTLS_KEY 设置为指向你的 PEM 编码证书文件和私钥文件。二者都设置后,服务器会在 LISTEN_ADDR 指定的地址上监听 HTTPS 连接。

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_CERTTLS_KEY 才能启用 TLS
TLS_KEY (未设置) PEM 编码私钥文件的路径
TLS_MIN_VERSION 1.2 接受的最低 TLS 协议版本:1.21.3。任何其他值都会导致启动错误
LISTEN_ADDR 0.0.0.0:80 监听的地址和端口。使用 TLS 时改为 0.0.0.0:443

如果只设置了 TLS_CERTTLS_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 的内部策略)可以提高下限:

bash
TLS_MIN_VERSION=1.3

将下限设为 1.3 后,TLS 1.2 的 ClientHello 会在握手期间被拒绝,并返回 protocol_version 警报;TLS 1.3 客户端不受影响。

无效值(1.11.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 连接前导,并向以预先知晓方式连接的客户端(例如 curl --http2-prior-knowledge)提供明文 HTTP/2。不使用 HTTP/2 的客户端会在同一端口上继续使用 HTTP/1.1。(不使用 Upgrade: h2c 握手——明文上的 HTTP/2 要求预先知晓。)

HTTP/2 的流控窗口被提高到高于协议默认值——每个连接 8 MB、每个流 4 MB,而默认为 64 KB——以避免在典型的 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 帧的最大数量(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)。该默认值随工作进程数量伸缩,使得单个连接上正当的并发页面加载不会产生超过进程池所能吸收的排队压力。

权衡

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_CERTTLS_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,也会记录一条警告。确认两个变量都已设置:

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。可用你的证书续订工具自动完成这一步(例如 certbot 的 --deploy-hook 选项)。

无法在同一端口上同时提供 HTTP 和 HTTPS

OxPHP 只在单个端口上监听。要同时支持两种协议,请使用能处理 HTTP 到 HTTPS 重定向的反向代理(Caddy、Traefik、nginx),或者运行第二个专门用于重定向流量的 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 的一个常见来源。
  • 证书和密钥文件仅在启动时读取。更新磁盘上的证书需要重启服务器。

参见