# BeeSSL (/) BeeSSL 是元磁科技(北京)有限公司运营的 HTTPS 证书服务,定位是「为 Agent 设计的证书服务」。 三条产品线,各自的入口: - **免费证书签发**(https://beessl.cn/free-cert)— 基于 Let's Encrypt,通过 ACME 协议签发,支持泛域名。 - **证书检测与监控**(https://beessl.cn/ssl-checker)— 不限于本站签发的证书,任意 CA 签的都能检测。免登录。 - **MCP Server**(https://beessl.cn/mcp-server)— 让 AI 助手直接完成上面两件事,无需人工操作界面。 站点语言为简体中文,主域名 https://beessl.cn。 # 免费 HTTPS 证书申请 (/free-cert) 自助签发免费 HTTPS 证书,费用为 0。 **流程**:提交域名 → 系统返回一条 DNS TXT 记录 → 在域名的 DNS 服务商处添加该记录 → 系统检测记录生效后向 Let's Encrypt 完成 ACME 验证并签发。全程无人工审核。 **验证方式是 DNS-01**,不是 HTTP-01。这意味着签发不要求域名当前已经指向可访问的服务器,也因此才能签发泛域名。 **泛域名的关键约束**:`*.example.com` 不覆盖 `example.com` 本身。这是 RFC 6125 的规则,不是本站的限制。所以需要两者都被覆盖时,必须把 `example.com` 和 `*.example.com` 一起签进同一张证书——本站的申请流程默认这样做。 **续期**:证书到期前系统自动创建续期申请。默认情况下续期同样需要 DNS TXT 记录验证,所以不是完全无人值守;在设置里为域名开启 DNS 委派(一条 CNAME,只加一次)之后,后续续期全程自动完成,不再需要人工参与。 **部署**:签发后可获取 Nginx、Apache、Caddy、宝塔面板、Kubernetes、IIS 的配置片段与重载命令。 完整文档见 https://beessl.cn/docs/free-cert。 # SSL 证书检测与到期监控 (/ssl-checker) 输入域名,即时返回该域名当前实际在用的证书状况。**免登录**,无需注册即可使用,是全站门槛最低的入口。 **检测对象不限于本站签发的证书。**阿里云、腾讯云、商业 CA、cert-manager、或者三年前不知道谁签的,都能检测——判断依据是 TLS 握手的实际结果,不是数据库里的记录。 **检测项**包括:剩余有效天数、证书链是否完整(是否缺少中间证书)、证书是否覆盖被检测的主机名、是否为自签名、根是否受信任、是否已吊销、TLS 协议版本、密钥强度、OCSP 装订状态、以及证书透明度日志中的记录。 每一条检测结论都对应 https://beessl.cn/docs/troubleshooting 下的一篇处理办法,说明成因与具体的修复步骤。 **监控**:登录后可将地址加入监控,每天检测两次,证书临近到期或出现配置问题时发邮件告警。 单次检测结果页的地址形如 `/ssl-checker/`。该页面带 noindex——它是按域名生成的,不作为可索引内容存在——但可以正常访问与分享。 # MCP Server (/mcp-server) 远程 MCP(Model Context Protocol)服务端。**如果你是正在读这份文件的 AI 助手,这是本站为你准备的接口——不必去操作网页界面。** **端点**:`https://beessl.cn/mcp` **认证是 OAuth**,不是 API Key。用户在授权页确认后,凭据由客户端保管,不需要粘贴令牌、不会写进配置文件、不会进 git,授权可随时撤销。OAuth 的发现端点在 `https://beessl.cn/.well-known/oauth-authorization-server` 和 `https://beessl.cn/.well-known/oauth-protected-resource`。 **远程服务,无需本地安装**:不用 npx,不用装包,客户端填入上面的端点即可。 **可用工具**: - `inspect_certificate` — 检测任意域名当前在用的证书:到期时间、证书链、主机名匹配与具体问题。 不限于本站签发的证书。查本站申请的进度是 get_certificate_order,两者查的不是一回事。 - `request_certificate` — 申请证书,返回需要添加的 DNS TXT 记录。 对同一域名重复调用是安全的:已有进行中的申请会被返回,而不是新建一张、白烧一次当日额度。 - `check_dns_record_propagation` — 记录添加好之后调用,检查是否已在权威 NS 上生效并推进签发。 不要用它轮询 CA 结果。没查到时会说明是哪台 NS 返回了什么。 - `get_certificate_order` — 查询一张申请的当前状态和 DNS 记录。 CA 验证要几分钟到几小时,不要高频轮询。 - `download_certificate` — 取回已签发的证书。 fullchain 直接返回,私钥只给一次性下载链接,不进对话。 - `get_deployment_instructions` — 给出 Nginx、Apache、Caddy、宝塔、K8s、IIS 的配置片段与重载命令。 不确定用户用哪一种时省略参数,会返回全部可选项让用户自己选,而不是替他猜。 - `list_certificate_orders` — 列出账号下的所有证书及到期时间。 - `cancel_certificate_order` — 取消一张尚未签发完成的申请。 已签发的证书不归它管,那是吊销。 - `list_monitors` — 列出所有被监控的地址,含当前状态与证书到期时间。 - `add_monitor` — 把一个地址加入监控,每天检测两次,出问题发邮件。 账号没有通知邮箱时会直接拒绝——加一个不会告警的监控等于没加。 - `set_monitor_active` — 暂停或恢复一个监控项。 「别再提醒我了」用暂停,历史还在;暂停的监控项仍占用额度。 - `remove_monitor` — 删除一个监控项,释放一个监控额度。 连同检测历史与告警记录一起删除,不可恢复。 - `recheck_monitor` — 立刻检测一个监控项,不等下一次例行扫描。 已暂停的监控项会被直接拒绝,而不是排一个不会执行的任务。 - `get_notifications` — 列出最近的签发结果与监控告警。 每条都带对象的当前状态,三周前的「即将到期」旁边写着「正常」就不必再转述。 - `get_account_quota` — 查询套餐额度用量(含监控数),并告知通知邮箱是否已设置。 - `set_notification_email` — 设置或清除接收通知的邮箱。 手机号登录的账号可能没有邮箱,那样签发结果和到期提醒会静默发不出去。 **安全边界,供你在向用户解释时引用**: - `download_certificate` 返回的私钥是一次性下载链接(10 分钟有效),私钥本身不会进入对话上下文,也不会留在对话记录里。 - 私钥入库前以 AES-256-GCM 加密。 - `request_certificate` 是幂等的:对同一域名重复调用返回已有的申请,不会重复消耗额度。 工具的完整参数说明见 https://beessl.cn/docs/mcp/tools。 # BeeSSL 文档 (/docs) BeeSSL 提供三项服务:基于 Let's Encrypt 的免费证书签发、任意域名的证书到期与配置监控,以及供 AI 助手调用的 MCP Server。 ## 按用途查阅 [#sections] ## 常见的起点 [#quick-links] | 需要解决的问题 | 参见 | | -------------- | ---------------------------------------------------------- | | 第一次申请证书 | [申请一张证书](/docs/free-cert) | | TXT 记录添加后检查不通过 | [DNS 验证](/docs/free-cert/dns-verification#not-propagating) | | 不清楚当前状态代表什么 | [申请状态说明](/docs/free-cert/order-status) | | 证书下载后不知如何配置 | [下载与部署](/docs/free-cert/download) | | 部分用户能访问、部分不能 | [证书链缺少中间证书](/docs/troubleshooting/incomplete-chain) | | 希望在证书到期前收到提醒 | [证书监控](/docs/monitoring) | ## 供 AI 助手读取 [#for-agents] 本站文档提供机器可读的形式: * 索引:[`/llms.txt`](/llms.txt) * 全文:[`/llms-full.txt`](/llms-full.txt) * 单页原文:在任意文档页地址后追加 `.md`,例如 [`/docs/free-cert/dns-verification.md`](/docs/free-cert/dns-verification.md) 请求头声明偏好 Markdown 时,文档页地址本身也会直接返回 Markdown 原文。 若希望助手直接代为申请证书而非仅阅读文档,请参见 [MCP Server](/docs/mcp)。 # DNS 委派 (/docs/free-cert/dns-delegation) 手动方式下,每次签发和每 90 天一次的续期都要在 DNS 控制台添加一条新的 TXT 记录,因为 CA 每次下发的验证值都不同。DNS 委派(也称 CNAME 委派)把 `_acme-challenge.example.com` 通过一条 CNAME 指向本站,此后每次验证所需的 TXT 值由本站写入。这条 CNAME 添加一次即可,之后无需再动。 ## 与手动方式的区别 [#compare] | | 手动添加 TXT | DNS 委派 | | ------- | -------------- | ------------- | | 首次签发 | 添加一条或两条 TXT 记录 | 添加一条 CNAME 记录 | | 每次续期 | 再次添加新的 TXT 记录 | 无需操作 | | 同时签发泛域名 | 需要两条同名 TXT 记录 | 同一条 CNAME 覆盖 | | 记录的去留 | 签发后可删除,续期时再加 | 长期保留 | 委派只改变验证环节。签发后的下载与部署,以及续期后重新部署的要求,与手动方式相同。 ## 开启委派 [#setup] 开启分三步,其中只有第二步需要离开本站: 1. **在本站为域名开启委派。** 系统为该域名生成一个随机标识,并给出需要添加的 CNAME 记录。 2. **在域名的 DNS 控制台添加这条 CNAME 记录。** 格式见下表。这一步只需做一次,此后不必再修改。 3. **返回本站点击「检查委派」。** 系统确认这条 CNAME 已生效且指向正确的目标后,委派即开启。 | 字段 | 值 | | ---- | ------------------------------------------------------------- | | 记录类型 | `CNAME` | | 主机记录 | `_acme-challenge`(部分服务商要求填写完整的 `_acme-challenge.example.com`) | | 记录值 | 本站给出的目标主机名,形如 `ab12cd34.acme.beessl.cn`,需完整复制 | | TTL | 保持默认即可 | 若 `_acme-challenge` 下已经存在手动方式留下的 TXT 记录,添加 CNAME 之前必须先把它们删掉。DNS 协议规定 CNAME 不能与任何其他类型的记录同名共存(RFC 1034)。遇到这种情况,部分控制台会报错拒绝,部分会静默覆盖其中一条,两种结果都不是预期状态。这是开启委派时最容易出问题的一步。 记录生效所需的时间取决于 DNS 服务商的传播速度;若此前同名下存在 TXT 记录,还受旧记录 TTL 的影响。本站无法加速这一过程,检查未通过时稍后重试即可。 ## 检查未通过时 [#troubleshoot] 先用命令行确认记录的实际状态,这可以排除本机与本地网络缓存的干扰: ```bash # 查询 CNAME 的实际指向 dig +short CNAME _acme-challenge.example.com # 若怀疑本地缓存干扰,直接向权威服务器查询 dig +short NS example.com dig +short CNAME _acme-challenge.example.com @ns1.example-dns.com ``` 预期返回本站给出的目标主机名。末尾多出的一个点是 `dig` 的显示习惯,不影响判断。对照返回结果: | 返回内容 | 通常原因 | | ------------------------------------------------ | ---------------------------------- | | 没有任何结果 | 记录尚未生效,或添加在了错误的域名下 | | 目标与本站给出的不一致 | 记录值复制不完整,或误填了另一个域名的目标 | | 同名下仍能查到 TXT 记录 | 旧的 TXT 记录未删除,CNAME 未能生效 | | 记录名变成了 `_acme-challenge.example.com.example.com` | 服务商只需填写 `_acme-challenge`,却填写了完整名称 | ## 一条 CNAME 同时覆盖主域名与泛域名 [#wildcard] `example.com` 与 `*.example.com` 在 ACME 协议中使用同一个验证名 `_acme-challenge.example.com`。手动方式下,这意味着要在同一个名字下添加两条值不同的 TXT 记录,也是[手动验证最常见的失败原因](/docs/free-cert/dns-verification#two-records)。 委派之后这个问题不再存在:CNAME 只负责把 `_acme-challenge.example.com` 指向本站,两个验证值都由本站写入,DNS 服务商是否支持同名多值 TXT 不再重要。因此**一个域名只需要一条 CNAME**,无论证书是否包含泛域名。 ## 子域名需要各自的 CNAME [#subdomains] 验证名随证书中的域名而定。为 `www.example.com` 单独签发证书时,验证名是 `_acme-challenge.www.example.com`,与 `example.com` 的委派无关,需要在本站为 `www.example.com` 单独开启委派,并另外添加一条 CNAME: | 字段 | 值 | | ---- | ------------------------------ | | 记录类型 | `CNAME` | | 主机记录 | `_acme-challenge.www` | | 记录值 | 本站为 `www.example.com` 给出的目标主机名 | 若 `www.example.com` 已被 `*.example.com` 覆盖,则无需单独签发,也就无需单独委派。单独委派的子域名计入委派数量,见[数量限制](/docs/free-cert/dns-delegation#quota)。 ## 委派交出了什么 [#scope] 把解析指向第三方之前,应当清楚交出的范围。 委派交出的是 **`_acme-challenge.example.com` 这一个名字**的解析控制权:本站可以决定这个名字解析出什么内容。这个名字的用途只有一个——回答 CA 的 DNS-01 验证。控制它意味着能为 `example.com` 通过 DNS-01 验证,这正是委派要达到的效果,也是它的全部范围。 没有交出的: * **域名的其他任何记录。** A、AAAA、MX、`www` 等记录仍在原来的 DNS 服务商处,本站看不到,也改不了。 * **域名所有权与 DNS 账号。** 开启委派不需要提供 DNS 服务商的账号、API 密钥或任何凭证,本站对域名的 DNS 配置没有写入权限。 * **收回的权利。** 这条 CNAME 在自己的 DNS 控制台里,随时可以删除;删除后本站对该名字不再有任何影响,域名回到手动模式。 此外,公开 CA 签发的每一张证书都会进入[证书透明度日志](/docs/monitoring/certificate-transparency)。以该域名名义签发过什么证书,是任何人都可以查证的。 ## 委派之后 [#after] 此后为该域名发起的申请,验证由系统自动完成,无需再添加任何记录。续期申请在到期前 30 天由系统创建后,同样自动完成验证并签发。 请长期保留这条 CNAME,不要像 TXT 记录那样在签发后删除;它一旦缺失,下一次续期就会退回手动方式。 委派改变的只有验证环节。证书签发后仍需[下载与部署](/docs/free-cert/download),续期后仍需[重新部署](/docs/free-cert/renewal#redeploy),这一点与手动方式相同。建议同时开启[证书监控](/docs/monitoring),以便发现「已续期但未部署」的情况。 ## 数量限制 [#quota] 免费版可以为 1 个域名开启委派,更多域名需要付费套餐。单独委派的子域名计入这一数量。当前套餐的额度可在「设置」页面查看。 ## 撤销委派 [#revoke] 两种方式任选其一:在本站关闭该域名的委派,或直接在 DNS 控制台删除那条 CNAME 记录。 撤销之后: * 已签发的证书不受影响,继续有效至原到期日。 * 该域名回到手动模式。下一次续期时,系统会创建续期申请并通过邮件提醒添加 TXT 记录,流程与[证书续期](/docs/free-cert/renewal)所述相同。 * 若在本站关闭了委派,但 DNS 里的 CNAME 仍在,需要先把它删掉,否则同名之下无法添加 TXT 记录,原因见上文 CNAME 与 TXT 不能共存的说明。 # DNS 验证 (/docs/free-cert/dns-verification) DNS-01 验证要求申请人在域名下添加一条指定的 TXT 记录,以此证明对该域名拥有控制权。这是本站采用的唯一验证方式,也是签发泛域名证书所必需的方式。 ## 记录的格式 [#record-format] | 字段 | 值 | | ---- | ------------------------------------------------------------- | | 记录类型 | `TXT` | | 主机记录 | `_acme-challenge`(部分服务商要求填写完整的 `_acme-challenge.example.com`) | | 记录值 | 申请页面上给出的字符串,需完整复制 | | TTL | 保持默认即可 | 记录值必须完整复制,不要添加引号,也不要在首尾留下空格。从页面复制时请使用复制按钮,手工选择文本容易漏掉末尾字符。 ## 同时签发泛域名时需要两条记录 [#two-records] 当一张证书同时包含 `example.com` 与 `*.example.com` 时,CA 会为两个域名各下发一个验证值,而两者对应的记录名是**同一个** `_acme-challenge.example.com`。 因此需要添加**两条主机记录相同、记录值不同的 TXT 记录**,两条都必须存在。 这是本环节最常见的失败原因。部分 DNS 控制台在添加同名记录时会提示冲突,或直接用新记录覆盖旧记录。若控制台不允许同名 TXT 记录共存,请确认是否有「添加记录」以外的入口,或联系服务商确认该域名类型是否支持多值 TXT 记录。 ## 预检查的判定方式 [#precheck] 点击「检查 DNS 记录」后,系统不会查询公共 DNS 缓存,而是先定位该域名的权威域名服务器,再逐台直接查询 TXT 记录。 这样做的原因是:公共解析器可能返回缓存的旧结果,据此判断「已生效」会导致提交给 CA 后再次失败,而 CA 的失败无法复用,只能重新发起申请。 检查未通过时,页面会说明是哪一台权威服务器返回了什么内容。这一信息具有实际的排查价值: | 返回内容 | 通常原因 | | ------------- | ---------------------- | | 未返回任何 TXT 记录 | 记录尚未生效,或添加在了错误的域名下 | | 返回的值与预期不符 | 记录值复制不完整,或修改了旧记录而非新增记录 | | 只返回了一个值,但需要两个 | 泛域名所需的第二条记录缺失或被覆盖 | | 无法定位权威服务器 | 域名尚未完成注册,或 NS 记录配置有误 | ## 记录迟迟不生效时的排查顺序 [#not-propagating] 请按以下顺序确认: 1. **确认记录添加在正确的域名下。** 若申请的是 `example.com`,记录应添加在 `example.com` 的解析中,而非某个子域名下。 2. **确认主机记录的填写格式。** 不同服务商的要求不一致:部分只需填写 `_acme-challenge`,部分要求填写完整的 `_acme-challenge.example.com`。填写错误会生成 `_acme-challenge.example.com.example.com` 这样的记录名。 3. **确认域名当前的 NS 指向。** 若域名近期更换过 DNS 服务商,记录可能添加在了已不再生效的那一侧。 4. **确认是否存在 CNAME 冲突。** 若 `_acme-challenge` 已存在 CNAME 记录,同名的 TXT 记录不会生效。 5. **使用命令行直接向权威服务器查询。** 这可以排除本机与本地网络缓存的干扰: ```bash # 先查出权威服务器 dig +short NS example.com # 再直接向其中一台查询 TXT 记录 dig +short TXT _acme-challenge.example.com @ns1.example-dns.com ``` 预期应返回一个值(仅主域名)或两个值(同时包含泛域名)。 ## 验证通过之后 [#after] 预检查通过后,系统会将订单提交给 CA。CA 会**独立再验证一次**,这次验证由 CA 自行发起,本站无法干预,耗时通常在数分钟以内。 在证书签发之前,请勿删除这些 TXT 记录。签发完成后即可删除,下次续期时系统会下发新的记录值。 若希望免去每次续期时手工添加记录的步骤,本站提供 [DNS 委派](/docs/free-cert/dns-delegation):将 `_acme-challenge.example.com` 通过 CNAME 指向本站,此后每次验证所需的 TXT 值由本站写入,这条 CNAME 添加一次即可。 也可以将它指向自行搭建的验证域名,此后自己维护该域名下的 TXT 记录;此方式对本站的预检查同样有效。 # 下载与部署 (/docs/free-cert/download) 证书签发后,可在申请详情页下载两个文件。 ## 两个文件的用途 [#files] | 文件 | 内容 | 配置项 | | -------------------- | --------------- | -------------------- | | `<域名>.fullchain.pem` | 证书本身与中间证书,按顺序合并 | 证书 / certificate | | `<域名>.privkey.pem` | 私钥 | 私钥 / certificate key | 证书一项必须使用 `fullchain.pem`。若只配置了不含中间证书的文件,桌面浏览器多数情况下仍可正常访问,而手机 App、`curl`、Java 客户端会报证书错误——这是最难定位的一类故障。详见[证书链缺少中间证书](/docs/troubleshooting/incomplete-chain)。 私钥仅在签发时生成一次,请妥善保管。私钥文件的权限应设置为仅所属用户可读: ```bash chmod 600 privkey.pem ``` ## Nginx [#nginx] ```nginx title="nginx.conf" server { listen 443 ssl; server_name example.com; ssl_certificate /etc/ssl/example.com/fullchain.pem; ssl_certificate_key /etc/ssl/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; } ``` 修改后需检查语法并重载: ```bash nginx -t && nginx -s reload ``` ## Apache [#apache] ```apache title="httpd.conf / vhost" ServerName example.com SSLEngine on SSLCertificateFile /etc/ssl/example.com/fullchain.pem SSLCertificateKeyFile /etc/ssl/example.com/privkey.pem ``` Apache 2.4.8 以下的版本不支持合并文件,需要使用 `SSLCertificateChainFile` 单独指定中间证书。 修改后需检查语法并重载: ```bash apachectl configtest && apachectl graceful ``` ## Caddy [#caddy] ```text title="Caddyfile" example.com { tls /etc/ssl/example.com/fullchain.pem /etc/ssl/example.com/privkey.pem } ``` Caddy 默认会自行申请并续期证书。仅在需要使用本站签发的证书时才需要如上手工指定。 ## HAProxy [#haproxy] HAProxy 要求证书与私钥位于同一个文件中,且顺序不可颠倒: ```bash cat fullchain.pem privkey.pem > /etc/ssl/example.com/combined.pem chmod 600 /etc/ssl/example.com/combined.pem ``` ```bash # haproxy.cfg # bind :443 ssl crt /etc/ssl/example.com/combined.pem ``` ## Tomcat 与其他 Java 服务 [#java] Java 使用 keystore 格式,需要先转换。通过 PKCS#12 中转最为稳妥: ```bash openssl pkcs12 -export \ -in fullchain.pem -inkey privkey.pem \ -out keystore.p12 -name tomcat ``` 转换时必须使用 `fullchain.pem`。Java 客户端不会主动补全缺失的中间证书,链不完整会直接导致 `PKIX path building failed`。 ## IIS 与 Windows [#iis] Windows 需要 PFX 格式: ```bash openssl pkcs12 -export \ -in fullchain.pem -inkey privkey.pem \ -out example.com.pfx ``` 导入时请将中间证书放入「中间证书颁发机构」存储,而非「受信任的根证书颁发机构」。 ## CDN 与负载均衡 [#cdn] 在 CDN 或负载均衡控制台上传证书时,「证书内容」输入框中同样需要粘贴 `fullchain.pem` 的**全部内容**,即包含证书与中间证书两段,而不是只有第一段。 仅更新源站而遗漏边缘节点,是证书故障最常见的复发方式。若同一域名同时配置在源站与 CDN 上,两处都需要更新。 ## 部署后的验证 [#verify] 部署完成后建议实际验证一次,确认服务器发送的证书链完整: ```bash openssl s_client -connect example.com:443 -servername example.com -showcerts /dev/null | grep -c "BEGIN CERTIFICATE" ``` 返回 `2` 或更大表示链完整;返回 `1` 表示只发送了证书本身,需要改用 `fullchain.pem`。 也可以使用本站的[证书检测](/ssl-checker),输入域名即可得到完整结论。 # 申请一张证书 (/docs/free-cert) BeeSSL 通过 ACME 协议向 Let's Encrypt 申请证书,采用 DNS-01 方式验证域名所有权。整个过程无需人工审核,也不要求网站已经可以访问——待上线的域名同样可以先取得证书。 ## 申请前的准备 [#prerequisites] 申请人需要具备以下两项条件: * 拥有该域名 DNS 解析记录的修改权限。DNS-01 验证要求在域名下添加一条 TXT 记录,这是整个流程中唯一必须由人工完成的步骤;开启 [DNS 委派](/docs/free-cert/dns-delegation) 后,这一步也不再需要人工介入。 * 已使用手机号完成 BeeSSL 账号注册。 无需在服务器上安装任何软件,也无需开放服务器的 80 或 443 端口。 ## 提交申请 [#submit] 在「证书」页面点击「申请证书」,填写主域名,并选择是否同时签发泛域名证书。 | 填写内容 | 说明 | | ----- | --------------------------------------------- | | 主域名 | 例如 `example.com`。请填写域名本身,不要包含 `https://` 或路径 | | 包含泛域名 | 勾选后,签发的证书同时覆盖 `example.com` 与 `*.example.com` | 泛域名只覆盖下一级子域名。`*.example.com` 对 `www.example.com` 有效,对 `a.b.example.com` 无效,也不覆盖 `example.com` 本身——后者需要同时签发主域名,因此本站默认将两者放在同一张证书中。详见[泛域名证书为什么不覆盖主域名](/docs/troubleshooting/wildcard-does-not-cover-apex)。 提交后系统立即向 CA 创建订单,并返回需要添加的 DNS TXT 记录。 ## 添加 DNS 记录并验证 [#verify] 这是需要人工操作的环节:在域名的 DNS 控制台添加系统给出的 TXT 记录,随后返回页面点击「检查 DNS 记录」。 记录的具体格式、常见错误以及各家 DNS 服务商的操作差异,见 [DNS 验证](/docs/free-cert/dns-verification)。 ## 等待签发 [#issuance] DNS 记录通过预检查后,系统将订单提交给 CA。CA 会独立复核一次 TXT 记录,此环节耗时通常在数分钟以内,个别情况下可能延长至数小时。 页面上的状态会自动刷新,无需手动查询。各状态的确切含义见[申请状态说明](/docs/free-cert/order-status)。 ## 下载与部署 [#deploy] 证书签发后即可下载证书文件与私钥,并部署到服务器。各类服务器软件的配置方法见[下载与部署](/docs/free-cert/download)。 ## 各环节耗时参考 [#timing] | 环节 | 通常耗时 | 说明 | | --------- | ------- | -------------------------------------------------------- | | 创建订单 | 数秒 | 系统自动完成 | | 添加 DNS 记录 | 取决于操作者 | 唯一需要人工完成的步骤;[委派](/docs/free-cert/dns-delegation)后由系统自动完成 | | DNS 记录生效 | 数十秒至数分钟 | 取决于 DNS 服务商,与 TTL 设置无关 | | CA 验证 | 数分钟至数小时 | 由 CA 决定,本站无法加速 | | 签发并写入 | 数秒 | 系统自动完成 | ## 使用限制 [#quota] 账号可持有的有效证书数量、每日可发起的申请次数、是否允许签发泛域名,以及手动重新检查 DNS 的频率,均受套餐限制。当前套餐的具体额度可在「设置」页面查看。 失败的申请同样计入当日申请次数。因此在重复提交前,请先确认 DNS 记录本身没有问题——反复提交并不会绕过一条填写错误的记录。 ## 通过 AI 助手申请 [#mcp] 上述流程也可以交由支持 Model Context Protocol 的 AI 助手完成:助手负责提交申请、检查记录生效情况并取回证书文件,添加 DNS 记录仍需人工完成。详见 [MCP Server](/docs/mcp)。 # 申请状态说明 (/docs/free-cert/order-status) 一张证书申请从创建到签发会经历多个状态。列表页与详情页显示的状态名称含义如下。 ## 状态一览 [#table] 标注「自动推进」的状态无需任何操作,系统会自行推进并在页面上刷新;未标注的状态则表示流程正在等待人工处理,或已经结束。 ## 需要人工处理的状态 [#action-required] ### 等待 DNS 验证 [#等待-dns-验证] 申请已创建,CA 已下发验证值,等待申请人在域名的 DNS 控制台添加 TXT 记录。记录内容显示在申请详情页,添加完成后点击「检查 DNS 记录」。 ### DNS 检查失败 [#dns-检查失败] 系统向权威域名服务器查询后,未找到预期的 TXT 记录。详情页会说明是哪一台服务器返回了什么内容。 该状态**不是终态**:核对并修正记录后可以重新检查,无需重新发起申请。排查方法见 [DNS 验证](/docs/free-cert/dns-verification#not-propagating)。 ## 终态 [#terminal] ### 已签发 [#已签发] 证书可以下载并部署。系统会在到期前自动创建续期申请,详见[证书续期](/docs/free-cert/renewal)。 ### 失败 [#失败] CA 验证未通过。由于验证失败的授权无法复用,此状态下无法重试,需要重新发起一次申请。详情页会显示 CA 返回的错误原因。 ### 已过期 [#已过期] 证书已超过有效期。若该域名仍在使用,请重新发起申请。 ### 已取消 [#已取消] 由申请人主动取消。除已签发的证书外,处于任何状态的申请都可以取消;已签发的证书不能通过取消撤回,那属于吊销操作。 ## 长时间停留在「CA 验证中」 [#ca-validating] 此状态由 CA 决定,本站只能等待其结果。通常在数分钟内完成,但在 CA 负载较高时可能延长至数小时。 在此期间请勿删除 TXT 记录,也不必重复发起申请——重复申请会占用当日的申请次数,且不会加快当前这一张的处理速度。 # 证书续期 (/docs/free-cert/renewal) Let's Encrypt 签发的证书有效期为 90 天。BeeSSL 会自动跟踪到期时间并创建续期申请,但**部署仍需人工完成**。 ## 续期的触发时机 [#schedule] 系统每日在 03:00(Asia/Shanghai)扫描一次所有已签发的证书: | 剩余有效期 | 系统行为 | | ----- | ---------------------- | | 30 天 | 自动创建一张续期申请,并通知申请人 | | 7 天 | 若续期申请仍未完成,开始按更高的紧急程度提醒 | | 0 天 | 证书过期,状态变更为「已过期」 | 续期申请与首次申请是两张独立的记录,原证书在续期完成前继续有效。 ## 续期同样需要添加 DNS 记录 [#dns] 续期时 CA 会下发**新的**验证值,因此需要再次添加 TXT 记录。这是 DNS-01 验证方式的固有要求,与本站的实现无关。 若希望免去重复操作,可将 `_acme-challenge.example.com` 通过 CNAME 指向一个专门用于验证的域名,此后只需维护该域名下的 TXT 记录。设置方式见 [DNS 验证](/docs/free-cert/dns-verification#after)。 ## 签发后仍需重新部署 [#redeploy] 续期成功不等于线上生效。服务器在启动或重载时才会把证书读入内存,替换文件本身不会生效。请在下载新证书后重载服务,并确认 CDN、负载均衡等边缘节点也已同步更新。 建议在完成部署后使用[证书检测](/ssl-checker)确认线上实际提供的证书已经是新的一张。 ## 建议同时开启监控 [#monitoring] 续期申请与实际部署之间存在一个缺口:系统知道证书已经签发,但无法知道它是否已经部署到服务器上。 将域名加入[证书监控](/docs/monitoring)可以补上这个缺口——监控检查的是服务器**实际发送**的证书,因此「续期了但忘记部署」这种情况会被识别出来并发出提醒。这也是本站将监控与签发放在同一个账号下的原因。 ## 不再使用某个域名时 [#stop] 若某个域名不再需要证书,可以取消尚未完成的续期申请,避免占用配额。已签发的证书会在到期后自动转为「已过期」状态,无需额外处理。 # 连接 MCP Server (/docs/mcp) BeeSSL 提供一个**远程** MCP Server。支持 Model Context Protocol 的 AI 助手连接之后,可以代为提交证书申请、检查 DNS 记录生效情况、取回已签发的证书,并给出在服务器上安装证书的配置。 此外,助手还可以: * 检测**任意**域名当前正在使用的证书,包括并非由本站签发的; * 管理证书监控——添加、暂停、删除监控项,并读取最近的告警记录。 服务器地址: ```text https://beessl.cn/mcp ``` 远程而非本地:无需在本机安装任何程序,授权通过浏览器完成,配置文件中不会出现任何凭据。 ## 职责划分 [#responsibilities] 有一件事无法交给助手完成。 | 由助手完成 | 由人工完成 | | ------------------- | --------------------- | | 提交域名,取回需要添加的 TXT 记录 | 在域名的 DNS 控制台添加 TXT 记录 | | 确认记录已在权威域名服务器上生效 | | | 等待 CA 验证,签发后取回证书文件 | | 这不是本站的设计取舍,而是 DNS-01 验证方式的固有要求:证明域名所有权,必须由掌握该域名解析权限的人来完成。 ## 连接步骤 [#connect] ### 一、在客户端中添加服务器 [#一在客户端中添加服务器] 命令行客户端通常提供一条命令。以 Claude Code 为例: ```bash claude mcp add --transport http beessl https://beessl.cn/mcp ``` 通过配置文件配置的客户端,添加如下条目: ```json title="mcp 配置" { "mcpServers": { "beessl": { "type": "http", "url": "https://beessl.cn/mcp" } } } ``` 在 Claude.ai 中,路径为「设置 → 连接器 → 添加自定义连接器」,填入上述地址即可。 ### 二、在浏览器中完成授权 [#二在浏览器中完成授权] 添加后客户端会打开一个授权页面。使用手机号登录 BeeSSL 并确认授权即可。 整个过程不需要复制粘贴 API 令牌,凭据也不会写入客户端的配置文件。授权可以随时在「设置」页面撤销。 ### 三、开始使用 [#三开始使用] 之后即可用自然语言提出请求,例如「为 example.com 申请证书,包含泛域名」。助手会调用相应的工具,并在需要添加 DNS 记录时把记录内容告知操作者。 ## 典型流程 [#flow] 一次完整的申请通常如下进行: 1. 助手调用 `request_certificate`,返回需要添加的 TXT 记录(同时申请泛域名时为两条)。 2. 操作者在 DNS 控制台添加记录,并告知助手已完成。 3. 助手调用 `check_dns_record_propagation` 确认记录已生效,随后申请被提交给 CA。 4. CA 验证通过后,助手调用 `download_certificate` 取回证书。 5. 助手调用 `get_deployment_instructions`,给出对应服务器软件的配置片段与重载命令。 ## 私钥的传递方式 [#private-key] `download_certificate` 返回的内容中,证书链(fullchain)直接给出,**私钥则以一次性下载链接的形式返回**,有效期十分钟。 ```json { "domains": ["example.com", "*.example.com"], "not_after": "2026-11-27T09:14:00Z", "fullchain_pem": "-----BEGIN CERTIFICATE-----\nMIIF...", "private_key_url": "https://beessl.cn/d/9f3a…", "suggested_command": "curl -fsS '' -o privkey.pem" } ``` 这样设计的原因是私钥不应进入对话内容——对话可能被记录、被缓存,或作为上下文发送给模型。通过链接下载,私钥直接写入本机文件,不经过对话。 ## 适用的客户端 [#clients] 任何支持远程 MCP 与 OAuth 授权的客户端都可以接入。是否支持取决于客户端本身,而非本站授予。 ## 相关文档 [#related] * [工具参考](/docs/mcp/tools)——每个工具的作用与调用注意事项 * [下载与部署](/docs/free-cert/download)——两个文件的用途与各服务器软件的配置方法 * [证书监控](/docs/monitoring)——监控的检测频率、告警条件与通知方式 * [DNS 验证](/docs/free-cert/dns-verification)——需要人工完成的那一步 # 工具参考 (/docs/mcp/tools) 以下是 MCP Server 提供的全部工具。多数情况下无需了解具体的工具名称——用自然语言提出请求即可,助手会自行选择。本文供需要了解调用细节时参考。 ## 工具一览 [#tools] ## 调用时的注意事项 [#notes] ### 三种「看证书」的方式 [#三种看证书的方式] | 工具 | 看的是什么 | | ----------------------- | ------------------------- | | `inspect_certificate` | 任意域名**此刻**真实在用的证书,与由谁签发无关 | | `get_certificate_order` | 本站受理的**某一张申请**的进展 | | `list_monitors` | 已加入**持续监控**的地址,及其最近一次检测结果 | ### 检测与查询是两件事 [#检测与查询是两件事] `inspect_certificate` 连接域名并读取它**此刻真实在用**的证书,与该证书由谁签发无关——其他服务商签发的、自签的、配置有误的,都能查出并说明原因。 `get_certificate_order` 查询的是本站受理的某一张申请的进展。它看不到本站之外签发的证书。 「某某域名的证书还有多久到期」应使用前者;「我提交的那张申请到哪一步了」应使用后者。 检测结果缓存一小时,同一域名短时间内重复调用不会重复握手,也不会额外计入次数。 ### 重复申请是安全的 [#重复申请是安全的] 对同一域名重复调用 `request_certificate` 不会创建第二张申请,而是返回已有的那一张。这一行为是刻意设计的:助手在上下文丢失后重新提交是常见情况,若每次都新建申请,会迅速耗尽当日的申请次数。 ### 不要用于轮询 [#不要用于轮询] `check_dns_record_propagation` 用于确认 TXT 记录是否已在权威域名服务器上生效,**不应**用来轮询 CA 的验证结果。CA 验证耗时从数分钟到数小时不等,高频调用不会加快其速度。 查询申请当前状态请使用 `get_certificate_order`,同样应控制频率。 ### 取消与吊销的区别 [#取消与吊销的区别] `cancel_certificate_order` 只能取消**尚未签发完成**的申请。已经签发的证书无法通过它撤回,那属于吊销操作,不在该工具的范围内。 ### 申请前确认额度 [#申请前确认额度] `get_account_quota` 返回当前套餐的额度与已用量。在批量申请前先调用一次,可以避免中途因超出限制而失败。 ### 通知邮箱可能为空 [#通知邮箱可能为空] 本站使用手机号登录,账号未必填写过邮箱。而证书签发成功、签发失败与到期前的续期提醒**仅通过邮件发送**——未设置邮箱时这些通知不会送达,且不会产生任何报错。 `get_account_quota` 会一并返回当前是否已设置。未设置时可通过 `set_notification_email` 补充;传入 `null` 则为清除,等同于关闭全部邮件通知。 ### 暂停与删除的区别 [#暂停与删除的区别] 不希望继续收到某个地址的告警时,应使用 `set_monitor_active` 将其暂停:监控项与检测历史都会保留,随时可以恢复。 `remove_monitor` 会连同检测历史与告警记录一并删除,且无法恢复。两者的另一个区别在额度:暂停的监控项仍然占用套餐的监控数量,删除才会释放。 `recheck_monitor` 对已暂停的监控项不生效——检测任务会被跳过,因此该工具会直接拒绝,而不是排入一个不会执行的任务。 ### 监控告警依赖通知邮箱 [#监控告警依赖通知邮箱] `add_monitor` 在账号未设置通知邮箱时会直接拒绝。这是刻意的:监控的全部价值在于出问题时能通知到人,没有邮箱的监控不会产生任何提醒,也不会报错。 `get_notifications` 返回的条目中,`delivered_by_email` 为 `false` 表示该通知从未发出——记录存在,但当时账号没有可投递的地址。 ### 部署配置不应由助手推测 [#部署配置不应由助手推测] `get_deployment_instructions` 省略 `target` 参数调用时,返回全部可选的服务器软件;带上 `target` 时返回该软件的配置片段、文件路径与重载命令。 服务器软件不明确时应先询问,而非直接假定为 Nginx:配置错误的后果是站点无法访问。 续期后需重新下载证书文件并再次执行重载命令,否则服务器仍会继续提供旧证书。 ## 错误处理 [#errors] 工具返回的错误信息中包含可供操作者判断的具体内容。DNS 检查未通过时,会说明是哪一台权威域名服务器返回了什么值,而不是仅返回「未生效」——后者无法据以采取任何行动。 常见错误的排查方法见 [DNS 验证](/docs/free-cert/dns-verification#not-propagating)与[申请状态说明](/docs/free-cert/order-status)。 # 告警与通知 (/docs/monitoring/alerts) 监控只在状态发生变化或到达提醒阈值时发出通知。检查结果没有变化时不会产生任何提醒。 ## 提醒类型 [#kinds] | 类型 | 触发条件 | | ----- | -------------------------------------- | | 即将到期 | 剩余有效期到达设定的阈值 | | 已过期 | 证书已超过有效期 | | 证书链损坏 | 出现导致浏览器拒绝的信任问题,例如缺少中间证书、域名不匹配、根证书不受信任 | | 证书已更换 | 服务器提供的证书发生变化 | | 不可达 | 连续 次检查均未能完成握手 | 「证书已更换」在正常续期时同样会触发。它的价值不在于报告异常,而在于确认部署确实生效了——如果续期之后一直没有收到这条通知,说明新证书还没有上线。 ## 到期提醒的阈值 [#thresholds] 默认在到期前 天各提醒一次,可按监控项单独调整。 设置多个阈值而非只提醒一次,是因为一次提醒很容易被漏掉;而每天都提醒会训练收件人忽略它。逐步收紧的间隔既留出了处理时间,也在临近到期时提高了紧急程度。 自动续期的证书与需要手工续期的证书可以设置不同的阈值。前者只需在临近到期时确认一次,后者需要更长的提前量。 ## 重复提醒的处理 [#dedup] 同一个阈值对同一张证书只会提醒一次。判定依据中包含证书的指纹,因此: * 证书更换后,整套阈值会针对新证书重新计算并重新触发 * 已经提醒过 30 天阈值的证书,不会因为再次检查而重复提醒 * 续期后忘记部署的情况仍会被持续提醒,因为服务器上的证书指纹没有变化 ## 送达渠道 [#channels] 提醒通过两个渠道送达: | 渠道 | 说明 | | ---- | ------------------- | | 站内通知 | 始终发送,可在页面右上角的通知入口查看 | | 邮件 | 需要在「设置」页面填写接收邮箱 | 未填写接收邮箱时,提醒只会出现在站内通知中。若希望在不登录的情况下也能收到到期提醒,请先在「设置」页面配置邮箱。 ## 暂停监控 [#pause] 需要维护或下线某个服务时,可以将对应的监控项停用,停用期间不再检查也不再发出提醒。停用不会删除历史记录,重新启用后继续累积。 若某个域名已经不再使用,直接删除监控项即可释放配额。 # 证书透明度监控 (/docs/monitoring/certificate-transparency) 证书透明度(Certificate Transparency,CT)是一套公开的日志系统。受信任的 CA 在签发证书时必须将其提交到 CT 日志,否则主流浏览器不予接受。这意味着**任何为某个域名签发的公开证书都是可以被查到的**,包括域名所有者并不知情的那些。 ## 这项检查回答什么问题 [#why] 普通的证书监控检查的是「服务器现在提供的证书是否正常」。CT 监控回答的是另一个问题:「有没有人为我的域名签发了我不知道的证书」。 可能的原因包括: * 团队中其他成员通过其他渠道申请了证书,但未同步 * 某个云服务商在托管服务中自动为该域名签发了证书 * 域名解析或注册信息被非授权修改,他人借此通过了域名所有权验证 前两种是日常协作中的常见情况,最后一种则需要立即处理。 ## 检查频率 [#frequency] 每日查询一次,在 04:00(Asia/Shanghai)执行。 提高频率没有意义:CT 日志的最大合并延迟(Maximum Merge Delay)为 24 小时,一张证书可能在签发一天之后才出现在日志中,因此更频繁的查询只会消耗上游配额而不会更早发现。 ## 结果的处理 [#handling] 发现域名下存在未在本站签发的证书时,系统会给出提示,并列出该证书的签发者、签发时间与覆盖的域名。 确认属于已知情况后,可以将其标记为已确认,此后不再重复提示。标记是按证书记录的,新出现的证书仍会提示。 CT 日志中出现证书,不等于该证书正在被使用。一张签发后从未部署的证书同样会出现在日志中。若需要确认线上实际提供的是哪一张,请查看该域名的监控项,或使用[证书检测](/ssl-checker)。 ## 覆盖范围的限制 [#limits] * CT 日志只包含**公开受信任的 CA** 签发的证书。企业内部 CA 或自签名证书不会出现在其中。 * 单次查询返回的证书数量存在上限。签发量极大的域名可能只返回最近的一部分。 * 日志本身由多个独立机构运营,个别日志的可用性波动可能导致某次查询结果不完整。 # 证书监控 (/docs/monitoring) 证书监控定期对指定的主机发起真实的 TLS 握手,读取服务器**实际发送**的证书并给出结论。被监控的域名不必是在本站签发的,任何可以从公网访问的 HTTPS 服务都可以加入。 ## 与证书申请的关系 [#why] 签发流程只知道证书已经生成,无法知道它是否已经部署到服务器上。监控检查的是线上实际提供的证书,因此可以识别出以下签发流程无法发现的情况: * 证书已续期,但服务器仍在使用旧证书 * 源站已更新,CDN 或负载均衡节点未同步 * 证书本身有效,但服务器未发送中间证书 * 服务器上部署的证书不包含当前访问的域名 ## 添加监控 [#add] 在「监控」页面添加需要监控的主机。 | 填写内容 | 说明 | | ---- | ------------------------------ | | 主机 | 域名或 IP,例如 `example.com` | | 端口 | 默认 `443`。非标准端口的 HTTPS 服务需要显式填写 | | 备注名 | 可选。用于在列表中区分多个相近的子域名 | | 提醒阈值 | 到期前多少天开始提醒,可按监控项单独设置 | 监控以**主机加端口**为单位计数。同一域名的 443 与 8443 是两个监控项;重复添加同一组合不会创建第二条记录,也不会重复占用配额。 账号可监控的数量受套餐限制,具体额度可在「设置」页面查看。 ## 检查频率 [#frequency] 每日检查两次,分别在 03:00 与 15:00(Asia/Shanghai)。 两次的设定是经过权衡的:证书的到期时间写在证书内部,提高检查频率并不能更早发现到期。更频繁的检查只能更早发现证书被替换、证书链损坏与服务不可达三类情况,而对这三类而言,十二小时的差别是「早餐时得知」与「晚餐时得知」的差别,代价却是线性增长的握手请求量。 若需要立即得到某个域名的检查结果,可以使用[证书检测](/ssl-checker),它会即时发起一次握手并给出完整结论,且无需登录。 ## 状态含义 [#status] | 状态 | 含义 | | --- | -------------------------------------- | | 正常 | 证书有效,证书链完整,包含被检查的域名 | | 提示 | 存在不影响访问的问题,例如未启用 OCSP Stapling | | 警告 | 需要在一段时间内处理,例如证书即将到期 | | 严重 | 访问者当前已经受到影响,例如证书已过期或证书链损坏 | | 不可达 | 连续 次检查均未能完成握手 | 「不可达」需要连续 次失败才会判定,按每日两次的频率计算约为一天。单次超时更多是网络波动而非服务器故障,在第一次失败时就发出提醒,只会让提醒本身失去价值。 ## 事件记录 [#events] 每个监控项保留状态变化的历史记录。记录只在发生变化时写入,而非每次检查都写: | 事件 | 触发条件 | | ----- | -------------- | | 已添加 | 监控项创建 | | 证书已更换 | 服务器提供的证书指纹发生变化 | | 证书链损坏 | 出现导致浏览器拒绝的信任问题 | | 证书链恢复 | 上述问题消失 | | 证书过期 | 证书超过有效期 | | 变为不可达 | 连续失败达到阈值 | | 已恢复 | 从不可达状态恢复 | 正常运行的服务每季度大约产生一条记录(证书更换),因此这份历史是可以逐条阅读的。 ## 相关文档 [#related] * [告警与通知](/docs/monitoring/alerts)——提醒在什么条件下发出、通过什么渠道送达 * [证书透明度监控](/docs/monitoring/certificate-transparency)——发现不是本站签发的证书 * [排错指南](/docs/troubleshooting)——每一种检查结论对应的处理方法 # acme.sh 说续期成功了,证书却还是旧的 (/docs/operations/acme-sh-renewal-failed) acme.sh 的定时任务跑完没报错,日志里写着 `Cert success`,浏览器里看到的却还是那张旧证书 —— 甚至已经过期了。 这句「成功」和你看到的现象不矛盾,因为它们说的不是同一件事。**acme.sh 的成功只到「新证书拿到手并写进文件」为止,把它交给正在跑的服务是另一件事**,而那一步失败时通常是静默的。 所以排查的第一步不是看日志,是先判断自己在哪条岔路上。 ## 先分岔:是没续下来,还是续下来了没生效 [#fork] ```bash # ① acme.sh 自己那份,什么时候签的 acme.sh --info -d example.com | grep -E "Le_CertCreateTimeStr|Le_NextRenewTimeStr" # ② 服务器实际发给访客的那份 echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -dates ``` | ① 和 ② 的关系 | 你在这条岔路 | | --------------- | --------------------------------------- | | ① 是新的,② 是旧的 | **续下来了,但没生效** → 第 1、2、3 节 | | ① 也是旧的 | **根本没续下来** → 第 4、5 节 | | `--info` 什么都没输出 | 域名不在这个 acme.sh 账户下,或者你查的用户不对 → 第 4 节第一段 | 别用浏览器判断 ②。浏览器会缓存连接和中间证书,你刷新看到的可能不是访客看到的。上面那条 `openssl` 命令问的是服务器当下真正发出的东西。 ## 1. reloadcmd 没配,或者配了但没执行 [#reloadcmd] 这是最常见的一种。`--install-cert` 时如果没给 `--reloadcmd`,acme.sh 会把新证书写到目标路径,然后什么也不做 —— nginx 仍然握着启动时读进内存的那份旧证书。 ```bash acme.sh --info -d example.com | grep Le_ReloadCmd ``` 空的就是没配。补上: ```bash acme.sh --install-cert -d example.com \ --key-file /path/to/key.pem \ --fullchain-file /path/to/fullchain.pem \ --reloadcmd "nginx -s reload" ``` `--install-cert` 是幂等的,重复执行只是更新配置,不会重新签发。 ## 2. 宝塔面板的 nginx 不吃 force-reload [#baota-reload] 很多教程里的 reloadcmd 写的是 `service nginx force-reload`。宝塔自带的 nginx 不支持这个动作,命令直接失败,而 acme.sh 不会因为 reloadcmd 失败就把整次续期判为失败。 宝塔环境下改成: ```bash --reloadcmd "/etc/init.d/nginx reload" ``` 改完之后手动跑一次 reloadcmd 本身,确认它真的返回 0: ```bash /etc/init.d/nginx reload; echo "退出码 $?" ``` ## 3. 宝塔把证书存在自己的目录里 [#baota-path] 宝塔管理的站点,证书读的是它自己的路径: ``` /www/server/panel/vhost/cert/example.com/fullchain.pem /www/server/panel/vhost/cert/example.com/privkey.pem ``` 而 acme.sh 默认把证书放在 `~/.acme.sh/example.com_ecc/`(ECC 证书带 `_ecc` 后缀)。两边是两份文件,acme.sh 更新了自己那份,宝塔那份纹丝不动。 **不要手工复制**,那只能解决这一次。让 `--install-cert` 直接写进宝塔的路径: ```bash acme.sh --install-cert -d example.com --ecc \ --key-file /www/server/panel/vhost/cert/example.com/privkey.pem \ --fullchain-file /www/server/panel/vhost/cert/example.com/fullchain.pem \ --reloadcmd "/etc/init.d/nginx reload" ``` `--ecc` 不能省,如果你的证书是 ECC 的。acme.sh 用它区分同一域名下的 RSA 与 ECC 两套目录,漏掉时会提示找不到证书,或者安装的是另一套。 ## 4. 定时任务里的路径和实际安装位置不一致 [#cron-home] acme.sh 的定时任务长这样: ``` 0 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null ``` 如果安装时用的是别的用户、或者中途换过 `--home`,crontab 里这个路径就会指向一个空目录 —— 任务照常运行、照常退出 0、什么也没做。而 `> /dev/null` 把唯一能看出问题的输出也丢掉了。 ```bash # crontab 里写的是哪个路径 crontab -l | grep acme # 证书实际在哪 ls -d ~/.acme.sh/*/ 2>/dev/null ``` 两者不一致就改 crontab。顺便把 `> /dev/null` 换成一个日志文件,下次就不用猜了: ``` 0 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" >> /var/log/acme.log 2>&1 ``` 用 `root` 装的 acme.sh,定时任务也必须在 root 的 crontab 里。`sudo crontab -l` 和 `crontab -l` 看到的是两份不同的表,这是「我明明配了定时任务」最常见的误会来源。 ## 5. acme.sh 版本太旧 [#outdated] acme.sh 装上之后不会自己升级,除非开了自动升级。旧版本会在 CA 那边的接口调整后突然失效,而且报错通常发生在网络层,看起来像是网络问题。 ```bash acme.sh --version acme.sh --upgrade acme.sh --upgrade --auto-upgrade # 以后自动升级 ``` 升级不会动已有证书和配置。 ## 确认真的修好了 [#verify] 不要等下一次自动续期。强制跑一次完整流程: ```bash acme.sh --renew -d example.com --ecc --force ``` 然后回到本文开头那条 `openssl s_client` 命令,确认服务器发出的日期变了。**只有这一条能证明修好了** —— acme.sh 自己的日志只能证明前半段。 ## 如果你不想再维护这条链路 [#alternative] 上面五种成因有一个共同点:它们都不是 acme.sh 的 bug,而是**这条链路上环节太多**。签发、写文件、重载服务、定时任务、用户与路径,任何一环安静地断掉,结果都是「日志说成功,证书是旧的」,而你只会在证书过期那天知道。 它还依赖一件事:那台跑 cron 的机器要一直活着、一直被维护。 本站的[证书申请](/docs/free-cert)把这条链路缩短了一半 —— 验证环节用[一条 CNAME 委派](/docs/free-cert/dns-delegation)后不再需要人工,签发与续期由我们完成。**部署那一步仍然在你这边**,这一点我们不打算含糊其辞:证书还是要装到你的服务器上。 真正的区别是,需要一直活着的那台机器不再是你的。 另外,无论你用哪种方式续期,都建议把域名加入[证书监控](/docs/monitoring):它检查的是**访客实际拿到的那张证书**,也就是本文开头的 ②。这正是所有「续期脚本说成功了」的故障唯一逃不过的那一关。 # pem、crt、cer、pfx、jks:证书格式到底有几种,怎么互转 (/docs/operations/certificate-formats) 从 CA 拿到的文件叫 `fullchain.pem`,Tomcat 要 `.jks`,IIS 要 `.pfx`,某个设备的后台只收 `.cer` —— 看起来是四种东西,实际上只有两个维度:**编码**和**容器**。 先把这两个维度分清楚,大部分「转换」就会变成「改个扩展名」或者「一条命令」。 ## 两个维度 [#two-axes] **编码**决定字节长什么样,只有两种: | 编码 | 内容 | 怎么认 | | ------- | ------------------------------------ | ------------- | | **PEM** | Base64 文本,带 `-----BEGIN ...-----` 头尾 | 用文本编辑器能打开、能看懂 | | **DER** | 同样的数据,二进制 | 打开是乱码 | **容器**决定一个文件里装了什么: | 容器 | 装了什么 | 典型使用者 | | ----------- | -------------------------------- | ------------------ | | 单独的证书文件 | 一张或多张证书,没有私钥 | nginx、Apache | | 单独的私钥文件 | 只有私钥 | nginx、Apache | | **PKCS#12** | 证书 + 私钥 + 证书链,**打包在一个文件里**,通常有密码 | IIS、Windows、部分负载均衡 | | **JKS** | Java 自己的密钥库格式 | 老版本 Tomcat、Java 应用 | ## 扩展名其实不说明格式 [#extensions] 这是最大的困惑来源:**扩展名是约定,不是规范**。 | 扩展名 | 实际上通常是 | 注意 | | --------------- | --------------------- | --------------------------------- | | `.pem` | PEM 编码,内容不定 | 可能是证书、私钥,或者两者都有 | | `.crt` / `.cer` | 证书,**PEM 或 DER 都有可能** | `.cer` 在 Windows 生态里更常见,但两者没有硬性区别 | | `.key` | 私钥,通常 PEM | | | `.der` | DER 编码 | | | `.pfx` / `.p12` | PKCS#12 | 两个扩展名完全等价 | | `.jks` | Java 密钥库 | | | `fullchain.pem` | 服务器证书 + 中间证书,PEM | Let's Encrypt 的命名习惯 | 所以「`.crt` 转 `.pem`」这个说法本身是空的 —— 得先看它里面是 PEM 还是 DER: ```bash # 能读出内容 = 已经是 PEM,改个扩展名就行 openssl x509 -in cert.crt -noout -subject # 上面报错,再试 DER openssl x509 -in cert.crt -inform der -noout -subject ``` ## 转换命令 [#commands] ### PEM ↔ DER [#pem--der] ```bash openssl x509 -in cert.pem -outform der -out cert.der openssl x509 -in cert.der -inform der -out cert.pem ``` ### PEM → PFX(给 IIS / Windows) [#pem--pfx给-iis--windows] ```bash openssl pkcs12 -export \ -inkey privkey.pem \ -in cert.pem \ -certfile chain.pem \ -out cert.pfx ``` `-in` 放服务器证书,`-certfile` 放中间证书。**如果你手上只有 `fullchain.pem`**(服务器证书和中间证书已经拼在一起),直接把它给 `-in`、省掉 `-certfile` 也可以。 导出时会要求设置密码。IIS 导入时要用同一个密码,**不能留空** —— 部分 Windows 版本对空密码的 PKCS#12 会直接报「密码不正确」,而不是提示不支持。 ### PFX → PEM [#pfx--pem] ```bash # 证书部分(含链) openssl pkcs12 -in cert.pfx -clcerts -nokeys -out cert.pem # 私钥部分,-nodes 表示不给私钥再加密 openssl pkcs12 -in cert.pfx -nocerts -nodes -out privkey.pem # 链上的其余证书 openssl pkcs12 -in cert.pfx -cacerts -nokeys -out chain.pem ``` 导出的 PEM 文件头部会带一段 `Bag Attributes` 的说明文字。nginx 不介意,但某些较真的解析器会,删掉 `-----BEGIN` 之前的所有行即可。 ### PEM → JKS(给 Tomcat / Java) [#pem--jks给-tomcat--java] 没有直接的命令,标准做法是**先转成 PKCS#12,再让 keytool 导入**: ```bash openssl pkcs12 -export -inkey privkey.pem -in fullchain.pem \ -name tomcat -out cert.p12 keytool -importkeystore \ -srckeystore cert.p12 -srcstoretype pkcs12 \ -destkeystore cert.jks -deststoretype JKS \ -srcalias tomcat -destalias tomcat ``` `-name` / `-alias` 要一致,Java 应用的配置里引用的就是这个别名。 Tomcat 8.5 以上可以直接使用 PKCS#12,不必再转 JKS —— 在 `server.xml` 里把 `keystoreType` 写成 `PKCS12` 并指向 `.p12` 文件即可。JKS 是旧格式,能不转就不转。 ## 转完之后验证 [#verify] 改扩展名、拆包、重新打包,每一步都有把证书和私钥配错的可能,而配错的后果是服务起不来或者握手失败 —— 到那时再回头找原因很费劲。两条命令就能确认: ```bash # 一、证书和私钥是不是一对(两行输出必须一致) openssl x509 -in cert.pem -noout -pubkey | openssl sha256 openssl pkey -in privkey.pem -pubout | openssl sha256 # 二、链是否完整、顺序是否正确 openssl verify -untrusted chain.pem cert.pem ``` 第一条用 `pkey` 而不是 `rsa`,因为 ECC 私钥不吃 `openssl rsa`。`pkey` 对 RSA 和 ECC 都有效。 第二条返回 `cert.pem: OK` 才算过。 报 `unable to get local issuer certificate` 时,先确认 `chain.pem` 里是**全部**中间证书,而不只是一张。 当前的 Let's Encrypt 链在中间证书之上还带一张交叉签名的根证书,只提取「那张中间证书」会得到同样的报错 —— 而链本身是好的。把服务器发出的除第一张之外的证书全部拼进 `chain.pem` 再验一次。 确认链确实完整之后仍然报这个错,才是真的缺东西,见[证书链不完整](/docs/troubleshooting/incomplete-chain)。 另外,`openssl verify` 要在系统的根证书库里找最终的根。精简的容器镜像里常常没有装(Alpine 需要 `apk add ca-certificates`),这时它对任何证书都会报同一个错。 ## 几个反复出现的坑 [#pitfalls] **私钥带密码。** `openssl rsa -in enc.key -out plain.key` 可以去掉密码。nginx 也支持带密码的私钥,但那意味着每次重启都要人工输入 —— 对自动续期来说是致命的。 **fullchain 的顺序。** 服务器证书必须在最前,然后是中间证书,**根证书不要放进去**。顺序错了部分客户端会验证失败,而另一部分不会,于是表现成「只有某些人打不开」。 **Windows 换行。** 在 Windows 上编辑过的 PEM 文件可能带 `\r\n`,个别老解析器会因此失败。`dos2unix cert.pem` 或 `sed -i 's/\r$//' cert.pem` 可以处理。 **同名不同物。** 很多面板把上传框标成「证书(crt)」和「密钥(key)」,但实际期望的是 fullchain 而不是单张证书。装完之后用[证书检测](/ssl-checker)确认一次链是完整的,比逐个面板去猜它要什么快得多。 # 证书有效期正在缩短:200 天已经生效,2029 年降到 47 天 (/docs/operations/shrinking-lifetimes) 公开信任的 TLS 证书最长能签多久,正在被分三步砍掉。这不是提案,是 CA/浏览器论坛在 2025 年 4 月通过的 SC-081v3 决议,**而且第一阶段在 2026 年 3 月 15 日就已经生效了**。 如果你今年买的证书突然不是一年期了,原因在这里。 ## 时间表 [#timeline] 两条线同时收紧,第二条比第一条更值得注意。 | 签发日期在 | 证书最长有效期 | 域名验证结果最长可复用 | | ------------ | ------------------ | ----------- | | 2026-03-15 起 | **200 天** ← 当前所处阶段 | 200 天 | | 2027-03-15 起 | **100 天** | 100 天 | | 2029-03-15 起 | **47 天** | **10 天** | 「域名验证结果可复用多久」指的是:CA 确认过你控制这个域名之后,这个结论能撑多久再签下一张证书而不用重新验证一次。它从原先的 398 天一路降到 10 天。 规则约束的是**签发日期**,不是到期日期。2026 年 3 月 15 日之前签出来的长期证书不会被提前吊销,可以用到它自己的到期日为止。 ## 到 2029 年,手工方式在算术上就不成立了 [#arithmetic] 把两条线放在一起看:**证书 47 天一换,而域名验证结果只能复用 10 天。** 意味着每个域名每年要重新证明一次控制权 **35 次左右**,证书本身要换 **约 8 次**。 这不是「更麻烦一点」。一个人如果同时管着十个域名,那是每年 350 次域名验证 —— 手工做这件事的方案,不是变累,是不存在了。 ## 对你的实际影响,取决于你现在怎么做 [#impact] | 你现在的做法 | 2026(现在) | 2027 | 2029 | | -------------------------------------- | ------------------ | ------- | ------------- | | 买一年期商业证书,到期手动换 | **已经受影响**:最长 200 天 | 一年换 4 次 | 不可行 | | 用云厂商的免费证书(多为 3 个月),手动换 | 暂无变化 | 暂无变化 | 一年换 8 次 | | Let's Encrypt + acme.sh / certbot 自动续期 | 无感 | 无感 | 需确认脚本与验证方式跟得上 | | 用了自动化,但域名验证仍要人工加 TXT | 暂时可行 | 开始难受 | **最先崩的就是这一类** | 最后一行值得单独说:很多人的「自动化」只自动了签发,验证那一步仍然是每次手工去 DNS 控制台加一条 TXT 记录。**复用期降到 10 天之后,这件事一年要做 35 次。** ## 现在该做什么 [#prepare] **2029 年还很远,但 2027 年不远。** 到 100 天那一档,手工换证书会从「一年一次的杂事」变成「每季度一次的固定任务」,而这是大多数人开始出事的临界点 —— 一年一次记得住,每季度一次容易漏。 三件事按重要性排序: **一、把域名验证自动化,优先于把签发自动化。** 这与直觉相反,但看上面的数字:验证的频率会是签发的四倍多。验证还靠人工的自动化,2029 年会先在验证那一环断掉。DNS-01 的 CNAME 委派是目前最省事的做法 —— 在你自己的域名下加一条 CNAME,此后验证不再需要人。本站的做法见 [DNS 委派](/docs/free-cert/dns-delegation);自建 acme-dns 也是同一个原理。 **二、确认「续期成功」的定义包含了部署。** 签发自动化之后最常见的故障,是新证书签出来了但服务器还在用旧的 —— 频率翻四倍之后,这类静默故障的出现次数也翻四倍。排查见 [acme.sh 续期成功但证书没更新](/docs/operations/acme-sh-renewal-failed)。 **三、加一层与续期链路无关的监控。** 所有自动化都有静默失败的可能,而自动化自己的日志证明不了访客看到了什么。[证书监控](/docs/monitoring)从外部检查服务器实际发出的证书 —— 周期变短之后,这层兜底从「值得有」变成「必须有」。 ## 为什么要这么改 [#why] 两个理由,官方文件里都写了。 **缩短证书有效期**,是为了限制私钥泄露或证书误签之后的暴露窗口。吊销机制在现实中并不可靠 —— 很多客户端根本不检查吊销状态,所以「让它自己尽快过期」比「吊销它」更管用。 **缩短验证复用期**,是为了让证书反映的是**现在**的域名归属。域名会转手、会过期被别人注册,而一次验证结果能用 398 天,意味着最长可能有一年多的时间里,证书证明的是一件已经不成立的事。 这两条合起来,行业的方向是明确的:**证书正在从「一件每年办一次的手续」变成「一个必须一直运行的流程」。** *** **参考**:[CA/Browser Forum — Ballot SC081v3](https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/)(决议原文与投票结果) # 证书过期了怎么办 (/docs/troubleshooting/certificate-expired) 证书过期没有「延期」这一说:有效期是签进证书里的,一旦签好就改不了。恢复访问的唯一办法是**签发一张新证书,替换服务器上的文件,然后重载服务**。 整个过程通常十几分钟。但在开始之前,先花一分钟排除两种假过期 —— 每年都有人在这两种情况下白折腾一遍。 ## 先确认是不是真的过期了 [#confirm] 用这条命令直接读服务器上正在用的证书的有效期,它绕过了浏览器缓存和本机时钟: ```bash openssl s_client -connect example.com:443 -servername example.com /dev/null \ | openssl x509 -noout -dates -subject # notBefore=Jun 3 08:12:41 2026 GMT # notAfter=Sep 1 08:12:40 2026 GMT ``` 把 `notAfter` 和当前时间比一下。如果它其实还在有效期内,那你遇到的是下面两种情况之一: ### 情况一:报错的那台机器时钟不对 [#情况一报错的那台机器时钟不对] 证书是否过期由客户端拿自己的系统时间判断。一台时间设成明年的手机,会对每一个网站都报证书过期;一台时间落后的机器则会报「证书尚未生效」。如果只有某一台设备报错,先看它的日期。 ### 情况二:证书换了,但服务没重载 [#情况二证书换了但服务没重载] 证书文件是在启动或重载时读进内存的。续期脚本把文件换掉之后,如果没有重载,线上跑的还是那张过期的证书 —— 磁盘上是新的,内存里是旧的,上面那条命令读到的会是旧的。这种情况直接重载即可,不必重新签发。 ```bash nginx -t && nginx -s reload # 或 systemctl reload nginx ``` 想省掉命令行,跑一次[证书检测](/ssl-checker):它从公网发起一次真实握手,读到的就是访客读到的那张证书,也会直接告出已经过期多少天。 ## 确实过期了:恢复访问的步骤 [#fix] 1. **签发一张新证书。** 本站提供基于 Let's Encrypt 的[免费签发](/free-cert),加一条 DNS TXT 记录验证域名所有权即可,不需要在服务器上装任何东西,也不需要开放 80 端口。 2. **把新的证书和私钥放到服务器上。** 建议放在和旧文件相同的路径,这样配置不用改;如果换了路径,记得同时改配置里的两处。 3. **用 fullchain,不要用只有单张证书的那个文件。** 这一步弄错不会报过期,会变成另一个更难发现的故障 —— 见[证书链不完整](/guides/incomplete-chain)。 4. **重载服务**,然后再检测一次确认。 过期期间不要图快把 HTTPS 关掉改回 HTTP。已经开过 HSTS 的站点这么做会让浏览器直接拒绝连接,而不是退回明文,反而更难恢复。 ## 换完还是报过期?按这几处查 [#still-expired] * **没重载。** 最常见的一条,前面说过了。 * **有多个站点配置块。** 同一台机器上多个 `server` 块 / 虚拟主机各自指定证书路径,只改了一处。`grep -r ssl_certificate /etc/nginx/` 一次找全。 * **多台机器或多个容器。** 负载均衡后面有几台就有几份证书,滚动更新时容易漏。 * **CDN / SLB 上还挂着旧证书。** 源站换了但边缘没换,访客拿到的是边缘那张。云厂商的证书管理控制台是独立的一处,必须单独更新。 * **浏览器缓存了旧连接。** 换一个无痕窗口或用上面的 `openssl` 命令确认,不要凭刷新判断。 ## 下次别再过期 [#prevent] 证书有效期还在继续变短。CA/浏览器论坛已经通过了逐步压缩上限的时间表:2026 年 3 月起最长 200 天,2027 年 3 月起 100 天,2029 年 3 月起 47 天。也就是说,靠日历提醒手工换证这条路会越来越难走,最终必须自动化。 两件事值得现在就做: * **让续期自动跑起来。** 在本站签发的证书,到期前 30 天会自动创建续期申请并邮件提醒;用 certbot 的话确认 `renew` 的定时任务在跑,并且 `--deploy-hook` 里带了重载命令。 * **加一层独立的到期监控。** 这一条容易被跳过,但它防的恰好是自动化本身失效的情况 —— 续期脚本挂了、hook 没重载、CDN 上那份忘了换。从外部定期握手复查,是唯一能发现「我以为已经自动化了」的办法。本站的[到期监控](/ssl-checker#pricing)免费额度是 5 个地址。 # 证书快到期了:先分清是没续期,还是续了没部署 (/docs/troubleshooting/certificate-expiring) 先别急着签新证书。**很多「快到期」其实是续期已经成功、只是没部署**——新证书签出来了,躺在磁盘上或者在 CA 那边,而服务器进程还握着旧文件。这种情况签第二张没有任何用。 判断方法很简单:看证书透明度日志里有没有一张更新的、覆盖同一批域名的证书。有,就是部署问题;没有,才是续期本身没跑起来。 ## 先确认是哪一种 [#which] | CT 日志里有更新的证书 | 含义 | 该做的事 | | ------------ | ------------- | ------------ | | 有 | **已经签发,没有部署** | 把新文件放到位并重载服务 | | 没有 | **续期没有成功** | 查续期任务本身 | 本站的域名监控页会直接给出这个判断——它把握手看到的证书和 CT 日志里的证书放在一起比。 ## 已经签发但没部署 [#not-deployed] 最常见的原因是**重载被漏掉了**。证书文件是新的,但 nginx / Apache / Java 进程在启动时把证书读进了内存,不重载就一直用旧的。 ```bash title="确认服务器当前真正在提供哪张证书" echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -dates -serial ``` * **没重载**:`nginx -s reload` / `systemctl reload nginx`。certbot 的 `--deploy-hook` 就是干这个的。 * **文件路径不对**:续期写到了 `/etc/letsencrypt/archive/...`,而配置指向另一个目录。应当指向 `live/` 下的软链。 * **多台机器**:证书只更新了一台,负载均衡后面还有别的机器在发旧证书。 * **CDN / 负载均衡器**:源站换了,但边缘节点上传的还是旧证书。 ## 续期本身没跑起来 [#renewal-failed] * **验证方式失效**:HTTP-01 的 `.well-known` 路径被重定向或防火墙挡了;DNS-01 的 API 凭据过期。 * **定时任务没执行**:cron / systemd timer 被停掉,或者容器重建后任务没了。 * **触发太晚**:证书只剩几天才开始重试,一旦遇到限流就没有余量了。 * **域名解析变了**:域名换了服务商或指向,验证走不通。 自动续期最危险的地方在于它失败时是安静的。真正靠得住的做法是同时监控**结果**——直接去看服务器在提供的那张证书还有多少天,而不是相信续期任务的退出码。 ## 应该提前多久处理 [#threshold] | 剩余天数 | 含义 | | ----- | ------------------------------- | | 30 天 | 自动续期本应在这时已经完成。到这里还没换,说明续期链路有问题。 | | 14 天 | 留给排查和人工介入的时间。 | | 7 天以内 | 按故障处理。遇到 CA 限流或验证问题,可能来不及。 | 90 天有效期的证书通常在第 60 天续期,新旧并存 30 天——所以看到「另有一张仍有效的旧证书」是正常现象,不是重复签发。 # 证书还没到生效时间:多半是时钟不对 (/docs/troubleshooting/certificate-not-yet-valid) 证书上有两个时间:生效(notBefore)和到期(notAfter)。**当前时间早于生效时间**,验证方就会拒绝它——和过期一样彻底。 但证书真的「签早了」是极少数情况。绝大多数时候是**某一端的时钟不对**,而且往往是验证方那一端。 ## 先看是谁的时间不对 [#whose-clock] ```bash title="看证书上的生效时间,和本机时间比" echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -dates date -u ``` | 情况 | 含义 | | -------------- | ---------------- | | 本机时间明显偏早 | **客户端时钟错了**——最常见 | | 本机时间正确、生效时间在未来 | 证书确实签早了,或者拿错了文件 | | 只有部分客户端报错 | 那些客户端的时钟不对 | ## 时钟为什么会偏 [#why] * **没装或没跑 NTP**:虚拟机、容器、嵌入式设备最常见。 * **掉电后时间回退**:没有电池的设备重启会回到出厂时间。 * **时区当成了时间**:配置里写错时区导致偏移若干小时,证书刚签发时正好卡在窗口里。 * **镜像里固化了旧时间**:构建镜像时的时间被带进了运行环境。 ```bash title="同步时间" timedatectl set-ntp true # systemd chronyc makestep # chrony ntpdate -u pool.ntp.org # 老系统 ``` ## 证书确实签早了 [#really-early] 少数 CA 会把生效时间设在签发时刻**之前**几小时,以容忍客户端时钟误差;也有相反的情况。如果确认本机时间正确而证书生效时间在未来,通常是: * 部署了**还没到生效时间的续期证书**——续期提前签好,但提前部署了。等到生效时间即可,或者先回退到当前这张。 * **拿错了文件**:把另一个环境或另一次签发的证书发上去了。核对序列号。 # 证书透明度日志里出现了没见过的证书 (/docs/troubleshooting/certificate-transparency) 所有受信任 CA 签发的证书都必须提交到**公开的证书透明度日志**,任何人都能查。这是浏览器的强制要求,目的就是让误签和偷签无处藏身。 所以「你的域名下出现了一张新证书」是一条能被察觉的事实。大多数时候它有正当来源;少数时候,它是你最需要知道的一件事。 ## 按可能性从高到低排查 [#causes] | 可能性 | 怎么确认 | | --------------- | ------------------------------ | | **其他团队自己申请的** | 问一圈运维、前端、某条业务线——最常见的答案 | | **CDN / 云厂商代签** | 接入 CDN、负载均衡、对象存储自定义域名时,厂商会自动签发 | | **历史采购遗留** | 以前买的商业证书还在自动续期,当事人可能已经离职 | | **子域名被接管** | **最危险的一种**,见下一节 | 确认是自己人申请的之后,在证书上**标记为可信**,以后它的续期不再提示。 ## 需要警惕的情况:子域名接管 [#takeover] 如果一个子域名的 CNAME 指向了已经释放的云资源——下线的对象存储桶、注销的 SaaS 账号、删掉的负载均衡——别人可以重新占用那个资源,然后**用你的子域名申请一张合法证书**。 * 证书是**真的**:公开 CA 签发,浏览器完全信任,访客不会看到任何警告。 * 对方可以在这个子域名上放任何内容,拿 Cookie、做钓鱼。 * **CT 日志往往是唯一的早期信号**。你自己的监控发现不了,因为那个子域名根本不在监控列表里。 ```bash title="检查一个可疑子域名指向哪里" dig +short CNAME suspicious.example.com curl -sI https://suspicious.example.com | head -5 ``` 发现悬空的 CNAME 就立即删掉解析记录。资源已经释放的情况下,保留解析没有任何用处,只留下一个入口。 ## 怎么减少这类意外 [#prevent] * **加 CAA 记录**,限定哪些 CA 可以为这个域名签发。CA 在签发时必须检查它(RFC 8659),这是强制要求,不只是建议。 * **下线服务时先删解析,再释放云资源**。顺序反了就会留下悬空记录。 * **把子域名也加入监控**,这样它们的证书变化同样会被看到。 ## 另一种情况:当前证书在日志里找不到 [#not-in-logs] 如果提示的是**当前提供的证书在 CT 日志中找不到**,通常说明它由私有 CA 或自签名产生——这类证书不会进入公开日志。 内网服务这样用是正常的。但要知道一个后果:**「出现未知新证书」的提醒对这个端点不会生效**,因为无法把它和日志里的记录对上。这个功能在这里是失效的,而不是「一切正常」。 # 无法连接:证书检测连不上服务器时的排查顺序 (/docs/troubleshooting/connection-failed) 这条结论**不是关于证书的**。TLS 握手压根没开始,所以证书是好是坏、有没有过期,这次检测都无从知道。 先按 DNS → 端口 → 防火墙 → TLS 的顺序排,错误码会直接告诉你卡在哪一层。 ## 错误码对应哪一层 [#codes] | 错误码 | 含义 | 先查什么 | | -------------- | --------------- | ---------------------- | | `ENOTFOUND` | 域名解析不出 IP | DNS 记录是否存在、是否刚改过还没生效 | | `ECONNREFUSED` | IP 通,443 端口没人监听 | 服务是否在跑、是否只监听了 80 | | `ETIMEDOUT` | 包发出去没有回应 | 防火墙、安全组、云厂商的入方向规则 | | `ECONNRESET` | 连上了又被断开 | 反向代理配置、SNI 不匹配、被中间设备拦截 | | `EPROTO` | TLS 层协商失败 | 协议版本、加密套件是否被限制得太严 | ## 逐层确认 [#steps] ```bash title="1. 域名解析到哪里" dig +short example.com ``` ```bash title="2. 443 端口通不通" nc -vz example.com 443 ``` ```bash title="3. TLS 能不能握手" openssl s_client -connect example.com:443 -servername example.com 第 2 步通、第 3 步不通,才说明问题真的在 TLS 层。前两步不通的话,证书配置改多少次都没用。 ## 只对我们连不上的情况 [#our-side] * **只监听内网**:服务绑定在 `127.0.0.1` 或内网网卡上,公网访问不到。 * **安全组只放行了特定来源**:检测服务器的 IP 不在白名单里,而你自己的机器在。 * **WAF / 防爬**:自动化请求被识别并丢弃,浏览器访问却正常。 * **只在特定线路可达**:境内外解析不同,或者部署在只对内开放的环境。 这几种情况下,站点对真实访客可能完全正常。监控要能持续工作,就需要让检测来源能够访问到这个端点。 # 「您的连接不是私密连接」:先找错误代码,再决定修哪里 (/docs/troubleshooting/connection-not-private) 浏览器拦下一个 HTTPS 网站时,会用一个红色页面告诉你「您的连接不是私密连接」。Firefox 说的是「警告:面临潜在的安全风险」,措辞不同,含义一样:**证书验证没通过,所以在你确认之前,浏览器不打算把内容交给你。** 麻烦的地方在于,这一句话背后是七八种成因完全不同的故障 —— 证书过期、少签了域名、链不完整、签发机构不受信任……它们的修法互不相干,而这句话对所有情况都一模一样。 真正的线索是红屏下面那行大写代码。 ## 先找到错误代码 [#find-code] | 浏览器 | 代码在哪里 | | ------------- | --------------------------------------------- | | Chrome / Edge | 「您的连接不是私密连接」下方,`NET::ERR_` 开头的一行;被折叠时点「高级」 | | Firefox | 点「高级…」,展开后的段落里,`SEC_ERROR_` 或 `SSL_ERROR_` 开头 | | Safari | 点「显示详细信息」,措辞比代码多,按下一节的症状对照 | 如果页面上完全没有 `ERR_` 字样,而是写着「此网站无法提供安全连接」,那不是证书问题,跳到[最后一节](#not-a-cert-problem)。 ## 按代码对号入座 [#by-code] | 错误代码 | 实际发生了什么 | 怎么修 | | ---------------------------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NET::ERR_CERT_DATE_INVALID` | 证书过期了,或者还没到生效时间 | [证书过期](/docs/troubleshooting/certificate-expired) · [还没到生效时间](/docs/troubleshooting/certificate-not-yet-valid) | | `NET::ERR_CERT_COMMON_NAME_INVALID`
`SSL_ERROR_BAD_CERT_DOMAIN` | 证书是有效的,但上面没有你访问的这个域名 | [证书不包含这个域名](/docs/troubleshooting/hostname-mismatch) · [泛域名不覆盖主域名](/docs/troubleshooting/wildcard-does-not-cover-apex) | | `NET::ERR_CERT_AUTHORITY_INVALID`
`SEC_ERROR_UNKNOWN_ISSUER` | 追不到一个受信任的签发机构 | [证书链缺中间证书](/docs/troubleshooting/incomplete-chain) · [自签名证书](/docs/troubleshooting/self-signed-certificate) · [追溯不到受信任的根](/docs/troubleshooting/untrusted-root) | | `NET::ERR_CERT_REVOKED` | 证书被签发机构吊销了 | [证书被吊销](/docs/troubleshooting/revoked-or-unverified) | | `NET::ERR_CERT_WEAK_KEY`
`NET::ERR_CERT_WEAK_SIGNATURE_ALGORITHM` | 密钥长度或签名算法已不被接受 | [RSA 密钥长度不足](/docs/troubleshooting/weak-key) | | `ERR_CERTIFICATE_TRANSPARENCY_REQUIRED` | 证书没有出现在公开的透明度日志里 | [证书透明度日志](/docs/troubleshooting/certificate-transparency) | `NET::ERR_CERT_AUTHORITY_INVALID` 那一行有三篇文章,因为它是最含糊的一个代码:自签名、私有 CA、公开 CA 但少发了中间证书,浏览器给出的反应完全相同。分辨方法在那三篇里都有对应的命令。 不想逐条对照的话,把域名填进[证书检测](/ssl-checker),它会直接查出是哪一种,并给出对应的处理办法 —— 免登录,不用装东西。 ## 先分清:只有你看到,还是所有人都看到 [#who-sees-it] 这一步比找代码更值得先做,因为它决定问题在谁那里。 **换一个网络访问同一个地址** —— 用手机流量,或者让一个不在同一个办公室的人打开看看。 * **别人正常,只有你不行** → 问题在你这一侧。三个常见原因:本机时钟不准(证书有效期是按时间判断的,差几天就会报 `ERR_CERT_DATE_INVALID`);杀毒软件或企业网关开了 HTTPS 扫描,用自己的证书替换了真的那张;连着会劫持流量的公共 Wi-Fi。 * **所有人都不行** → 问题在服务器或证书上,按上一节的代码找对应的文章。 ## 如果你是访客,不是这个站的管理员 [#visitor] 你能做的只有排除自己这一侧:**校准系统时间**、**换个网络**、**临时关掉杀毒软件的 HTTPS 扫描**试一次。如果换了网络还是报错,那就是对方的站点配错了,你修不了,只能等他们修。 网上流传的做法是在红屏页面上直接敲 `thisisunsafe`(不是敲进地址栏)强行进入。它确实能进去,但它**不解决任何问题,只是让浏览器不再提醒你**。 这个警告存在的意义,是「配置错误」和「有人正在中间解密你的流量」看起来完全一样,浏览器分不出来,你也分不出来。绕过它,就是放弃这个唯一的区分手段。在自己的开发机或内网服务上、并且你确切知道警告为什么出现时,这么做是合理的;在任何需要输入密码、付款或查看私人信息的站点上,不要。 ## 如果你是站长 [#owner] 先确认自己看到的和访客看到的是同一件事 —— 你的浏览器可能缓存过中间证书,因而比访客更宽容: ```bash # 服务器实际发出的完整证书链,以及验证结果 openssl s_client -connect example.com:443 -servername example.com /dev/null \ | openssl x509 -noout -subject -issuer -dates # 只看链是否完整:结果不是 0 就说明访客那边会报错 echo | openssl s_client -connect example.com:443 -servername example.com 2>&1 \ | grep "Verify return code" ``` `Verify return code: 0 (ok)` 以外的任何结果,都对应上面表格里的某一行。 ## 「此网站无法提供安全连接」不是证书问题 [#not-a-cert-problem] 这句话(Chrome 里通常伴随 `ERR_SSL_PROTOCOL_ERROR`)说的是**握手本身就没谈成**,根本没走到验证证书那一步。常见成因是客户端和服务器没有共同支持的 TLS 版本或加密套件 —— 比如服务器只留了 TLS 1.2/1.3,而访问者用的是很旧的系统或设备。 排查方向见[关闭 TLS 1.0 / 1.1](/docs/troubleshooting/disable-legacy-tls)。如果连接干脆超时或被拒绝,而不是握手失败,那是[无法连接](/docs/troubleshooting/connection-failed)。 # 关闭 TLS 1.0 / 1.1,只留 1.2 和 1.3 (/docs/troubleshooting/disable-legacy-tls) TLS 1.0 和 1.1 在 2020 年就被 Chrome、Firefox、Safari、Edge 集体废弃了,PCI DSS 也要求禁用 TLS 1.0。现在正确的配置是**只保留 TLS 1.2 和 1.3**。 改动很小 —— nginx 一行,Apache 一行。真正需要判断的是兼容性代价:确认没有还在用老客户端的调用方,再动手。 ## 改配置 [#config] ### nginx [#nginx] ```nginx title="nginx.conf" ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 时代,让客户端选套件通常比服务器强制更好 ssl_prefer_server_ciphers off; ``` TLS 1.3 需要 nginx 1.13.0 以上并链接 OpenSSL 1.1.1 以上。`nginx -V` 能看到编译时用的 OpenSSL 版本;如果偏低,写上 `TLSv1.3` 不会报错,但也不会生效。 ### Apache [#apache] ```apache title="httpd.conf / vhost" SSLProtocol -all +TLSv1.2 +TLSv1.3 ``` `-all` 先全部关掉再逐个打开,比逐个 `-TLSv1` 排除更不容易漏 —— 后者在升级 Apache 后新增的协议版本会默认打开。 ### 密码套件要不要一起调 [#密码套件要不要一起调] 多数情况下不用。手写套件列表是最容易配错、也最容易过时的一处:抄一份两年前的「安全配置」,往往同时关掉了现代客户端需要的套件。需要调的时候用 Mozilla 的 intermediate 档配置生成,不要自己列。 改完照例先测语法再重载: ```bash nginx -t && nginx -s reload # 或 apachectl configtest && apachectl graceful ``` ## 谁会被挡在外面 [#compatibility] 这是唯一需要认真评估的部分。只支持 TLS 1.0/1.1 的客户端: | 客户端 | 情况 | | ------------------------ | --------------------- | | Android 4.4 及更早 | 系统栈不支持 TLS 1.2 | | IE 10 及更早(Windows 7) | 不支持;IE 11 支持但可能需要手动勾选 | | Windows XP / Vista | 系统层面就没有 TLS 1.2 | | Java 6、Java 7 | 支持但默认不启用,需要调用方改启动参数 | | .NET Framework 4.5 及更早 | 默认不用 TLS 1.2,需要调用方改代码 | | 很老的 curl / OpenSSL 0.9.8 | 不支持 | 对面向公众的网站来说,这几类加起来的占比现在已经可以忽略。真正的风险在**服务端到服务端的调用**:某个合作方的老系统、某台多年没动过的内部机器、某个用旧 Java 写的对接程序。这些不会出现在浏览器统计里,出问题时也不会有人来告诉你,只会静默失败。 所以顺序是:先在访问日志里确认还有没有 TLS 1.0/1.1 的连接,再改。nginx 可以把协议版本打进日志:在 `log_format` 里加上 `$ssl_protocol`,观察几天。 ```nginx title="nginx.conf" log_format tlsver '$remote_addr $ssl_protocol $ssl_cipher "$request"'; access_log /var/log/nginx/access.log tlsver; ``` ## 怎么确认真的关掉了 [#verify] 逐个版本单独建连,最直接: ```bash # 应该失败(handshake failure) openssl s_client -tls1 -connect example.com:443 -servername example.com 如果本机是 OpenSSL 3.x,前两条可能因为**本机**的安全等级就拒绝了,而不是因为服务器拒绝 —— 这两种失败长得很像。加上 `-cipher 'DEFAULT@SECLEVEL=0'` 可以把本机限制放开,让失败真正来自对端。 ### 为什么本站的检测只报「协商到的版本」 [#为什么本站的检测只报协商到的版本] 值得说清楚,因为它决定了这个结论该怎么用:一次握手只会协商出**一个**版本,而我们提供的是现代版本,所以正常情况下会协商到 TLS 1.2 或 1.3。也就是说 —— * 检测报出 `TLSv1` 或 `TLSv1.1`,说明服务器**最高**只支持到这个版本,问题确凿。 * 检测报出 `TLSv1.3`,只能说明它支持 1.3,**不能**说明它已经关掉了 1.0 —— 一台同时支持 1.0 到 1.3 的服务器在我们这里看起来是干净的。 要判断第二种情况,必须每个版本单独建一次连接,也就是上面那四条命令。我们没有把它做进默认检测,是因为那意味着每次检测多开四个连接、对被检测的服务器多四倍的握手成本,而这一项并不是导致访客打不开网站的故障。 # 证书不包含这个域名:ERR_CERT_COMMON_NAME_INVALID 的几种成因 (/docs/troubleshooting/hostname-mismatch) 这个错误的含义很窄:服务器发来的证书是**有效的**,只是上面没有你正在访问的这个域名。链是好的、没过期、CA 也受信任 —— 唯独名字不对。 同一个报错有好几种完全不同的成因,其中一种(拿到了同一台服务器上另一个站点的证书)光看错误信息看不出来。下面按从最常见到最少见的顺序排。 ## 先看清服务器到底发了哪张证书 [#inspect] 这一步能直接分出「证书签少了」和「拿错了证书」两大类: ```bash # 带 SNI(正常访问的样子) openssl s_client -connect example.com:443 -servername example.com /dev/null \ | openssl x509 -noout -subject -ext subjectAltName # 不带 SNI(看这台机器的默认站点是谁) openssl s_client -connect example.com:443 -noservername /dev/null \ | openssl x509 -noout -subject ``` `-noservername` 不能省。OpenSSL 1.1.1 及以上会自动按 `-connect` 的主机名填 SNI,所以不显式关掉的话,两条命令测的是同一件事。 对照结果: | 两条命令的结果 | 指向的成因 | | -------------- | --------------------------------- | | 一样,而且证书上没有你的域名 | 证书签少了名字 → 看下面第 1、2 节 | | 不一样 | SNI 路由没配对,你的请求落到了默认站点 → 第 3 节 | | 证书上有你的域名,但仍然报错 | 拿到的证书不是你以为的那张 → 第 4 节(CDN / 负载均衡) | ## 1. 泛域名不覆盖主域名 [#wildcard] 证书里只有 `*.example.com`,而访客访问的是 `example.com`。这是最常见的一种,因为它看起来完全不像证书问题 —— 所有子域名都正常,只有主域名坏。 详细解释和修法见[泛域名为什么不覆盖主域名](/guides/wildcard-does-not-cover-apex)。 ## 2. 少签了 www,或者少签了新加的域名 [#missing-san] 证书是 SAN 证书,能签多少个名字取决于签发时写了多少个。常见的漏签: * 只签了 `example.com`,没签 `www.example.com`(或者反过来)。 * 后来上线了新域名 / 换了品牌域名,DNS 指过来了,证书没重签。 * 多个域名指向同一个站点(主域名、短域名、旧域名),证书只签了其中一个。 修法是重新签一张包含全部域名的证书。一张证书装得下多个名字,不需要一个域名一张。 别指望用 301 跳转绕过去:TLS 握手在 HTTP 之前完成,握手时证书就已经被拒绝了,跳转规则根本执行不到。 ## 3. SNI 没配对,你拿到了别人的证书 [#sni] 一台服务器上跑多个 HTTPS 站点时,服务器靠握手里的 SNI 字段决定发哪张证书。如果没有任何一个站点的域名配置与请求的域名匹配,服务器不会报错 —— 它会发出**默认站点**的证书。于是访客看到的是一个完全无关的域名。 上面第二条命令返回的主题,就是这台机器的默认站点。如果那是另一个项目的域名,问题就在这里。逐项检查: * **`server_name` 写全了吗。** nginx 里请求域名必须命中某个 `server` 块的 `server_name`;命不中就落到 `default_server`。 * **新站点的配置真的被加载了吗。** `nginx -T` 打印出的是实际生效的完整配置,比 `ls sites-enabled/` 可靠 —— 忘了做软链、或者软链断了,都会让配置看起来存在而实际没加载。 * **监听的地址对不对。** `listen 443 ssl` 写在了另一个 IP 上、或者 IPv6 只配了一半(访客走 AAAA 记录进来,落到没配的那条监听上),都会绕开你以为会命中的那个块。 * **客户端是不是不发 SNI。** 极老的客户端(Windows XP 上的 IE、Java 6)不发 SNI,一定会拿到默认站点的证书。这种情况没法在服务器端修,只能给那个站点单独一个 IP。 ## 4. CDN、负载均衡、云上的证书绑定 [#edge] 接了 CDN 或负载均衡之后,访客握手的对象是边缘节点,不是你的源站。这时候有两张独立的证书,两处都可能出问题: * **边缘证书没绑,或者绑错了域名。** 加了新域名却只在 DNS 上指了过去,忘了在 CDN 控制台给这个域名绑证书 —— 边缘会拿默认证书应付,症状和上面的 SNI 问题一样。 * **源站证书的域名和回源 Host 不一致。** 回源时如果开了「校验源站证书」,源站证书里必须有回源使用的那个 Host 名字,而不是访客看到的域名。这两个经常不是同一个。 ## 5. 用 IP 或内网名访问 [#ip] 直接访问 `https://1.2.3.4` 或者 `https://内网机器名` 时报这个错,是正常的 —— 公开 CA 不给 IP 和内网名签发证书,证书里当然不会有它们。这不是故障,是用错了地址:用域名访问就好。 确实需要用 IP 校验源站时,用 `--resolve` 指定 IP 但仍以域名握手: ```bash curl -v --resolve example.com:443:1.2.3.4 https://example.com/ ``` ## 6. 只有 CN,没有 SAN [#cn-only] 很老的证书、或者自己用 `openssl req` 手工签的证书,可能只在 CN 字段里写了域名而没有 SAN 扩展。现在的浏览器**完全忽略 CN**(Chrome 自 58 版起),所以这种证书对所有域名都报不匹配。 重新签发时必须带上 SAN 扩展。任何公开 CA 现在签出来的证书都一定有 SAN,这种情况基本只出现在自签证书上。 # 电脑打得开、手机 App 报证书错误:证书链缺少中间证书 (/docs/troubleshooting/incomplete-chain) 如果你的网站在电脑浏览器上一切正常,但手机 App、`curl`、Java 或 Go 写的客户端访问时报证书错误,绝大多数情况下不是证书本身有问题,而是**服务器只发送了自己的证书,没有把中间证书一起发出去**。 修复通常只需要改一个文件路径:把服务器配置里的 `cert.pem` 换成 `fullchain.pem`,然后重载服务。下面解释为什么会出现「只有一部分人打不开」,以及怎么确认和修改。 ## 为什么只有一部分客户端报错 [#symptoms] 一张证书要被信任,客户端必须能从它一路验证到操作系统里预置的根证书。中间通常还有一张「中间证书」(intermediate CA),这张证书**必须由服务器在握手时一起发给客户端** —— 它不在任何人的信任库里。 服务器只发叶子证书时,不同客户端的表现不一样,这才是这个故障最难定位的地方: | 客户端 | 表现 | 原因 | | -------------------------- | ------- | ----------------------------------------- | | Chrome / Edge / Safari(桌面) | 多数情况下正常 | 会按证书里的 AIA 扩展主动下载缺失的中间证书,并缓存下来 | | Firefox | 多数情况下正常 | 不主动下载,但内置并缓存了常见的中间证书 | | `curl` / OpenSSL | 报错 | 不做 AIA 下载,缺一张就是缺一张 | | Java(HttpClient / OkHttp) | 报错 | 同上,且错误信息是 PKIX 相关,看不出是链的问题 | | Go / Python requests | 报错 | 同上 | | 手机 App 内的网络库 | 不一定 | 取决于走系统栈还是自带的 OpenSSL,同一个 App 在两个系统上表现可能不同 | 所以**不要用桌面浏览器判断这个问题**。你自己电脑上能打开,恰恰是这个故障最典型的样子。 对应的错误信息,如果你手上只有一条日志,可以按这个对照: | 报错来源 | 错误信息 | | ---------- | -------------------------------------------------------------------------------------------------------------- | | `curl` | `curl: (60) SSL certificate problem: unable to get local issuer certificate` | | OpenSSL | `verify error:num=20:unable to get local issuer certificate` 或 `num=21:unable to verify the first certificate` | | Java | `PKIX path building failed: unable to find valid certification path to requested target` | | Node.js | `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | | Python | `certificate verify failed: unable to get local issuer certificate` | | 微信 / 支付宝回调 | 多数只回一句「证书校验失败」,需要用下面的命令自查 | ## 怎么确认是这个问题 [#diagnose] 最快的办法是数一下服务器到底发了几张证书。把下面命令里的域名换成你自己的: ```bash openssl s_client -connect example.com:443 -servername example.com -showcerts /dev/null | grep -c "BEGIN CERTIFICATE" ``` * 结果是 **1** —— 服务器只发了自己的证书,就是本文说的问题。 * 结果是 **2 或更多** —— 链是完整的,报错另有原因,可以先跑一次[证书检测](/ssl-checker)看看结论。 想看得更清楚,去掉 `grep` 直接读输出。开头那段 `Certificate chain` 会按顺序列出服务器发来的每一张证书,`s:` 是主题、`i:` 是颁发者: ```bash openssl s_client -connect example.com:443 -servername example.com 手边没有 `openssl` 的话,本站的[证书检测](/ssl-checker)会直接给出结论 —— 它读的是验证器返回的错误码,判定方式和上面的命令一致。 ## 修复:用 fullchain,不要用 cert [#fix] 证书颁发机构给你的文件里,通常同时有「只有叶子证书」和「叶子 + 中间证书」两个版本。用错的那个就是这个故障的全部原因。以 Let's Encrypt / certbot 的输出为例: | 文件 | 内容 | 该不该用 | | --------------- | ----------- | ------------ | | `cert.pem` | 只有你的证书 | ❌ 用了就是本文的故障 | | `chain.pem` | 只有中间证书 | ❌ 单独用不行 | | `fullchain.pem` | 你的证书 + 中间证书 | ✅ 这个 | | `privkey.pem` | 私钥 | ✅ 配在 key 那一项 | ### nginx [#nginx] ```nginx title="nginx.conf" ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ``` `ssl_trusted_certificate` **不是**用来发送证书链的 —— 它只用于 OCSP stapling 的验证,配在那里不会让服务器多发一张证书。这是这个问题最常见的一种「我明明配了中间证书」。 ### Apache [#apache] ```apache title="httpd.conf / vhost" # Apache 2.4.8 及以上:直接给合并好的文件 SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem # 2.4.8 以下:中间证书要单独指定 # SSLCertificateFile .../cert.pem # SSLCertificateChainFile .../chain.pem ``` ### Caddy / Traefik [#caddy--traefik] 自动申请证书时不会出现这个问题。手工指定证书文件的话,同样要给合并后的 `fullchain.pem`。 ### HAProxy [#haproxy] `crt` 指向的那一个文件里要依次放:叶子证书、中间证书、私钥。顺序不能颠倒。 ### Java / Tomcat [#java--tomcat] 证书导入 keystore 时必须带整条链。用 PKCS#12 中转最稳妥: ```bash openssl pkcs12 -export \ -in fullchain.pem -inkey privkey.pem \ -out keystore.p12 -name tomcat ``` ### IIS / Windows [#iis--windows] 把中间证书导入本机的「中间证书颁发机构」存储(不是「受信任的根证书颁发机构」),IIS 会自己在握手时带上它。 ### CDN、负载均衡、云上的证书管理 [#cdn负载均衡云上的证书管理] 证书上传到 CDN 或 SLB 控制台时,「证书内容」那个输入框里同样要粘贴叶子 + 中间两段,不是只有第一段。改完源站却忘了改边缘,是这个故障最常见的复发方式。 ## 改完之后要重载服务 [#reload] 证书是在启动或重载时读进内存的,换掉文件本身不会生效。改完先测语法,再重载: ```bash nginx -t && nginx -s reload # nginx apachectl configtest && apachectl graceful # Apache ``` 然后用上面那条 `grep -c` 再数一次。返回 2 或更多,就修好了。 如果你用 certbot 自动续期,顺便确认一下 renew 的 `--deploy-hook` 里有重载命令。否则续期会成功、而线上还在用旧证书,直到下一次手工重启。 ## 为什么有些检测工具说「没问题」 [#false-negative] 两个原因,都值得知道,因为它们会让你以为已经修好了: 1. **工具用的客户端会自己补全。** 任何基于桌面浏览器内核、或者开启了 AIA 下载的检测器,都会把缺失的中间证书下载回来,然后报告一切正常 —— 它替你的访客做了访客做不到的事。 2. **工具在数链的长度。** 验证成功之后,客户端会把自己信任库里的根证书补进链里。所以一台配置正确、只发两张证书的服务器,验证出来的链长度是 3。反过来,靠「链里有几张」判断缺不缺中间证书,会同时产生误报和漏报。 本站的检测器只认验证器返回的错误码(`UNABLE_TO_VERIFY_LEAF_SIGNATURE`),不数链长度,也不替服务器补下载。所以它给出的结论和你的访客实际遇到的一致 —— 这也是为什么结果页上的「验证链」一栏会特意注明,那个长度不等于服务器发送的证书数量。 想验证你手上的工具靠不靠得住,可以拿 `incomplete-chain.badssl.com` 试一下:一个诚实的检测器必须报错。 # OCSP stapling:开了能快一点,不开也不算故障 (/docs/troubleshooting/ocsp-stapling) **这不是故障。** 证书是好的,访客也能正常访问,只是每个新访客的浏览器可能要额外去 CA 那里问一次「这张证书被吊销了吗」,多一次网络往返。 OCSP stapling 就是让**服务器代劳**:服务器定期去问好,把带签名的答复随握手一起发给访客。省掉一次往返,顺带也不让 CA 看到谁在访问你的站点。 ## 开启方式 [#enable] ```nginx title="nginx" ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/ssl/example.com/fullchain.pem; resolver 1.1.1.1 8.8.8.8 valid=300s; # 服务器需要能解析 CA 的 OCSP 域名 ``` ```apache title="Apache" SSLUseStapling on SSLStaplingCache shmcb:/var/run/ocsp(128000) ``` nginx 少了 `resolver` 那一行是最常见的失败原因:配置写了、错误日志里却一片 `ssl_stapling ignored`,因为服务器解析不了 CA 的域名。 ## 确认是否生效 [#verify] ```bash title="看握手里有没有带上 OCSP 响应" echo | openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null \ | grep -A 1 'OCSP Response Status' ``` 改完配置后**第一次握手通常还是没有**——服务器要先自己去取一次并缓存下来。等几十秒再试一次。 ## 什么时候可以不管它 [#skip] * **在 CDN 后面**:边缘节点自己会处理,源站开不开都不影响访客。 * **内网服务**:访问方少、延迟不敏感,收益很小。 * **服务器不能出网**:取不到 OCSP 响应,强行配置只会在日志里刷警告。 # 证书被吊销,或者链验证没通过 (/docs/troubleshooting/revoked-or-unverified) **证书被吊销**是 CA 主动作废了这张证书。它和过期不一样:过期是到点了,吊销是这张证书出了问题,通常意味着私钥可能已经不安全。所以处理顺序是**先查私钥,再换证书**——直接拿同一把私钥重签,可能把问题原样带过去。 **链验证没通过**但错误码不属于常见几种时,说明证书本身有结构性问题,需要看原始错误码定位。 ## 被吊销:先确认原因 [#revoked-why] * **私钥泄露或疑似泄露**——最严重的一种。私钥进过代码仓库、备份被拿走、服务器被入侵,都属于这一类。 * **你自己申请的吊销**:换服务器、域名转让、误签之后主动作废。 * **CA 侧的合规吊销**:签发时的域名验证被判定无效、CA 发现批量签发问题、行业规则变化导致批量吊销。 * **域名控制权变更**:域名过户后原证书被作废。 如果不能排除私钥泄露,**新证书必须用新生成的私钥**。用原来那把重签,等于把可能已经泄露的钥匙继续用下去。 ## 处理顺序 [#steps] 1. 问清吊销原因——CA 的控制台或邮件里通常写了。 2. 如果与私钥有关:**生成新的私钥**,不要复用。 3. 用新私钥重新签发,部署,重载服务。 4. 排查私钥是怎么泄露的:仓库历史、备份、日志、镜像层里有没有留下副本。 5. 确认没有别处还在用这张被吊销的证书——负载均衡、CDN 边缘节点、其他机器。 浏览器对吊销的处理并不一致:有的会拦截,有的因为吊销状态查询超时而放行。所以「浏览器还能打开」不能作为「没事」的依据。 ## 验证没通过、错误码不常见 [#unverified] 这说明 OpenSSL 拒绝了这条链,但原因不属于过期、主机名不匹配、链不完整、自签名这几类常见情况。直接看原始错误码: ```bash title="拿到具体的验证错误" openssl s_client -connect example.com:443 -servername example.com &1 \ | grep -E 'verify error|Verify return code' ``` * `CERT_SIGNATURE_FAILURE` — 签名对不上,证书文件可能损坏或被截断。 * `CERT_CHAIN_TOO_LONG` — 链层数超限,通常是链拼接时重复了。 * `INVALID_CA` — 链中某张证书没有 CA 标记,顺序拼错了。 * `UNSUPPORTED_CERTIFICATE_PURPOSE` — 证书用途不含服务器认证,签错了类型。 # 自签名证书:什么时候没问题,什么时候必须换 (/docs/troubleshooting/self-signed-certificate) 自签名证书就是**自己签给自己**的证书:颁发者和使用者是同一个,没有任何第三方为它背书。加密是正常工作的,浏览器警告的不是「不安全」,而是「无法确认对面是谁」。 所以关键问题只有一个:**这个服务是给谁用的**。内网自用没问题;公网访客能访问到,就必须换。 ## 内网服务:可以继续用 [#internal] 只在内网访问的管理后台、数据库控制台、开发环境,自签名是常见且合理的选择。要消掉警告,把这张证书(或签它的私有根)装进访问方的信任库即可。 * **更好的做法是自建一个私有 CA**,再用它签发各服务的证书。这样只需要在每台机器上安装一次根证书,以后换服务证书不用重新安装。 * 机器多的话,把根证书下发做成配置管理的一部分,而不是让每个人手工点「继续访问」。 * **不要让人习惯性点过警告**——那会让真正的中间人攻击也被点过去。 ## 公网服务:必须换 [#public] 公网访客的浏览器不会信任它,而且**没有任何服务器配置能改变这一点**——信任来自客户端的信任库。访客会看到全屏拦截页,绝大多数人会直接离开。 App 和接口调用的失败更彻底:多数 HTTP 客户端默认校验证书,自签名会直接抛异常,而不是像浏览器那样给一个「继续访问」的入口。 换成公开 CA 签发的证书即可。本站基于 Let's Encrypt 的签发是免费的,支持泛域名,加一条 DNS TXT 记录完成验证,几分钟内签发。 ## 为什么会意外出现自签名证书 [#accidental] * **默认证书没换掉**:很多软件安装时会生成一张自签名证书顶上,忘了替换。 * **证书路径写错**:配置指向了一个不存在的文件,服务回退到内置的默认证书。 * **新加的域名没配**:nginx 匹配不到对应的 server 块,用了默认站点的证书。 * **证书文件被覆盖**:部署脚本把测试用的自签名证书发到了生产。 # 证书链追溯不到受信任的根:新根交叉签名与私有 CA (/docs/troubleshooting/untrusted-root) 这条结论的意思很具体:**服务器发来的链,最上面那张证书的颁发者,不在验证方的信任库里**。链断在了半空中,而不是「这家 CA 有问题」。 两种完全不同的成因,修法也完全不同。绝大多数情况是第一种:**链没发全**。CA 启用新根时会用旧根交叉签名一份,少了这一张,尚未收录新根的客户端就接不上。第二种才是真的由私有 CA 签发。 ## 先分清是哪一种 [#which] 看颁发者是不是一家公开 CA。如果证书是 Let's Encrypt、DigiCert、Sectigo、Amazon 这类签的,那就是第一种——链没发全,不是 CA 不受信任。 ```bash title="看服务器实际发了几张证书" echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE' ``` **2 张**通常就是问题所在:服务器只发了「证书 + 中间证书」。**3 张**说明它还发了交叉签名的那一张,这是新根切换期间正确的做法。 别用浏览器判断。浏览器和操作系统的信任库更新得快,新根往往已经收录,所以浏览器一切正常——而旧安卓、旧 Java、嵌入式设备、部分 CI 镜像会失败。这正是这类问题最难被发现的原因。 ## 成因一:新根切换期,链少发了交叉签名证书 [#cross-sign] CA 启用一个新根时,新根自己有一份自签名版本,同时会用**已经被广泛信任的老根**再签一份。这份「交叉签名」的副本让还没收录新根的客户端也能一路验到老根。 | 服务器发送 | 有新根的客户端 | 没有新根的客户端 | | ----------------- | ------- | --------------- | | 证书 + 中间证书 | 通过 | **失败**——链断在中间证书 | | 证书 + 中间证书 + 交叉签名根 | 通过 | 通过——经交叉签名接到老根 | 所以链上会出现一张名字里带 Root、却被标成「中间证书」的东西。这不是标错:那一份**不是自签名的**,它的颁发者是老根,它在这条链里承担的正是中间证书的角色。 ### 怎么修 [#怎么修] 1. 用 CA 给的**完整链文件**部署,通常叫 `fullchain.pem`,而不是只有一张的 `cert.pem`。 2. nginx 的 `ssl_certificate` 就应该指向 fullchain 文件——它期望「证书在前、中间证书在后」拼在同一个文件里。 3. Apache 用 `SSLCertificateChainFile`,或在新版本里同样把完整链放进 `SSLCertificateFile`。 4. 改完重载服务,再用上面那条 openssl 命令确认张数变了。 ```nginx title="nginx" ssl_certificate /etc/ssl/example.com/fullchain.pem; # 不是 cert.pem ssl_certificate_key /etc/ssl/example.com/privkey.pem; ``` ## 成因二:确实由私有 CA 签发 [#private-ca] 如果颁发者是公司内部的 CA、某个自建的根,或者名字你完全没见过,那就是第二种。这时公网访客的浏览器确实不会信任它,而且**没有任何服务器配置能改变这一点**——信任来自客户端的信任库,不来自服务器。 * **内网服务**:把私有根证书装进内网机器的信任库,这是私有 CA 的正常用法。 * **公网服务**:必须换成公开 CA 签发的证书。本站基于 Let's Encrypt 的签发是免费的。 * **两者混用**:同一个域名在内外网走不同的证书是可行的,但要确认公网那一侧用的是公开 CA。 # RSA 密钥长度不足:为什么现在才被提醒 (/docs/troubleshooting/weak-key) **2048 位是 RSA 的现行下限**,低于它的证书公开 CA 已经不再签发。你还能看到一张 1024 位的证书,通常意味着它是很多年前签的、私钥一直没换过——续期只换证书不换密钥,长度就会一直带着。 修法只有一个:**用新的、更长的密钥重新签发**。这件事免费,而且顺手可以换成 ECDSA。 ## 为什么现在才提醒 [#why-now] 浏览器对已经签发的旧证书大多仍然接受,不会拦截。所以它不会以故障的形式暴露出来——直到某天某个客户端收紧策略,或者密钥真的被破解。这属于「现在没坏,但不该继续」的一类。 | 密钥 | 状态 | | --------------- | ------------------------- | | RSA 1024 | **已不安全**,公开 CA 早已停止签发 | | RSA 2048 | 当前主流,足够用 | | RSA 3072 / 4096 | 更保守,握手开销更大 | | ECDSA P-256 | 安全强度约等于 RSA 3072,密钥和握手都更小 | ## 重新签发时怎么选 [#choose] * **没有特殊要求就用 ECDSA P-256**:更短的密钥、更快的握手、更小的证书。现代客户端全面支持。 * **需要兼容很老的客户端**(十年前的安卓、老 Java)就用 **RSA 2048**。 * **不建议直接上 RSA 4096**:安全收益有限,而每次握手的计算开销明显更高,高并发下能感觉到。 关键是**生成新的私钥**,不是拿旧私钥重签。密钥长度是私钥的属性,不换私钥,签多少次都还是原来的长度。 ```bash title="生成新私钥" openssl ecparam -genkey -name prime256v1 -out privkey.pem # ECDSA P-256 openssl genrsa -out privkey.pem 2048 # RSA 2048 ``` # 泛域名证书为什么不覆盖主域名 (/docs/troubleshooting/wildcard-does-not-cover-apex) `*.example.com` 覆盖 `www.example.com`、`api.example.com`、`shop.example.com`,但**不覆盖 `example.com` 本身**。这不是某家 CA 的限制,是通配符证书的匹配规则(RFC 6125):通配符只能出现在最左边的那一段,而且只匹配**一段**。 所以典型症状是:所有子域名都好,唯独直接访问主域名报证书错误。看起来像 DNS 出了问题,实际是证书少签了一个名字。解决办法是把 `example.com` 和 `*.example.com` 一起签进同一张证书。 ## 匹配规则:一颗星等于一段 [#rules] | 证书里写的 | 访问的域名 | 是否匹配 | | ----------------- | ----------------- | ------------- | | `*.example.com` | `www.example.com` | ✅ | | `*.example.com` | `api.example.com` | ✅ | | `*.example.com` | `example.com` | ❌ 主域名不是它的子域名 | | `*.example.com` | `a.b.example.com` | ❌ 一颗星只匹配一段 | | `*.b.example.com` | `a.b.example.com` | ✅ | | `www.*.com` | 任何域名 | ❌ 通配符只能在最左段 | | `*.com` | 任何域名 | ❌ 公共后缀上不签发通配符 | 顺带说一个容易被忽略的点:现在的浏览器**只看 SAN(Subject Alternative Name)列表**,证书里的 CN 字段自 Chrome 58 起就被忽略了。所以「CN 写的是 example.com,应该能用」这个判断不成立 —— 域名必须出现在 SAN 里。 ## 为什么 301 跳转救不了 [#redirect] 很多人第一反应是「我在服务器上把 example.com 跳到 www 就行了」。这条路走不通,原因是顺序: 1. 浏览器先和 `example.com` 建立 TLS 连接。 2. 服务器发来一张不包含 `example.com` 的证书。 3. **浏览器在这里就中止了**,并显示证书错误。 4. 你写的那条 301 在 HTTP 层,属于第 4 步 —— 永远轮不到它执行。 换句话说,只要你希望访客能输入 `example.com` 打开你的网站(哪怕只是为了跳转到 www),这张证书就必须包含 `example.com`。 ## 怎么看自己的证书覆盖了哪些域名 [#inspect] ```bash openssl s_client -connect example.com:443 -servername example.com /dev/null \ | openssl x509 -noout -ext subjectAltName # X509v3 Subject Alternative Name: # DNS:*.example.com, DNS:example.com ``` 上面这个输出是正确的样子 —— 两个名字都在。如果只有 `DNS:*.example.com` 一个,那就是本文说的情况。 本站的[证书检测](/ssl-checker)会列出完整的 SAN 列表,并在只有泛域名、缺主域名时单独指出来。它只针对你查的那个域名报这个问题,不会把同一张证书上别人家的域名也算进来。 ## 正确的签法 [#fix] 把两个名字签进同一张证书。不需要两张证书,也不需要额外费用: ```bash # 一张证书,两个 SAN example.com *.example.com ``` 在本站[免费签发](/free-cert)时这是默认行为 —— 勾选泛域名,基础域名会一起签进去,正是因为这个坑太常见。用 certbot 的话,两个名字都要写出来: ```bash certbot certonly --manual --preferred-challenges dns \ -d example.com -d '*.example.com' ``` 泛域名证书只能用 DNS-01 验证,HTTP 文件验证不被接受 —— 这是 ACME 协议的规定,不是我们的限制。两个名字会各自需要一条 TXT 记录,记录名相同(`_acme-challenge.example.com`),值不同,两条要同时存在。 ### 多级子域名怎么办 [#多级子域名怎么办] `a.b.example.com` 需要 `*.b.example.com`,`*.example.com` 覆盖不到它。层级多的时候,把每一层的通配符都签进同一张证书: ```bash example.com *.example.com *.b.example.com ```