frankenphp中hsts的includesubdomains必须与max-age>0配合使用,单独配置无效;max-age至少设为31536000,includesubdomains拼写须为小写且完整包含在单字符串中,请求须经https、域名及子域证书需全覆盖。

FrankenPHP里HSTS的includeSubDomains必须和max-age一起用
单独加includeSubDomains没效果——FrankenPHP(实际是它内嵌的Caddy)只在max-age大于0时才生成Strict-Transport-Security头,includeSubDomains只是附加指令。如果你只写includeSubDomains但漏了max-age,curl -I 看不到任何HSTS头。
常见错误现象:
- 配置写了
includeSubDomains,但浏览器开发者工具 Network → Response Headers 里压根没有Strict-Transport-Security - 子域名访问仍被允许降级到HTTP,HSTS策略未生效
正确做法:
-
max-age至少设为31536000(1年),太小会导致策略很快过期,失去防护意义 -
includeSubDomains拼写必须严格为小写includeSubDomains(不是includeSubdomains或includesubdomains),Caddy对大小写敏感 - 整个值必须作为单个字符串写进
header指令,不能拆成多行或用变量拼接
Caddyfile里配HSTS子域的三要素缺一不可
FrankenPHP通过Caddyfile控制HTTP响应头,HSTS子域生效依赖三个硬性条件同时满足:
- 请求必须走HTTPS(即端口443且TLS已建立),HTTP明文请求不会触发HSTS头输出
- Caddyfile中
header指令必须位于启用HTTPS的server块内(比如yourdomain.com或泛域名块),不能写在全局{ }里 - 域名解析和证书必须覆盖主域及所有目标子域(例如
example.com和*.example.com都得在Let's Encrypt证书的SAN列表里)
典型配置示例(放在Caddyfile对应站点块中):
example.com {
header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
php_server
}
注意:preload可选,但一旦加上,就得提交到浏览器HSTS预加载列表,后续无法轻易撤回。
子域HSTS生效前最容易被忽略的两个验证点
很多人配完以为万事大吉,结果子域名(如api.example.com)仍不强制HTTPS,问题往往出在这两处:
- DNS解析没到位:子域名A记录或CNAME必须指向同一台FrankenPHP服务器IP,否则请求根本没走到你的Caddy实例,自然不会收到HSTS头
- 证书没包含通配符:Let's Encrypt默认只给主域名发证书,
*.example.com需显式申请(Caddy会自动处理,但前提是SERVER_NAME环境变量或Caddyfile里声明了该域名)
验证命令(分别对主域和子域执行):
curl -I https://example.com | grep Strict curl -I https://api.example.com | grep Strict
两者都必须返回带includeSubDomains的完整头,才算真正生效。
误开includeSubDomains又没覆盖全部子域的后果很直接
如果主域名example.com开了includeSubDomains,但某个子域(比如legacy.example.com)还没迁到HTTPS、也没配证书,那用户首次访问legacy.example.com就会失败——浏览器收到主域HSTS策略后,会拒绝任何HTTP连接,且不给降级提示。
所以真实生产节奏应该是:
- 先只对主域开HSTS(不带
includeSubDomains),观察几周确认无异常 - 再逐个上线子域HTTPS,确保每个子域都能稳定返回有效证书和HSTS头
- 最后才打开
includeSubDomains,并把max-age从短周期(如300)逐步拉长
这个过程没法跳步,HSTS是单向开关,开了就收不回,尤其子域部分。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











