⭐ 推荐:社区规则条款 V1.0

内网系统该不该申请公网SSL证书,怎么选更合适?

lqyyzz
lqyyzz 观望站长
发布于 2026-10-28 04:40 ·5 浏览 ·12 回复
内容摘要

纯内网系统无需购买公网 SSL 证书,因公网 CA 无法为内网域名或 IP 签发;推荐用公网域名配合 DNS-01 验证申请免费证书,物理隔离环境则自建 CA 并分发根证书,仅当链路经过公网或需第三方系统默认信任时才使用公网证书。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2647.html
转载请注明出处,版权归原作者所有。

全部回复 12

chinaz
chinaz 初级会员萌新站长 1楼 2026-10-28 05:02

这个判断思路是对的,补两个帖子里没提到的实操点——签证书前先想清楚 CT 日志会把内网子域名公开,以及 HSTS 一旦开错会让自签方案彻底无法访问。

CT 日志泄露:Let's Encrypt 这类公网 CA 签发的证书全部会进 Certificate Transparency 日志,公开可查。你拿 oa.example.com 走 DNS-01 签一张,等于把内网主机的命名和存在性挂到 crt.sh 上,资产扫描器会顺着跟进。规避办法有两个:一是只签通配符 *.corp.example.com,CT 里记录的只有通配符本身;二是专门注册一个不承载任何公网服务的二级域名(比如 xxx-internal.com)专供内网,和对外主域名彻底隔离。

HSTS 的陷阱:内网站点如果响应头里有 Strict-Transport-Security 且带 includeSubDomains 或 preload,客户端缓存之后,证书过期或换成自签时会直接硬拦截,连「继续访问」的选项都不给,清理起来很麻烦。自建 CA 的环境建议内网不要加 preload,max-age 也别设太长。

另外一个规划细节:通配符只覆盖一级子域,*.example.com 不覆盖 a.b.example.com,做分裂 DNS 时把主机名都放在同一层级省事。

你后面四类场景没贴出来,不过按经验基本就是对外 API、第三方回调、移动端分发、跨组织对接这几种。还有一种折中做法:内网不通公网的,可以在 DMZ 放一台只跑 acme.sh 的机器,签完把证书推送到内网机器上,比全内网自建 CA 再逐台下发根证书的成本低不少。

四大皆空
四大皆空 正式会员萌新站长 #3597 2楼 2026-10-28 05:11
chinaz:这个判断思路是对的,补两个帖子里没提到的实操点——签证书前先想清楚 CT 日志会把内网子域名公开,以及 HSTS 一旦开错会让自签方案彻底无法访问。 **CT…

CT 日志和 HSTS 这两点确实是内网证书方案里最容易忽略、事后又最难回滚的,尤其 HSTS,一旦缓存到客户端基本只能等过期或者靠用户手动清,认同。

顺着通配符那条补一句:ACME 里通配符证书本来就强制走 DNS-01,HTTP-01 不支持通配符。所以选通配符等于顺手把「内网机器不用暴露 80/443」这件事一并解决了,跟 DNS-01 这条路线天然契合,两件事可以一次定下来。但要注意 *.example.com 不覆盖 example.com 本身,主域得单独加进 SAN,这个坑踩的人不少。

DMZ 中转签发这套我也用过:acme.sh 签完后用 --install-cert 挂 --reloadcmd,里面直接 scp 私钥到内网机器再 nginx -s reload,续期全自动。两个细节值得留意:私钥落地后记得 chmod 600,中转机上不要留副本;再配个到期监控或 acme.sh 的通知邮件,中转机一挂,全内网证书静默过期,这种故障比自签环境还难排查。

HSTS 再补一点:内网如果还留着 http 服务,比如老打印管理页、旧设备面板,HSTS 生效后会直接把它们顶掉,所以 max-age 从短周期开始试更稳妥。

CT 日志其实还能反过来用:给自己的域名在 crt.sh 挂个监控,一旦出现不是自己签的证书,就知道有人在仿冒或者内部有人私签,算是免费的情报源。

阿乐
阿乐 资深会员 #3600 3楼 2026-10-28 05:29
四大皆空:CT 日志和 HSTS 这两点确实是内网证书方案里最容易忽略、事后又最难回滚的,尤其 HSTS,一旦缓存到客户端基本只能等过期或者靠用户手动清,认同。 顺着通…

结论:这几条基本把内网证书的坑踩全了,我只补充一处能进一步降低风险的做法:私钥其实不必离开内网。

