证书这活儿:PEM/JKS/P12 怎么选、免费证书哪家强、自动轮换怎么搞
Posted on 二 24 3月 2026 in Journal
| Abstract | 证书这活儿:PEM/JKS/P12 怎么选、免费证书哪家强、自动轮换怎么搞 |
|---|---|
| Authors | Walter Fan |
| Category | learning note |
| Version | v1.0 |
| Updated | 2026-03-24 |
| License | CC-BY-NC-ND 4.0 |
短大纲
- 从凌晨被证书过期告警叫醒讲起
- PEM / JKS / PKCS#12 三种格式的本质差异、适用场景与互转
- 免费证书:Let's Encrypt 之外还有谁,边界在哪
- 自动轮换的两条路:ACME 客户端 vs Kubernetes cert-manager
- 一张可直接抄的检查清单
凌晨三点被 PagerDuty 叫醒,打开笔记本,满屏 NET::ERR_CERT_DATE_INVALID。原因很快查清了:某个内部服务的 TLS 证书三个月前签发,昨天到期,没人续。
这事不稀奇。做后端做久了,谁没被证书坑过?该配的忘了配,该续的忘了续,PEM 和 JKS 搞混了格式塞错地方,或者一张自签证书在测试环境待了两年,有一天被人复制到了生产。
证书管理不难,难的是持续正确地做。这篇文章就聊三件事:格式怎么选、免费证书怎么拿、自动轮换怎么搞。
先搞清楚:证书到底是个什么东西
不展开密码学原理,只说关键的几句。
一张 TLS/SSL 证书的核心是三样东西:
- 公钥:给客户端用来加密通信或验证签名
- 身份信息:域名、组织名、有效期等
- CA 签名:证书颁发机构(CA)用自己的私钥对上面两项做的签名,证明"这张证书是我认过的"
客户端拿到证书后做两件事:检查 CA 签名是否可信(信任链)、检查证书是否过期或被吊销。通过了,才建立加密连接。
整个信任体系像一条链:
Root CA → Intermediate CA → Leaf Certificate (你的服务)
根 CA 证书预装在操作系统和浏览器里,中间 CA 由根 CA 签发,你的证书由中间 CA 签发。链条断了,浏览器就报红。
PEM / JKS / PKCS#12:三种格式到底差在哪
这是我见过最容易搞混的地方。很多人被 .pem、.crt、.key、.jks、.p12、.pfx 这些后缀搞得头大,其实理清楚只需要记住三个"容器"。
PEM:文本格式,Unix 世界的通用语
PEM 是 Privacy Enhanced Mail 的缩写,但跟邮件早没关系了。它就是 Base64 编码 + 头尾标记:
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhki...
-----END CERTIFICATE-----
特点:
- 纯文本,cat 就能看,diff 能比,git 能追踪
- 一个文件可以串联多张证书(certificate chain)
- 私钥单独存一个
.key文件(也是 PEM 格式) - Nginx、Apache、HAProxy、Go、Python、Node.js 原生支持
PEM 是最"透明"的格式。你几乎可以用记事本编辑它——虽然没人推荐这么干。
JKS:Java 生态的老朋友
JKS 是 Java KeyStore,Java 自己搞的一种二进制容器格式。
特点:
- 二进制,不能直接看内容,需要
keytool命令操作 - 可以在一个文件里同时存私钥、证书、信任链
- 用密码保护整个 keystore 以及里面每个条目
- 主要用于 Java 应用:Tomcat、Spring Boot、Kafka、Elasticsearch
JKS 的问题是只有 Java 生态认它。你想让 Nginx 读 JKS?不行。想用 Python 脚本解析?得先转格式。而且 JKS 用的加密算法比较老(早期版本用 SHA1),Oracle 从 Java 9 开始推荐用 PKCS#12 替代。
PKCS#12 (.p12 / .pfx):跨平台的二进制容器
PKCS#12 是一个 RSA 实验室定义的标准,文件后缀 .p12 或 .pfx(Windows 偏好叫 .pfx,内容一样)。
特点:
- 二进制,密码保护
- 能把私钥 + 证书 + 信任链全部打包在一起
- 跨平台:Java、.NET、Go、浏览器都能读
- 是 Java 9+ 默认推荐的 keystore 格式
PKCS#12 像一个"加密的压缩包",把你需要的东西都塞进一个文件。搬运方便,但调试不如 PEM 透明。
三者对比
| 特性 | PEM | JKS | PKCS#12 (.p12/.pfx) |
|---|---|---|---|
| 编码 | Base64 文本 | 二进制 | 二进制 |
| 可读性 | cat 可看 | 需要 keytool | 需要 openssl/keytool |
| 私钥+证书同文件 | 通常分开 | 可以 | 可以 |
| 密码保护 | 私钥可选加密 | 有 | 有 |
| 生态 | Nginx/Apache/Go/Python/Node | Java | 跨平台通用 |
| 信任链 | 文件内串联 | store 内多条目 | 包内多条目 |
| 标准化 | 事实标准 | Java 私有 | PKCS#12 (RFC 7292) |
一句话选型建议:Web 服务器和云原生场景用 PEM;纯 Java 栈用 PKCS#12(别再用 JKS 了);需要把私钥和证书打包给人的时候用 PKCS#12。
格式互转速查
工作中最常见的转换需求,openssl 和 keytool 基本能搞定:
# PEM → PKCS#12
openssl pkcs12 -export \
-in cert.pem -inkey key.pem -certfile chain.pem \
-out bundle.p12 -name myalias
# PKCS#12 → PEM
openssl pkcs12 -in bundle.p12 -out all.pem -nodes
# 只导出证书(不含私钥)
openssl pkcs12 -in bundle.p12 -nokeys -out cert.pem
# 只导出私钥
openssl pkcs12 -in bundle.p12 -nocerts -nodes -out key.pem
# PKCS#12 → JKS (Java 9+)
keytool -importkeystore \
-srckeystore bundle.p12 -srcstoretype PKCS12 \
-destkeystore app.jks -deststoretype JKS
# JKS → PKCS#12
keytool -importkeystore \
-srckeystore app.jks -srcstoretype JKS \
-destkeystore bundle.p12 -deststoretype PKCS12
转格式的时候最容易出的两个坑:
- 忘了带中间证书:只导了 leaf cert,没带 intermediate CA,客户端验不过信任链
- 密码搞丢了:PKCS#12 和 JKS 都有密码保护,转来转去密码对不上就完了。用密码管理器存好,别写在代码里
免费证书:不只是 Let's Encrypt
说到免费证书,大家第一反应是 Let's Encrypt。它确实改变了整个行业,但不是唯一选项。
Let's Encrypt
- 全球最大的免费 CA,ISRG 运营
- 支持 DV(Domain Validation)证书
- 有效期 90 天,逼着你做自动续期(这其实是好事)
- 通过 ACME 协议自动签发和续期
- 支持通配符证书(需要 DNS-01 验证)
- 速率限制:每个注册域名每周 50 张证书
ZeroSSL
- 免费 DV 证书,也支持 ACME
- 有 Web UI,适合不熟悉命令行的人
- 免费层每月 3 张 90 天证书
- 付费层有更多额度和 REST API
Google Trust Services
- Google 自己的公共 CA
- 支持 ACME 协议
- 证书有效期和速率限制政策较宽松
- 适合 GCP 生态里的服务
自签证书(Self-Signed)
- 不是"免费 CA"但确实免费
- 只适合开发、测试、内部服务
- 客户端不会自动信任,需要手动导入 CA 证书
- 工具:
openssl、mkcert(本地开发推荐)、cfssl
选型边界
| 场景 | 推荐 |
|---|---|
| 公网 Web 服务 | Let's Encrypt / ZeroSSL |
| 内部微服务 mTLS | 自签 CA 或 SPIFFE/SPIRE |
| 企业级需求(OV/EV证书) | 商业 CA(DigiCert、Sectigo 等) |
| 本地开发 | mkcert |
| Kubernetes Ingress | cert-manager + Let's Encrypt |
免费证书有一个共同限制:只做 DV(域名验证)。需要 OV(组织验证)或 EV(扩展验证)证书的,还是得找商业 CA 付钱。不过老实说,对绝大多数服务来说,DV 够了。
自动轮换:别再靠日历提醒了
证书过期是最蠢但最常见的生产事故之一。手工续期有两个问题:人会忘,流程会断。
自动轮换有两条主流路径。
路径一:ACME 客户端
ACME(Automatic Certificate Management Environment)是 Let's Encrypt 推广的标准协议(RFC 8555)。流程大致是:
@startuml
!theme plain
skinparam backgroundColor #FEFEFE
skinparam defaultFontSize 12
participant "ACME Client\n(certbot/acme.sh)" as C
participant "ACME Server\n(Let's Encrypt)" as S
participant "Web Server\n(Nginx)" as W
C -> S : 1. 请求签发 example.com
S --> C : 2. 返回验证挑战\n(HTTP-01 / DNS-01)
C -> W : 3. 放置验证文件\n/.well-known/acme-challenge/xxx
S -> W : 4. 回调验证
S --> C : 5. 验证通过,签发证书
C -> W : 6. 部署新证书,reload
note over C : 定时任务每天检查\n到期前 30 天自动续期
@enduml

