证书这活儿: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 证书的核心是三样东西:

  1. 公钥:给客户端用来加密通信或验证签名
  2. 身份信息:域名、组织名、有效期等
  3. 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。

格式互转速查

工作中最常见的转换需求,opensslkeytool 基本能搞定:

# 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

转格式的时候最容易出的两个坑:

  1. 忘了带中间证书:只导了 leaf cert,没带 intermediate CA,客户端验不过信任链
  2. 密码搞丢了: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 证书
  • 工具:opensslmkcert(本地开发推荐)、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 证书签发流程

常用 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

cert-manager 架构

一个最小可用配置:

# 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 国际许可协议进行许可。