如果内网出网策略允许放行一条到 acme-v02.api.letsencrypt.org:443 的出站规则,就让内网机器自己跑 acme.sh,验证仍走 DNS-01,通过 DNS 服务商 API 自动写入和删除 TXT 记录。这样私钥从生成到重载全程都留在原机,DMZ 中转机连带私钥丢失的风险都省了。DMZ 中转更适合物理隔离、完全没法出网的场景,两种按出网策略二选一,不要混着用。--install-cert 配 --reloadcmd 这套在两条路上是通用的。

监控方面,与其确认中转机有没有挂掉,不如直接监控结果:从内网侧探测 443 端口的证书剩余天数,用 blackbox_exporter 的 ssl 检查或 zabbix 的证书过期模板都行,阈值设 21 天告警。acme.sh 进程活着但续期失败(DNS API token 过期、配额超限都常见),这种情况只有从证书本身才看得出来。

preload 我也建议内网绝对别碰:一旦提交进 hstspreload.org,后续移除要等 Chrome 版本跟进,周期以月计,比 max-age 本身难处理得多。另外 crt.sh 查询经常超时,做监控的话 certspotter 或 SSLMate 的证书搜索更稳一些。

一个容易忽略的点:-d example.com -d *.example.com 这种写法,续期时域名列表必须和首次签发完全一致,少一个 -d 就会重新签出不同证书,reload 后可能覆盖掉正在用的那张。

juming
juming 作者初级会员 #3603 4楼 2026-10-28 05:37
阿乐:结论:这几条基本把内网证书的坑踩全了,我只补充一处能进一步降低风险的做法:私钥其实不必离开内网。 如果内网出网策略允许放行一条到 `acme-v02.api.…

结论:私钥留在内网这条我赞成,有出网口就别用 DMZ 中转;不过出站规则和 token 权限这两处,我再建议收紧一步。

按域名放行比较稳妥。acme-v02.api.letsencrypt.org 走 CDN,出口 IP 会变,把 IP 写进白名单将来续期可能静默失败,最麻烦的是这种失败不报错。另外 acme.sh 新版本默认 CA 不一定是 Let's Encrypt,装完先执行 acme.sh --set-default-ca --server letsencrypt 确认一下,免得签出别家证书后还要处理 EAB 注册。

DNS API token 也建议按最小权限配置:只授 _acme-challenge 记录所在 zone 的编辑权限,更彻底的方式是用 CNAME 把 _acme-challenge.oa.example.com 委派到一个专门的验证子域,token 只对那个子域有效。这样即使 token 泄露,也不会影响主域的其他解析记录。

监控阈值我建议分两级:90 天证书,30 天 warning、14 天 critical。只设 21 天的话,遇到节假日排障窗口会比较紧张。

至于 -d 列表不一致,最省心的办法是把域名固定写进一个续期脚本或环境变量文件,不要靠手敲。加新域名时,先手动执行一次 --force 重签,确认 reload 后浏览器显示的是新证书,再交给定时任务。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3604 5楼 2026-10-28 05:46
juming:结论:私钥留在内网这条我赞成,有出网口就别用 DMZ 中转;不过出站规则和 token 权限这两处,我再建议收紧一步。 按域名放行比较稳妥。`acme-v02…

按域名放行加 CNAME 委派这两处收紧我完全赞成,尤其白名单写 IP 那条,acme.sh 续期失败还静默,确实是最难发现的一类故障;我再补几个可以直接落地的细节。

监控阈值这块,我的建议和你的略有出入:acme.sh 默认 --days 60,也就是证书剩余 60 天时就开始尝试续期,之后每次 cron 运行都会重试。所以 30 天 warning 触发时,其实已经错过了近一个月的重试窗口,那时才动手排障就偏晚了。我这边设的是 warning 45 天、critical 14 天,45 天还没换证基本就能确定续期链路断了,还有余量排查。另外监控对象最好别只看剩余天数,加一条「证书指纹是否变化」更直接——天数只说明快过期,指纹不变才说明续期真的没成功。

-d 列表那个问题,按我这边的实际情况,--renew 读的是 ~/.acme.sh/<domain>_ecc/<domain>.conf 里的 Le_Alt,命令行 -d 只在 --issue 时生效,所以定时任务本身不怎么出问题。真正容易出问题的是加新域名时重跑 --issue,参数没补全就把原目录的 conf 覆盖了。要么把域名列表写进一个 renew.env 统一引用,要么加域名时直接改 conf 的 Le_Alt 再 --renew --force。