常用 ACME 客户端:
- certbot:Let's Encrypt 官方推荐,Python 写的,插件丰富
- acme.sh:纯 Shell 脚本,零依赖,支持几十种 DNS 提供商
- lego:Go 写的,单二进制,适合容器环境
一个 acme.sh 的典型配置:
# 首次签发(DNS 验证,支持通配符)
acme.sh --issue --dns dns_cf -d example.com -d "*.example.com"
# 自动部署到 Nginx
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/key.pem \
--fullchain-file /etc/nginx/ssl/fullchain.pem \
--reloadcmd "systemctl reload nginx"
# acme.sh 会自动在 crontab 里加续期任务
路径二:Kubernetes cert-manager
如果你的服务跑在 Kubernetes 里,cert-manager 是事实标准。
@startuml
!theme plain
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
skinparam defaultFontSize 12
package "Kubernetes Cluster" {
[cert-manager\nController] as CM #LightGreen
[Issuer / ClusterIssuer\n(ACME config)] as ISS #LightYellow
[Certificate CR] as CERT #LightBlue
[Secret\n(tls.crt + tls.key)] as SEC #Plum
[Ingress / Gateway] as ING #LightSkyBlue
CERT --> CM : watch
CM --> ISS : 读取签发配置
CM --> SEC : 写入证书
ING --> SEC : 引用 TLS Secret
}
cloud "Let's Encrypt\nACME Server" as LE
CM --> LE : ACME 协议\n签发/续期
note bottom of CM
自动监控证书到期
提前 30 天续期
更新 Secret 后
Ingress 自动生效
end note
@enduml