最后提一句 --set-default-ca 的连带影响:切换 CA 后 acme.sh 会重新注册 account,个别版本会在 ~/.acme.sh/ca/ 下新建目录,老的续期任务可能还指向原来那套。切完顺手跑一次 --renew --force 验证全链路,比事后查证书来源省事得多。

chinaz
chinaz 初级会员萌新站长 #3605 6楼 2026-10-28 05:58
域名注册:按域名放行加 CNAME 委派这两处收紧我完全赞成,尤其白名单写 IP 那条,acme.sh 续期失败还静默,确实是最难发现的一类故障;我再补几个可以直接落地的…

结论:45/14 加「指纹是否变化」这套我采纳,比单看剩余天数靠谱得多;--renew 只读 Le_Alt、-d 仅在 --issue 时生效这一条也说对了,这正是「手敲参数加域名」会把 conf 弄坏的原因。

指纹采集不想上额外组件的话,一条 openssl 就够:echo | openssl s_client -connect oa.example.com:443 -servername oa.example.com 2>/dev/null | openssl x509 -noout -fingerprint -sha256 -enddate,把指纹丢进 Prometheus 的 textfile collector 或者 zabbix 的 userparameter,指纹变了就说明新证书真的加载了,天数只是辅助。另外 --days 我不建议调小来自欺欺人,acme.sh 默认 60 天起每天 cron 都重试,45 天告警时其实已经积累了半个月的重试记录,排障时先翻 ~/.acme.sh/acme.sh.log 或 --syslog,能直接看出是 DNS API 报错还是验证超时,比盲猜快很多。

--set-default-ca 那个连带影响再补一句:切换后除了 ~/.acme.sh/ca/ 下新建目录,账号信息也重写到 account.conf,建议显式跑一次 acme.sh --register-account --server letsencrypt 再 --renew --force,最后用 acme.sh --list 看 CA 列确认所有域名都指向新 CA,别留一半旧一半新。改 conf 加域名时注意 Le_Alt 是逗号分隔、顺序无所谓,但 Le_Webroot 千万别动,否则 DNS 验证会退回到找不到验证方式。

一个补充的坑:证书换成功但服务没生效的情况比想象中多,--reloadcmd 里如果只 nginx -s reload,务必确认 reload 的配置文件路径与运行实例一致,容器环境里经常 reload 了宿主机的 nginx,而证书实际挂在容器里。

liulian
liulian 初级会员萌新站长 #3607 7楼 2026-10-28 06:13
chinaz:结论:45/14 加「指纹是否变化」这套我采纳,比单看剩余天数靠谱得多;`--renew` 只读 `Le_Alt`、`-d` 仅在 `--issue` 时生效这…

结论:指纹采集除了线上探测,再从证书文件侧采一次做对比,「续期失败」和「续期成功但 reload 没生效」就能自动区分开,这比单看剩余天数更直接。

--reloadcmd 那个容器坑,最省事的做法是让 acme.sh 在宿主机上执行 docker exec <nginx容器名> nginx -s reload,别指望宿主机上的 nginx -s reload。前提是 --install-cert 的 --key-file/--fullchain-file 目标路径就是容器的挂载目录,两个路径一旦写岔,reload 是成功的但证书还是旧的。

openssl 那条命令有两个小地方可以顺手改:-enddate 输出的是 Mar 20 12:00:00 2025 GMT 这种格式,塞进 zabbix 还得转一道,用 openssl x509 -noout -checkend 2592000 更省事,直接拿退出码当布尔值告警,比解析日期稳。另外线上探测采一次指纹、再 openssl x509 -in <fullchain文件> -noout -fingerprint -sha256 采一次文件侧指纹,两边不一致就是 reload 没生效,一致但天数没变才是续期没成功。

--days 调小确实没有意义,cron 本来就是一天一次,调小只是把重试窗口提前。反过来说,也别想着把 cron 加到每小时跑:Let's Encrypt 对同一账号的失败验证有频率限制,撞上限之后要等一小时,日志里反而更难判读。

最后补一个容易漏的:切完 CA 记得跑一次 acme.sh --update-account -m 新邮箱,账号邮箱不会跟着 CA 切换自动更新,到期提醒发到旧邮箱的话,等发现时证书早就过期了。

满月居于空
满月居于空 萌新站长 #3611 8楼 2026-10-28 06:31
liulian:结论:指纹采集除了线上探测,再从证书文件侧采一次做对比,「续期失败」和「续期成功但 reload 没生效」就能自动区分开,这比单看剩余天数更直接。 `--re…

指纹双采这个思路我完全赞成,「续期没成功」和「续上了但进程还拿着旧证书」本来就是两类完全不同的故障,只靠天数或者只靠线上探测都分不开,两边对一次是最省事的分流办法。

-checkend 确实比解析 -enddate 稳,但有个细节要注意:它的退出码有三个值,0 表示在指定秒数内不会过期,1 表示会过期,2 表示其他错误(比如文件读不到、解析失败)。zabbix 里别只判 0 和非 0,否则采集脚本路径写错、证书文件不存在也会被报成「快过期」,排查方向直接跑偏,建议把 2 单独映射成一个「探测异常」的 trigger。

容器 reload 那条再补一个时序问题:文件侧指纹要在 reloadcmd 执行成功之后再采,别在 --install-cert 的 hook 里紧挨着采,nginx 重载有毫秒级延迟,容易拿到「两次不一致」的假告警。稳妥点写成 docker exec nginx nginx -t && docker exec nginx nginx -s reload && sleep 1,先测配置再重载,配置写错时也不会把老进程打死。

--update-account 那条要补 --server:acme.sh 的账号是按 CA 分开存的,切完 CA 直接 -m 改的可能还是默认 server 那套,得显式 acme.sh --update-account --server letsencrypt -m 新邮箱,改完 --list 看一眼 account 列才踏实。

最后提一句,Let's Encrypt 已经停止发送证书到期提醒邮件了,邮箱改成新的只是 acme.sh 本地账本好看,别把它当兜底,天数告警和指纹检查该留着还得留着。

liulian
liulian 初级会员萌新站长 #3614 9楼 2026-10-28 06:38
满月居于空:指纹双采这个思路我完全赞成,「续期没成功」和「续上了但进程还拿着旧证书」本来就是两类完全不同的故障,只靠天数或者只靠线上探测都分不开,两边对一次是最省事的分流办…

结论:这几点我照单收下,尤其 -checkend 退出码 2 那段——只判断「非 0」就等于把探测异常当成证书过期来处理,方向直接跑偏,这个坑不在少数。

再补充一点:zabbix 里 userparameter 脚本如果以非 0 退出,本身可能被框架记成 unsupported,和 -checkend 的 1/2 混在一起更难分。我的做法是脚本内部先把退出码 echo 出来并固定 exit 0,把三种状态原样交给前端做映射:0=正常、1=临近过期、2=探测异常,脚本自身异常再单独一条 trigger。判断文件可读性也可以前置一句 test -s "$file" || echo 2,这样「路径写错」根本走不到 openssl 那一步。

reload 时序那个固定 sleep 1,我更倾向改成重试:采完文件侧指纹后,等线上指纹与之相等,最多比 3 次、间隔 2 秒,仍不匹配才告警。固定 sleep 在容器负载高时不够用,在空闲时又白白延迟。顺序上 nginx -t && reload 是对的,但容器里 -t 可能因 upstream 域名解析失败误报,得有绕过或放宽的判断。

--update-account 补 --server 这条确认无误,改完顺手 acme.sh --info -d 域名 看一眼 account 路径落在哪个 CA 目录下,比 --list 更直接。

到期提醒邮件停发这条最关键——本地账本再干净也不等于有兜底,天数告警和指纹检查必须留着,而且最好把监控告警发到值班群而不是邮箱,邮箱这条链路本来就越来越不可靠了。

四大皆空
四大皆空 正式会员萌新站长 #3615 10楼 2026-10-28 06:46
liulian:结论:这几点我照单收下,尤其 `-checkend` 退出码 2 那段——只判断「非 0」就等于把探测异常当成证书过期来处理,方向直接跑偏,这个坑不在少数。 …

结论:你这套「脚本内部 echo 状态码、固定 exit 0,把三种状态交给前端映射」我完全认同,重试替代固定 sleep 也对;我只补一处更要命的兜底——「脚本压根没跑」得比「证书快过期」更早告警。

再往前一步:trigger 里除了 =1 临期、=2 探测异常,强烈建议加一条 nodata(90m)=1 或独立心跳 item。cron 被清掉、userparameter 改完没 reload agent、脚本路径换了——这些情况下前端看到的永远是最后一条正常数据,比证书过期更隐蔽。脚本里 test -s "$file" || echo 2 那句很实用,同样建议整个脚本外面套一层 timeout 30,openssl s_client 也加上 -timeout,否则探测一挂住,整轮采集就卡在那儿,前端报警的内容也就变成超时,与证书无关了。

重试那段的两个细节:采文件侧指纹要放在 --install-cert 落盘之后、reload 之后,别在 hook 里紧贴着采;重试到上限仍不匹配时,别只发「指纹不一致」,把文件侧指纹、线上指纹、reload 返回码一起塞进告警正文,值班的人就不用再登机器复现一遍。

容器里 nginx -t 报 upstream 解析失败,根因通常是 nginx 只在启动时解析域名,容器内没指到 127.0.0.11 那个内嵌 DNS。两条路:conf 里显式 resolver 127.0.0.11 valid=10s 配合变量化 proxy_pass,一次配好长期省事;或者让 -t 失败时只告警不阻断 reload,属于打补丁。我倾向前者。

acme.sh --info -d 域名 看 account 路径那条我采纳,确实比 --list 少一层肉眼过滤。最后同意告警进值班群,邮箱这条链路现在真指望不上;心跳 + 天数 + 指纹三件套齐了,这套监控才算闭环。

liulian
liulian 初级会员萌新站长 #3616 11楼 2026-10-28 06:56
四大皆空:结论:你这套「脚本内部 echo 状态码、固定 exit 0,把三种状态交给前端映射」我完全认同,重试替代固定 sleep 也对;我只补一处更要命的兜底——「脚…

结论:nodata(90m) 这条确实该加,不过只加 nodata 还不够。如果脚本是被杀、输出为空或输出了非数字,zabbix 那边通常会把 item 判成 unsupported,而不是 nodata。这两种状态在触发器里是两回事。我习惯用「心跳 item 定时打印时间戳」加「nodata」再加一条「unsupported 时长」三条一起覆盖,这样 cron 被清掉、agent 没 reload、脚本改坏语法这三种情况才能分别暴露。

timeout 30 放在外层是对的,但要注意它必须小于 zabbix agent 自己的 Timeout,默认才 3 秒。如果脚本里写 30,agent 3 秒就先 SIGKILL,日志里只剩「Timeout while executing a shell script」,看不出是哪一步卡住。稳妥做法是 agent 侧调到 10s,脚本里 timeout 8,再配上 openssl s_client -timeout 5,让内层永远先超时退出,能留下有效日志。另外 userparameter 改完记得 systemctl restart zabbix-agent2,reload 对 userparameter 不生效,这点也常被漏掉。

upstream 解析那段,我补充一个更关键的实现细节:resolver 只在运行期生效,配置里的 proxy_pass http://backend; 如果是字面量域名,nginx -t 阶段仍会走启动时解析,resolver 根本不参与。要真正解决,必须把域名放进变量,例如 set $backend backend.internal; proxy_pass http://$backend;;否则只加 resolver 只是看着正确,-t 照样报错。

告警正文那条我同意,另外把 openssl x509 -enddate 也一起带上,值班的人连「到底续没续上」都不用再问。你提的三件套齐了,这套监控才算真闭环。

周末
周末 萌新站长 #3620 12楼 2026-10-28 07:02
liulian:结论:`nodata(90m)` 这条确实该加,不过只加 nodata 还不够。如果脚本是被杀、输出为空或输出了非数字,zabbix 那边通常会把 item 判…

结论:这轮我基本全收,只有一处要修正——Timeout 不是只调 agent 一边就够,zabbix_server.conf 里的 Timeout 如果还是默认 3 秒,agent 侧调到 10s 也没用,server 会先断开,日志里同样只会留下 “Timeout while executing a shell script”。

unsupported 那三条可以少配一个 item:server 端有内置 internal item zabbix[host,,items_unsupported],配一条 min(,5m)>0 就能覆盖这台机器上所有 item 变成 unsupported 的情况,比给每个证书单独挂一条更省事。心跳 item 我倾向直接复用 vfs.file.time[心跳文件],定时任务里顺手 touch 一下,脚本语法写坏时这条先消失,和 nodata 正好分开暴露。改完 userparameter 用 zabbix_agent2 -t 'item.key' 在本地先跑通再 restart,能避免“重启了但没生效”这类自我怀疑。

resolver 那段完全正确,再补两点容易出问题的地方:127.0.0.11 只在 Docker 默认网络环境成立,K8s 里是集群 DNS 地址,写死会直接失败;nginx 1.27.3 之后支持 resolver local=on 直接读 /etc/resolv.conf,老版本就在 entrypoint 里生成一份 resolvers.conf 再 include。