一个最小可用配置:
# ClusterIssuer: 全集群可用的 Let's Encrypt 签发器
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
---
# Certificate: 声明式证书,cert-manager 自动签发和续期
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com-tls
namespace: default
spec:
secretName: example-com-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.com
renewBefore: 720h # 到期前 30 天续期
cert-manager 的好处是声明式:你只管定义"我要一张什么证书",它负责签发、续期、更新 Secret。Ingress Controller 监听到 Secret 变化,自动重载。整条链路不需要人介入。
两条路径怎么选
| 场景 | 推荐 |
|---|---|
| 传统 VM 上的 Nginx/Apache | certbot 或 acme.sh |
| Docker 单机部署 | acme.sh + cron |
| Kubernetes 集群 | cert-manager |
| 多云、混合部署 | acme.sh(通用性最好) |
| 内部 CA 签发 | cert-manager + 自定义 Issuer 或 Vault PKI |
几个实操层面容易踩的坑
坑一:只部署了 leaf cert,没带中间证书
浏览器可能缓存了中间 CA 所以能打开,但 curl、SDK、移动端就报错。解决办法:部署 fullchain 而不是单张证书。用 openssl s_client 验证:
openssl s_client -connect example.com:443 -showcerts
看输出里有没有完整的证书链。
坑二:证书和私钥不匹配
换证书的时候用了旧的私钥,或者反过来。快速检查:
# 两个 md5 必须一致
openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa -noout -modulus -in key.pem | openssl md5
坑三:Let's Encrypt 速率限制撞了
测试时反复签发,把每周 50 张的额度用完了。生产签不了。测试环境永远用 staging endpoint:
https://acme-staging-v02.api.letsencrypt.org/directory
坑四:自动续期配了但没验证
cron job 加了,从来没跑过。或者跑了但 DNS 验证失败,续期静默失败,直到证书过期才发现。
建议:监控证书剩余天数。Prometheus 有 ssl_certificate_expiry_time_seconds 这个 metric,Blackbox Exporter 也能探测。低于 14 天就报警。
坑五:内部服务用公共 CA 证书
内部 api.internal.company.com 也去 Let's Encrypt 签一张?能行,但暴露了内部域名结构(CT Log 里全是公开的)。内部服务建议自建 CA 或者用 SPIFFE/SPIRE 做 mTLS。
证书管理的生命周期
把上面的点串起来,证书管理不是"签一张就完了",而是一个持续循环:
@startuml
!theme plain
skinparam backgroundColor #FEFEFE
skinparam activityFontSize 12
start
:评估需求\n公网/内部? DV/OV/EV?;
:选择 CA\n免费(Let's Encrypt) / 商业 / 自建;
:选择格式\nPEM(Web) / P12(Java/跨平台) / JKS(旧Java);
:签发证书\n手动 or ACME 自动化;
:部署证书\n配置 Web Server 或 K8s Secret;
:监控到期\nPrometheus / Blackbox Exporter;
if (到期前 30 天?) then (yes)
:自动续期\ncert-manager / acme.sh / certbot;
:部署新证书\nreload / Secret 更新;
else (no)
:继续监控;
endif
:审计与合规\nCT Log / 吊销检查 / 密钥轮换;
stop
@enduml

明天就能做的检查清单
- [ ] 盘点所有 TLS 证书:域名、格式、到期时间、存放位置
- [ ] 确认每个服务部署的是 fullchain 而不是单张 leaf cert
- [ ] 为公网服务配置 ACME 自动续期(certbot/acme.sh/cert-manager)
- [ ] 测试环境用 staging endpoint,别消耗生产速率限制
- [ ] 新 Java 项目用 PKCS#12 替代 JKS
- [ ] 监控证书剩余天数,低于 14 天触发告警
- [ ] 内部服务用自建 CA 或 mTLS,别把内部域名暴露到 CT Log
- [ ] 私钥文件权限收紧到
600,不要提交到 Git - [ ] 做一次"证书突然过期"的演练,看恢复流程要多久
总结
证书管理不是什么高深的活儿,但它有个讨厌的特点:平时不出事,出事就是凌晨三点。 大部分证书事故的根因都不是技术不行,而是流程断了——该续的没续,该监控的没监控,该自动化的还在靠人。
格式上,PEM 最透明,PKCS#12 最通用,JKS 正在退场。免费证书领域,Let's Encrypt 已经是基础设施级别的存在,90 天有效期逼着你做自动化,这是件好事。自动轮换,传统环境用 acme.sh 或 certbot,Kubernetes 用 cert-manager,都是成熟方案。
一句话:证书管理的最高境界,是你忘了它的存在。 因为该自动的都自动了,该告警的都告警了,你只需要在续签成功的日志里偶尔看到一行 Certificate renewed successfully,然后继续睡觉。
@startmindmap
!theme plain
skinparam backgroundColor #FEFEFE
* 证书管理最佳实践
** 格式选型
*** PEM: 文本, Web/云原生
*** PKCS#12: 二进制, 跨平台
*** JKS: Java 遗留, 建议迁移
** 免费证书
*** Let's Encrypt (90天, ACME)
*** ZeroSSL (Web UI)
*** Google Trust Services
*** 自签 (内部/开发)
** 自动轮换
*** ACME 客户端
**** certbot
**** acme.sh
**** lego
*** Kubernetes
**** cert-manager
**** ClusterIssuer + Certificate CR
** 常见坑
*** 缺中间证书
*** 证书私钥不匹配
*** 速率限制
*** 续期静默失败
*** 内部域名暴露 CT Log
** 监控告警
*** 剩余天数 < 14 天
*** Prometheus + Blackbox
*** 续期失败告警
@endmindmap

扩展阅读
- Let's Encrypt 官方文档: https://letsencrypt.org/docs/
- ACME 协议 RFC 8555: https://datatracker.ietf.org/doc/html/rfc8555
- cert-manager 文档: https://cert-manager.io/docs/
- acme.sh GitHub: https://github.com/acmesh-official/acme.sh
- mkcert - 本地开发零配置 HTTPS: https://github.com/FiloSottile/mkcert
- PKCS#12 RFC 7292: https://datatracker.ietf.org/doc/html/rfc7292
- SSL Labs 服务器测试: https://www.ssllabs.com/ssltest/
- ZeroSSL: https://zerossl.com/
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。