SSL / 证书
访问 https://api.example.com/orders 时,客户端需要确认对端有权代表 api.example.com,随后在加密连接中发送订单请求。证书把这个名称与一把公钥关联起来;服务端持有对应私钥,客户端依据自己的信任配置验证这份关联。
https://api.example.com/orders
├─ https HTTP 使用 TLS 保护通信
├─ api.example.com 客户端要验证的服务名称
├─ 443 未显式指定时的默认端口
└─ /orders TLS 建立后发送的 HTTP 资源路径同一张域名证书可以用于不同端口,资源路径也不写进证书。DNS 决定连接地址,证书校验仍围绕 URL 中的名称展开。名称解析过程见域名 / DNS,地址与端口连接见防火墙 / 端口 / 网络。
TLS 怎样使用证书
身份认证、密钥协商与数据加密
“SSL 证书”是沿用下来的产品名称。现代 HTTPS 使用 TLS;部署时通常保留 TLS 1.2 和 TLS 1.3,并根据实际客户端兼容要求维护配置,不启用旧 SSL 协议。版本与算法选择可对照 TLS 安全部署建议。
TLS 中有几种不同的密钥操作:
- CA 用自己的私钥签署证书,客户端用签发者的公钥检查签名。
- 服务端在握手中使用证书对应私钥,证明它持有该私钥。
- 双方协商并派生流量密钥,用对称加密保护后续数据。
以采用临时密钥交换的 TLS 1.3 首次证书握手为例,服务端证书的公钥用于认证,临时密钥交换产生共享秘密,二者共同参与安全连接的建立。把这个过程写成“浏览器生成密码,再用证书公钥加密交给服务器”,会误套旧式 RSA 密钥交换。
CertificateVerify 把服务端身份与本次握手关联,Finished 检查握手完整性。会话恢复可以使用 PSK,流程不一定再次发送完整证书;0-RTT 提前数据另有重放风险,涉及下单、支付等有副作用的请求时需要应用层专门处理。具体消息与安全条件见 TLS 1.3 规范。
RSA 和 ECDSA 是常见证书公钥算法。ECDSA P-256 的证书与签名通常较小;RSA 常用于兼容较旧客户端。选择应结合客户端支持和组织密码策略,不能把“RSA 证书”理解成只支持旧 TLS。服务端也可按产品能力同时配置两类证书。TLS 1.3 的密码套件名称与 TLS 1.2 不同,不要把一份旧 ssl_ciphers 字符串当作全部 TLS 配置。
SNI、ALPN 与 HTTP Host 各自发生在哪里
一个 IP 上可以托管多个 HTTPS 域名。客户端在 TLS 握手的 SNI 扩展中携带目标名称,入口据此选择证书;随后 HTTP 请求中的 Host 或 HTTP/2 的 :authority 用于请求路由。HTTP 头到达时,服务端通常早已完成证书选择。
所以,使用 https://IP/ 再手工加 Host: api.example.com,无法代替正常域名 HTTPS 测试。指定连接 IP 又保留域名身份,应使用后面的 curl --resolve。
ALPN 用于协商应用协议,例如 h2 或 http/1.1。HTTP/3 使用 QUIC 并集成 TLS 1.3;单纯配置 Nginx 的 TCP 443 监听不会自动启用 HTTP/3。SNI 与多站点证书配置见 Nginx HTTPS 文档。
一张 X.509 证书包含什么
证书是有结构、有签名的数据,.crt、.pem 只是常见文件后缀。
服务器证书
├─ Subject / Issuer 主体 / 签发者名称
├─ Serial Number 签发者分配的序列号
├─ Validity notBefore 到 notAfter
├─ Subject Public Key 服务器公钥及算法
├─ Extensions
│ ├─ subjectAltName DNS 名称或 IP 地址等服务标识
│ ├─ basicConstraints 是否为 CA、允许的下级 CA 深度
│ ├─ keyUsage 允许的密钥操作
│ └─ extendedKeyUsage 服务器认证、客户端认证等用途
└─ Signature 签发者对证书内容的签名SAN(Subject Alternative Name)是 HTTPS 名称匹配的关键字段。DNS:api.example.com 匹配域名;IP 访问需要 iPAddress 类型的 SAN,把 IP 字符串写进 DNS 类型并不等效。新证书应使用 SAN,不能依赖 CN 兜底。服务身份规范还限制了通配符匹配:*.example.com 可匹配 api.example.com,不覆盖 example.com 或 a.api.example.com。
CA:TRUE 允许证书作为 CA 使用,keyCertSign 对应签发证书;服务器叶子证书通常为 CA:FALSE,包含适合其算法的密钥用途以及 serverAuth。客户端证书使用 clientAuth。扩展会限制证书可用于什么操作,不能因为签名和日期正常就忽略用途。X.509 规范定义这些字段与路径验证规则。
CSR(Certificate Signing Request)是签名请求,包含公钥、申请信息和请求者签名,不携带私钥。CA 会按验证结果和签发策略生成最终证书,CSR 中请求了某个扩展也可能不被采纳。签发后仍需检查成品。
叶子、中间 CA 与根信任锚
常见签发关系如下:
根 CA 根公钥通过客户端的信任配置进入信任集合
└─ 签发中间 CA 中间证书的签名可沿路径验证
└─ 签发服务器证书 SAN 中包含 api.example.com
服务端通常发送:服务器证书 → 中间 CA 证书(必要时多张)
客户端本地持有:受信任根 CA 或其他明确配置的信任锚
服务端单独保管:服务器私钥Nginx 的 fullchain.pem 通常按“叶子在前、中间证书在后”拼接。根证书通常不需要发送;把一张自签根放进服务端发送链,客户端也不会因此信任它。信任来自客户端预置或管理员批准的配置。
一张叶子证书里没有“装着整条中间证书链”。浏览器可能缓存或下载中间证书,因此某台电脑能打开网页,干净容器却可能因服务端漏发中间证书而失败。部署时应提供所需中间证书,并按目标客户端检验链路;交叉签名和不同根库还可能产生不同可用路径。
CDN、负载均衡和源站要分开看:
浏览器 ── TLS A ── CDN / 负载均衡 ── TLS B 或 HTTP ── 源站
↑ A 的证书 ↑ B 的证书TLS 在哪里终止,哪里就要持有相应服务器私钥或使用受控密钥服务。第一跳通过认证后,第二跳仍需独立配置;浏览器展示的是第一跳证书。四层透传则把 TLS 交给后端终止,前置转发设备不一定加载该站点证书。
在隔离容器中完成一次 HTTPS 配置
准备工具与运行身份
下面使用 Linux Bash、Docker Engine 和 Ubuntu 24.04。镜像构建需要访问 Ubuntu 软件源,由构建阶段的 root 安装软件;实验容器以 UID/GID 10001:10001 运行,禁用外部网络、不挂载宿主目录、不发布宿主端口。所有密钥随临时容器结束而消失。
在新的空工作目录中,把以下内容保存为 Dockerfile:
FROM ubuntu:24.04
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
openssl nginx curl ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir /lab && chown 10001:10001 /lab
USER 10001:10001
WORKDIR /lab
CMD ["bash"]构建并进入容器:
docker build -t local/tls-certificate-lab:1 .
docker run --rm -it --network none local/tls-certificate-lab:1以下各步均在这个容器的同一个 Bash 会话、/lab 目录执行。Ubuntu 仓库提供安全维护版本,不假定它与官网最新版本号相同;先记录实际工具版本和身份:
set -euo pipefail
umask 077
id
openssl version
nginx -v
curl --version | head -n 1
mkdir pki v1 v2id 应显示 UID 10001;umask 077 使新私钥默认仅当前用户可读写。实验用未加密私钥便于非交互启动,不能据此把生产 CA 私钥也放进 Web 容器。生产内部 PKI 应独立保管根密钥、限制签发权限,并保存签发记录和恢复材料。
受限网络可在同架构联网环境构建该无密钥工具镜像,再通过受控镜像仓库或离线分发导入。不要为拉取软件关闭 TLS 校验,也不要把生成后的私钥重新打进镜像。
建立根 CA 和中间 CA
先生成练习用根密钥与自签根证书:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
-keyout pki/root.key -out pki/root.pem -days 30 -sha256 \
-subj '/CN=TLS Lab Root' \
-addext 'basicConstraints=critical,CA:TRUE,pathlen:1' \
-addext 'keyUsage=critical,keyCertSign,cRLSign'根的 pathlen:1 为一层中间 CA 留出空间。接着创建中间 CA 的密钥与 CSR:
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
-keyout pki/intermediate.key -out pki/intermediate.csr \
-subj '/CN=TLS Lab Intermediate'
cat > pki/intermediate.ext <<'EOF'
basicConstraints=critical,CA:TRUE,pathlen:0
keyUsage=critical,keyCertSign,cRLSign
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
EOF
openssl x509 -req -in pki/intermediate.csr \
-CA pki/root.pem -CAkey pki/root.key -set_serial 0x1001 \
-days 14 -sha256 -extfile pki/intermediate.ext -out pki/intermediate.pem中间 CA 的 pathlen:0 不再允许普通下级 CA,仍可签发叶子证书。-set_serial 只为这个独立练习固定标识;真实 CA 要管理序列号唯一性、申请授权、吊销与审计,不能用反复覆盖同一序列号的脚本签生产证书。参数和扩展语法分别见 OpenSSL req 与扩展配置。
给服务名称签发叶子证书
实验名称为 api.lab.test。它通过 curl 的地址覆盖映射到容器回环,不依赖公网 DNS,也不修改系统 hosts。
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
-keyout v1/privkey.pem -out v1/server.csr -subj '/CN=api.lab.test'
cat > pki/server.ext <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature
extendedKeyUsage=serverAuth
subjectAltName=DNS:api.lab.test
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
EOF
openssl x509 -req -in v1/server.csr \
-CA pki/intermediate.pem -CAkey pki/intermediate.key -set_serial 0x2001 \
-days 2 -sha256 -extfile pki/server.ext -out v1/cert.pem
cat v1/cert.pem pki/intermediate.pem > v1/fullchain.pem
ln -s v1 current叶子有效期短于中间 CA。2、14、30 天只是练习参数,公有 CA 的期限由其产品与签发策略决定。
签发完成后先离线验证名称、用途和签发路径:
openssl x509 -in v1/cert.pem -noout -subject -issuer -serial -ext subjectAltName
openssl verify -CAfile pki/root.pem -no-CApath -no-CAstore \
-untrusted pki/intermediate.pem -purpose sslserver \
-verify_hostname api.lab.test v1/cert.pem应看到 serial=2001、DNS:api.lab.test 和 v1/cert.pem: OK。-CAfile 提供受信任根,-untrusted 提供用于构建路径的中间证书;这里“untrusted”指它不是预先指定的信任锚,并非它已经被判定为恶意证书。选项见 OpenSSL verify。
加载到 Nginx,再从客户端访问
配置两个仅监听回环的端口:8443 发送完整链,8444 故意只发送叶子。将以下配置写入 /lab/nginx.conf:
cat > nginx.conf <<'EOF'
worker_processes 1;
pid /lab/nginx.pid;
error_log /lab/error.log warn;
events { worker_connections 64; }
http {
access_log off;
client_body_temp_path /lab/client_temp;
proxy_temp_path /lab/proxy_temp;
fastcgi_temp_path /lab/fastcgi_temp;
uwsgi_temp_path /lab/uwsgi_temp;
scgi_temp_path /lab/scgi_temp;
ssl_protocols TLSv1.2 TLSv1.3;
server {
listen 127.0.0.1:8443 ssl;
server_name api.lab.test;
ssl_certificate /lab/current/fullchain.pem;
ssl_certificate_key /lab/current/privkey.pem;
location / { default_type text/plain; return 200 "tls-ok\n"; }
}
server {
listen 127.0.0.1:8444 ssl;
server_name api.lab.test;
ssl_certificate /lab/current/cert.pem;
ssl_certificate_key /lab/current/privkey.pem;
location / { return 200 "leaf-only\n"; }
}
}
EOF
nginx -t -p /lab/ -c /lab/nginx.conf
nginx -p /lab/ -c /lab/nginx.confsyntax is ok 和 test is successful 表示配置与相关文件能被加载。高位端口及 /lab 下的 PID、日志、临时目录使普通用户可以运行这套配置,不能将这里的无访问日志设置直接当作生产配置。
现在让这个 curl 请求单独信任练习根:
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--connect-timeout 3 --max-time 5 --cacert pki/root.pem \
--resolve api.lab.test:8443:127.0.0.1 https://api.lab.test:8443/应输出 tls-ok,退出码为 0。-q 必须放在第一个参数位置,避免读取默认 curl 配置;--noproxy '*' 排除环境代理影响;--resolve 仅改变该名称和端口对应的连接地址,URL、SNI 与名称校验仍使用 api.lab.test。--fail-with-body 使 HTTP 4xx/5xx 也以失败退出。这些选项各自独立,见 curl 手册。
如果立即得到连接拒绝,先查看 /lab/error.log 和 Nginx 进程,确认上一步启动成功后再重试;不要跳过校验改用 -k。--cacert 不会把根证书安装进系统,也不会影响其他程序。
对照三种证书失败
以下命令预期失败,因此把退出码放在 if 的失败分支中读取,避免 set -e 提前结束会话。
先不提供练习根,使用容器自带公共信任库:
if curl -q --noproxy '*' --fail-with-body --silent --show-error \
--max-time 5 --cacert /etc/ssl/certs/ca-certificates.crt \
--resolve api.lab.test:8443:127.0.0.1 https://api.lab.test:8443/; then
echo 'unexpected trust success'; exit 1
else
rc=$?; printf 'untrusted-root exit=%s\n' "$rc"; test "$rc" -eq 60
fi退出码应为 60。服务端有完整发送链,但客户端不信任练习根;修复是为经过确认的 CA 配置信任,而非让服务端多发一张根证书。
再指定可信根,访问只发送叶子的 8444:
if curl -q --noproxy '*' --fail-with-body --silent --show-error \
--max-time 5 --cacert pki/root.pem \
--resolve api.lab.test:8444:127.0.0.1 https://api.lab.test:8444/; then
echo 'unexpected chain success'; exit 1
else
rc=$?; printf 'missing-intermediate exit=%s\n' "$rc"; test "$rc" -eq 60
fi这里缺少从叶子到根之间的中间证书,常见信息为 unable to get local issuer certificate。与前例相比,错误码相同,原因不同。应把这个监听的 ssl_certificate 改为 fullchain.pem 并重载;完整链端口 8443 已提供了成功对照。
最后保留正确链,却访问证书没有包含的名称:
if curl -q --noproxy '*' --fail-with-body --silent --show-error \
--max-time 5 --cacert pki/root.pem \
--resolve other.lab.test:8443:127.0.0.1 https://other.lab.test:8443/; then
echo 'unexpected hostname success'; exit 1
else
rc=$?; printf 'hostname-mismatch exit=%s\n' "$rc"; test "$rc" -eq 60
fiNginx 仍能接收 TCP/TLS 连接,但返回的证书只有 api.lab.test。修复需要正确的访问名称或包含所需 SAN 的证书,单改 HTTP Host 没有作用。
把 8444 的发送链修复为完整链,再用相同名称、端口和 CA 重试:
sed -i 's@ssl_certificate /lab/current/cert.pem;@ssl_certificate /lab/current/fullchain.pem;@' nginx.conf
nginx -t -p /lab/ -c /lab/nginx.conf
nginx -p /lab/ -c /lab/nginx.conf -s reload
repaired=''
for attempt in {1..20}; do
if repaired=$(curl -q --noproxy '*' --fail-with-body --silent --show-error \
--max-time 5 --cacert pki/root.pem \
--resolve api.lab.test:8444:127.0.0.1 https://api.lab.test:8444/); then break; fi
sleep 0.1
done
printf '%s\n' "$repaired"
test "$repaired" = 'leaf-only'响应文本仍叫 leaf-only,这是固定 HTTP 内容;TLS 链已经修复,curl 应成功退出。更换响应文字本身不影响证书校验。
文件换了,运行中的证书何时变化
先定义一个读取实际服务端序列号的函数。这里的 OpenSSL 同时检查信任链和名称,并保留错误日志:
peer_serial() {
timeout 8 openssl s_client -connect 127.0.0.1:8443 \
-servername api.lab.test -verify_hostname api.lab.test \
-verify_return_error -CAfile pki/root.pem -no-CApath -no-CAstore \
-showcerts </dev/null > peer.txt 2> peer-check.log || {
cat peer-check.log >&2; return 1;
}
openssl x509 -in peer.txt -noout -serial
}
peer_serial应输出 serial=2001。普通 s_client 会为了诊断而在某些验证错误后继续握手;严格检查要加 -verify_return_error,并显式设置目标名称校验。-showcerts 展示对端发送内容,不能单凭它断言链可信。s_client 手册解释了这些差异。
用一把新私钥签发第二版叶子,保持 SAN 和 CA 不变:
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
-keyout v2/privkey.pem -out v2/server.csr -subj '/CN=api.lab.test'
openssl x509 -req -in v2/server.csr \
-CA pki/intermediate.pem -CAkey pki/intermediate.key -set_serial 0x2002 \
-days 2 -sha256 -extfile pki/server.ext -out v2/cert.pem
cat v2/cert.pem pki/intermediate.pem > v2/fullchain.pem
openssl verify -CAfile pki/root.pem -no-CApath -no-CAstore \
-untrusted pki/intermediate.pem -purpose sslserver \
-verify_hostname api.lab.test v2/cert.pem
ln -s v2 current.next
mv -T current.next current
openssl x509 -in current/cert.pem -noout -serial
peer_serial本地路径已变成 serial=2002,新建连接仍应看到 serial=2001。这份 Nginx 配置在加载时读取静态证书文件,磁盘符号链接切换不会自动更新已加载内容。
验证配置后再发重载信号,并给新 worker 一个有上限的启动等待:
nginx -t -p /lab/ -c /lab/nginx.conf
nginx -p /lab/ -c /lab/nginx.conf -s reload
serial=''
for attempt in {1..20}; do
serial=$(peer_serial)
if [ "$serial" = 'serial=2002' ]; then break; fi
sleep 0.1
done
printf '%s\n' "$serial"
test "$serial" = 'serial=2002'
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--max-time 5 --cacert pki/root.pem \
--resolve api.lab.test:8443:127.0.0.1 https://api.lab.test:8443/应看到 serial=2002 和 tls-ok。重载时新 worker 使用新配置,旧 worker 处理已有连接后退出;测试应新建连接,不能拿一条早已建立的长连接判断是否换证成功。机制见 Nginx 控制文档。
若新版本加载失败且旧版仍有效、未泄漏,可在这个实验目录中重新把 current 指向 v1,执行配置检查与重载,再确认新连接序列号回到 2001。生产发布要串行执行证书变更,避免另一个任务在 nginx -t 与重载之间修改文件;过期或泄漏的旧版不可作为安全回退目标。
练习结束前,还可检查公钥匹配和剩余期限:
openssl x509 -in current/cert.pem -pubkey -noout > cert.pub
openssl pkey -in current/privkey.pem -pubout > key.pub
cmp cert.pub key.pub
openssl x509 -in current/cert.pem -checkend 3600 -noout
nginx -p /lab/ -c /lab/nginx.conf -s quit
exitcmp 相同则无输出且退出 0,不同则非零;读取或转换失败也必须单独处理。-checkend 3600 检查证书是否将在一小时内到期,非零还可能来自读取错误,应保留诊断信息。它不会代替完整路径、名称和当前有效性验证。字段和选项见 OpenSSL x509。exit 后临时容器自动删除,练习根不会留在宿主信任库中。
为真实服务选择和申请证书
公有 CA、私有 CA 与可验证名称
公网网站通常使用客户端已信任的公有 CA。DV 验证域名控制权;OV、EV 还涉及组织身份验证,选择取决于身份展示、采购和合规要求,不能据此比较 TLS 加密强度。公有证书的验证与签发要求见 CA/Browser Forum 基线要求。站点使用有效证书也仍可能承载恶意业务,浏览器的 HTTPS 状态只描述连接安全的一部分。
内部服务有两种常见选择。若使用组织控制的公开可注册域名,例如 service.internal.example.com,可以用公网 DNS 验证取得公有证书,而不开放业务端口。若使用 localhost、保留测试名称或私有地址,通常由内部 CA 处理;不要向公有 CA 申请本地实验名称。参见 localhost 证书说明。
公网 IP 证书则取决于 CA 产品、验证方式和客户端能力,不能概括为“公网 CA 一律不签 IP”。Let's Encrypt 的 profile 决定可用标识类型和期限,shortlived 支持 IP;实际可用项以 ACME directory 与官方 profile 文档为准。短期证书要求更可靠的自动续期,不宜继续套用固定的“提前 30 天”规则。
公有证书还会涉及 Certificate Transparency。域名出现在公开证书日志中可能暴露内部服务命名,所以“服务只在内网可达”和“名称不公开”要分别决策,详见 CT 日志说明。
CA 怎样验证控制权
ACME 客户端负责账户、订单、挑战应答、提交 CSR 和取回证书。账户密钥用于 ACME 账户操作,服务器私钥用于 TLS 服务,DNS API 凭据用于改解析,三者用途不同。
| 挑战方式 | CA 从哪里检查 | 适用条件与限制 |
|---|---|---|
| HTTP-01 | 公网 80 端口上的 /.well-known/acme-challenge/ | Web 入口能返回正确内容;不支持通配符 |
| DNS-01 | 权威 DNS 的 _acme-challenge TXT | 可自动管理公开 DNS;可验证通配符和仅内网提供服务的公开域名,不验证 IP 标识 |
| TLS-ALPN-01 | 公网 443 上特定 ALPN 的 TLS 应答 | 入口和客户端支持专门挑战处理;普通 HTTPS 站点响应不能替代,不支持通配符 |
HTTP-01 的文件由 ACME 客户端写入,CA 负责读取。多入口必须让每个可能收到验证请求的节点返回相同内容。Let's Encrypt 对 HTTP 跳转有协议、端口和跳转次数限制;配置路径时最好直接提供挑战内容。完整条件见挑战类型文档。
DNS-01 的 TXT 值由当前挑战计算产生,不能长期保留一个固定 token。DNS 服务商 API、权威节点更新和缓存都会影响验证。使用最小权限凭据,必要时把 _acme-challenge 委派到专门区域;CA 能跟随委派,不代表任意客户端插件都能自动写入被委派区域,必须核对插件配置。
还要检查 CAA:它规定哪些 CA 获准签发,限制可能来自上层名称或别名处理过程;不要遇到签发失败就删除整组 CAA。查询与规则见 CAA 文档。
用 Certbot webroot 完成首次签发
下面转到真实 Linux 主机的部署方法,与前面的无外网实验分开。需要 Ubuntu 24.04、sudo 权限、已获授权的域名与邮箱,公网 DNS 指向当前入口,80 端口可达。命令中的 example.com 和邮箱是占位值,替换并确认授权后才执行签发。该过程会联系 CA;本地实验不需要运行这一组命令。
在尚未部署入口的新主机上,可使用发行版软件包建立 Nginx 和 Certbot 主线:
sudo apt-get update
sudo apt-get install -y nginx certbot openssl curl dnsutils ca-certificates
nginx -v
certbot --version
sudo certbot plugins已有 Nginx 时先检查来源和版本,不重复安装另一套包。Ubuntu 仓库版本适用于这里的 webroot 基本功能;新 profile 或 DNS 插件可能要求更新客户端,应依据 Certbot 官方安装入口选择一种维护方式,避免 apt、snap、pip 多套程序和定时任务混用。企业镜像源和离线安装仍需保留包签名及依赖校验。
先只建立 HTTP 服务,避免 Nginx 引用尚不存在的证书。创建挑战目录:
sudo install -d -o root -g root -m 0755 /var/www/acme/.well-known/acme-challenge
sudoedit /etc/nginx/sites-available/api.example.com.conf将以下内容写入该新站点文件,替换所有域名:
server {
listen 80;
server_name api.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type text/plain;
try_files $uri =404;
}
location / { return 404; }
}Ubuntu 默认主配置的 http 内包含 sites-enabled/*。先检查该链接不存在,再启用新站点并验证;若服务器使用其他配置布局,应修改其实际包含目录,不创建一份永远不加载的文件。
sudo ln -s /etc/nginx/sites-available/api.example.com.conf \
/etc/nginx/sites-enabled/api.example.com.conf
sudo nginx -t && sudo systemctl reload nginx
printf 'acme-path-ok\n' | sudo tee /var/www/acme/.well-known/acme-challenge/probe >/dev/null
sudo chmod 0644 /var/www/acme/.well-known/acme-challenge/probe在另一台有公网直连条件的客户端上定义域名并做 GET:
DOMAIN='api.example.com'
dig -r "$DOMAIN" A +short
dig -r "$DOMAIN" AAAA +short
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--connect-timeout 5 --max-time 15 \
"http://$DOMAIN/.well-known/acme-challenge/probe"应输出 acme-path-ok。若发布了 AAAA,还要验证 IPv6 实际入口;本机回环或一次外部访问只反映对应路径,不能替 CA 的多个验证视角作保证。404 查 location、目录和多节点一致性;超时查 DNS、监听、网络规则与接入限制。中国大陆公网入口还需满足服务商接入和备案等适用要求,证书签发本身不会替代这些要求。
回到服务器,先让 Certbot 进行测试签发。测试会联系 Let's Encrypt staging,不保存可用于公众访问的生产证书:
DOMAIN='api.example.com'
ACME_EMAIL='tls-admin@example.com'
sudo certbot certonly --webroot -w /var/www/acme \
--cert-name "$DOMAIN" -d "$DOMAIN" --email "$ACME_EMAIL" --dry-run按交互提示核对账户和条款。测试成功后,再明确执行真实签发:
sudo certbot certonly --webroot -w /var/www/acme \
--cert-name "$DOMAIN" -d "$DOMAIN" --email "$ACME_EMAIL"
sudo certbot certificates成功输出会列出证书名称、域名、有效期和文件路径。certonly 负责取得证书,不自动完成这份 Nginx 配置;--cert-name 是本机证书集合名称。增加 SAN 用多个 -d,所有名称都必须完成授权。已有同名证书时先确认该操作是否要更新现有集合。命令语义见 Certbot 使用指南。
不要把重试写成无限循环。授权失败和重复签发可能受到 CA 限制,应按错误详情及 Retry-After 处理,先修复验证路径,再用测试环境复测。限额文档也说明了支持 ARI 的续期与普通重复签发在限额上的差异。
DNS 自动化与其他证书工具
DNS-01 应选择与 DNS 服务商匹配、可自动完成认证和清理的插件。先用 certbot plugins 确认插件,按其文档配置区域、凭据文件和传播等待;凭据文件只允许执行签发任务的账号读取。手工添加 TXT 适合临时操作,长期续期需要自动认证 hook 或 DNS 插件。不要把某家云平台的参数复制给另一家。
已有 acme.sh 时,重点核对 CA 选择、账户目录权限和安装步骤。业务服务不要直接依赖 acme.sh 的内部工作目录;使用 --install-cert 将证书安装到预先授权的目标路径,并配置实际可执行的 --reloadcmd。该命令会被后续自动更新复用,运行用户必须有写文件和受控重载的权限,不能依赖无人值守任务里弹出的 sudo 密码提示。各 DNS API、安装与更新参数见 acme.sh 官方仓库。
Caddy 适合让入口同时管理证书申请和更新。自动 HTTPS 依赖挑战可达性和可写、持久化的数据目录;容器反复丢失账户、证书状态会导致重新申请。tls internal 可以使用 Caddy 内部 CA,但远端客户端仍需明确分发信任。手工指定证书则要另行安排证书更新。安装及配置分支见 Automatic HTTPS 和 tls 指令。
云厂商的托管证书还要确认是否可导出私钥、是否仅供指定负载均衡/CDN 使用、自动更新覆盖哪些资源。选择托管后,应从实际公网入口检查证书,不能拿控制台“已签发”当作所有部署节点均已更新。
把证书部署到正确的 TLS 入口
Nginx 的 HTTPS 配置与私钥权限
沿用前面的 Certbot 主机,假定 certbot certificates 确认实际目录为 /etc/letsencrypt/live/api.example.com/。在同一个站点文件中保留 80 端口的 challenge location,将其普通请求改为固定域名跳转,再增加 HTTPS server:
server {
listen 80;
server_name api.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type text/plain;
try_files $uri =404;
}
location / { return 308 https://api.example.com$request_uri; }
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:TLS:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
access_log /var/log/nginx/api.example.com.access.log;
error_log /var/log/nginx/api.example.com.error.log warn;
location = /tls-health {
default_type text/plain;
return 200 "tls-entry-ok\n";
}
location / { return 404; }
}这是独立可访问的 TLS 入口,暂不假设某个后端已经安装;/tls-health 返回固定文本,用来检查入口。应用接入时再把普通 location 改为真实上游。HTTP/2 为可选项:http2 on; 需要 Nginx 1.25.1 及以上和对应模块,Ubuntu 24.04 仓库的 1.24 系列不能直接照抄它。上例不依赖 HTTP/2,启用方法见 HTTP/2 模块。
会话缓存用于减少重复握手开销,ticket 关闭简化了示例中的 ticket 密钥管理。共享缓存名称及大小要与现有全局配置协调。需要调整 TLS 密码策略或多证书配置时,应按部署版本核对 Nginx SSL 模块,不要堆入不理解的旧算法列表。
系统 Nginx 通常由 root master 读取私钥,再创建受限 worker。私钥应仅对确需读取的身份开放;Certbot 的 live 文件可能链接到 archive,排查权限时要检查整条路径。非 root 容器则需要给容器 UID/GID 精确读取权限,并挂载可完整访问的目录。只挂载某个符号链接文件可能漏掉实际目标;更新时还可能保留旧文件挂载。不要用 chmod 777 解决问题。
服务器执行配置检查与重载,再到外部客户端执行 curl:
sudo nginx -t && sudo systemctl reload nginx
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--connect-timeout 5 --max-time 15 https://api.example.com/tls-health应返回 tls-entry-ok。上例只配置 IPv4 监听;如 DNS 发布 AAAA,需在已具备 IPv6 地址、路由与访问规则的入口补相应 [::]:80、[::]:443 ssl 监听并分别验证,或撤去不提供服务的 AAAA。不要让 CA 与用户随机落到未配置的地址族。
文件格式转换与 Java 应用
PEM 是带 BEGIN ... 标记的文本编码,可在一个文件中串联多张证书;DER 是二进制编码。PKCS#12(常见 .p12、.pfx)是容器格式,可同时携带证书链与私钥。因此收到 .pfx 时应按含敏感密钥材料处理,不能把所有“证书文件”都当作可公开附件。
对已确认位于当前受限目录的 cert.pem、chain.pem 和 privkey.pem,普通文件所有者可执行:
umask 077
openssl x509 -in cert.pem -outform DER -out cert.der
openssl x509 -inform DER -in cert.der -noout -subject
openssl pkcs12 -export -inkey privkey.pem -in cert.pem \
-certfile chain.pem -name server -out server.p12
openssl pkcs12 -in server.p12 -info -noout导出与读取会交互询问密码,不把密码写在命令行或仓库中。转换只改变存储形式,不会增加 SAN、延长有效期或修复签发链。旧软件若无法读取新 PKCS#12 加密算法,应先核对兼容性并升级,谨慎评估旧算法模式,而非无条件追加 -legacy。参数见 OpenSSL PKCS#12 文档。
Java 中,保存本机私钥与证书的 keystore 用于出示身份;truststore 保存用于验证对端的信任材料。应用出站访问内部 HTTPS 时,通常只需可信 CA,不应分发服务器私钥。先通过可信渠道核对内部根的 SHA-256 指纹,再以应用配置维护者身份建立专用 truststore:
openssl x509 -in internal-root.pem -noout -subject -fingerprint -sha256
keytool -importcert -alias internal-root -file internal-root.pem \
-keystore app-trust.p12 -storetype PKCS12
keytool -list -v -keystore app-trust.p12 -storetype PKCS12交互确认指纹和密码后,应能查到该别名。创建文件还需让实际 HTTP 客户端使用它;如果用这个单根 truststore 替代整个默认集合,公共网站可能不再受信任。不要直接覆盖 JDK 的全局 cacerts,也不要把 TrustAll 作为内部 CA 接入方式。keytool 和 JSSE 指南说明了存储与加载方式。Spring Boot 可使用 PEM/JKS/PKCS#12 SSL Bundle,再配置到服务器或相应客户端;仅声明 Bundle 不一定自动作用于所有 HTTP 客户端,参见 Spring Boot SSL。
系统 curl、浏览器、Java、容器内程序可能使用不同的根库或 TLS 后端。内部根更新要按实际使用方分发,生产根 CA 轮换通常先让客户端同时信任新旧根,再迁移服务器链,确认旧链不再使用后撤去旧根;根密钥泄漏则需要更紧急的撤信任和重签安排。
上游 HTTPS 和 mTLS
Nginx 终止公网 TLS 后,如果到后端还有 HTTPS,Nginx 此时是 TLS 客户端。其 proxy_ssl_verify 默认关闭,不能只把 proxy_pass 改成 https:// 就认为已验证上游身份。
假定实际后端地址为 10.0.0.20:8443,证书 SAN 为 backend.internal.example.com,可信内部 CA 包已由管理员安装到 /etc/nginx/trust/internal-ca.pem。把普通 location 改为:
location / {
proxy_pass https://10.0.0.20:8443;
proxy_ssl_server_name on;
proxy_ssl_name backend.internal.example.com;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/trust/internal-ca.pem;
proxy_ssl_verify_depth 2;
proxy_set_header Host backend.internal.example.com;
proxy_set_header X-Forwarded-Proto $scheme;
}替换地址、名称和 CA 路径后,执行 nginx -t、重载并通过入口调用后端。出现 502 时看 Nginx 错误日志:upstream SSL certificate verify error 指向第二跳证书验证,connect() failed 指向连接问题。proxy_ssl_name 明确用于 SNI 和验证的上游名称,HTTP Host 则应按后端虚拟主机要求设置。指令见 Nginx proxy 模块。
mTLS 在服务器认证之外增加客户端证书认证。Nginx 接收客户端证书时,可在独立、经过授权的 HTTPS server 配置中增加:
ssl_client_certificate /etc/nginx/trust/client-ca.pem;
ssl_verify_client on;
ssl_verify_depth 2;客户端需从负责签发的 CA 取得包含 clientAuth 用途的证书及配对私钥,并保护私钥文件。假定当前目录已有 client-fullchain.pem、client.key 和验证服务器所需的 server-ca.pem,以有权读取这些文件的普通用户请求:
curl -q --noproxy '*' --fail-with-body --silent --show-error \
--connect-timeout 5 --max-time 15 --cacert server-ca.pem \
--cert client-fullchain.pem --key client.key https://api.example.com/tls-health应返回该独立入口配置的响应。再去掉客户端证书参数,或换成不受信任、用途错误的证书,确认请求被拒绝;同时保留原有服务端验证。不要直接在已有公共入口开启 mTLS,否则普通浏览器访问会受影响。
认证通过后还需把证书身份映射到应用权限。如果向后端转发证书身份字段,入口必须覆盖客户端伪造的同名头,且后端只接受可信入口连接。某张证书来自受信任 CA,并不意味着它可以调用所有管理接口。
续期、加载、监控与泄漏处理
定时任务要完成到哪一步
一个可用的续期过程包括:
定时检查 / CA 续期建议
→ 完成授权与签发
→ 保存新证书及密钥
→ 检查目标服务配置
→ 让每个 TLS 终止节点加载
→ 新连接验证实际证书和业务响应有效期由 CA、profile 和实际证书决定。支持 ARI 的客户端可参考 CA 建议窗口;其他客户端也有自己的到期判断。定时运行的是“检查并按需续期”,不应人为等到某个固定日历日期才启动脚本。
在使用 Ubuntu apt 版 Certbot 的主机上,先查看实际定时任务:
systemctl list-timers --all certbot.timer
systemctl cat certbot.timer certbot.service
sudo journalctl -u certbot.service -n 80 --no-pager应存在下次运行计划,且 ExecStart 指向预期的 Certbot。若该安装提供了 unit 却未启用,在确认没有重复 cron、snap 或外部调度后执行 sudo systemctl enable --now certbot.timer。其他安装方式使用自己的任务名称,不能因查不到这个 unit 就再装一套客户端。
对前面已成功签发的证书,进行续期演练:
sudo certbot renew --cert-name api.example.com --dry-run预期是该证书的模拟续期成功。--dry-run 默认不执行 deploy hook;它也可能运行 pre/post hook,因此仍应在明确了解现有 hooks 后执行。
让后续真正续期成功时自动重载系统 Nginx,可由 root 在 /etc/letsencrypt/renewal-hooks/deploy/20-nginx-reload 创建以下文件,内容中的绝对路径应先用 command -v 核实:
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/usr/bin/systemctl reload nginx用 sudoedit 保存并设置权限;下面第二条命令会真实执行重载,仅在允许重载该入口时运行:
sudo chmod 0750 /etc/letsencrypt/renewal-hooks/deploy/20-nginx-reload
sudo certbot renew --cert-name api.example.com --dry-run --run-deploy-hooks启用 --run-deploy-hooks 后,成功测试调用适用的 hook,使用当前活动证书而非临时 staging 证书。演练成功仍需查看 hook 日志和实际入口,尤其是 hook 本身失败的情况。不能用“Certbot 没有报签发失败”代替部署成功判断。
容器入口应由受控部署任务重载正确容器或更新服务,单台宿主的 systemctl reload nginx 不会影响另一个容器。证书目录、符号链接目标、运行身份和 reload 命令必须属于同一部署方案,多节点则要逐节点确认后再完成变更。
从真实入口检查到期和版本
文件日期检查适合提前发现本地问题;公众实际看到的证书来自当前连接所到达的 TLS 终止点。下面是 Linux 上的独立检查,在允许直连的客户端运行,DOMAIN 为目标名称,CONNECT 可以是域名或已获授权的具体节点 IP:
set -euo pipefail
DOMAIN='api.example.com'
CONNECT="$DOMAIN:443"
CHECK_DIR=$(mktemp -d)
if timeout 15 openssl s_client -connect "$CONNECT" -servername "$DOMAIN" \
-verify_hostname "$DOMAIN" -verify_return_error \
-CAfile /etc/ssl/certs/ca-certificates.crt -no-CApath -no-CAstore \
-showcerts </dev/null > "$CHECK_DIR/peer.txt" 2> "$CHECK_DIR/verify.log"; then
openssl x509 -in "$CHECK_DIR/peer.txt" -noout -serial -fingerprint -sha256 -dates
else
cat "$CHECK_DIR/verify.log" >&2
exit 1
fi这里只读取公开证书,临时目录保留供检查;无需保存私钥。内部 CA 替换为已核实的 CA 文件。timeout 非零时也可能是连接或退出等待超时,要结合日志判断;不应丢弃标准错误后把空结果算成“证书不过期”。自动监控还应检查获取失败和 HTTP 可用性,不能只对成功取得的 notAfter 做减法。
设定告警时先明确人工接管需要多久,再结合证书剩余寿命和续期频率留出重试时间。持续看“续期任务失败、部署失败、在线证书剩余时间、探测失败”四种信号。不同 CA/profile 的期限不同,固定阈值可能让短证书从签发开始就一直报警。CDN、负载均衡、源站和多地址入口应分别登记名称、证书位置、更新任务与负责人。
密钥泄漏、吊销和信任变化
服务器私钥泄漏后,需要生成新密钥并重新签发、部署到所有终止节点,并按 CA 流程吊销受影响证书、排查泄漏来源。若正在遭到冒用,应及时隔离或吊销,不能为了等替换证书而继续放任风险。新证书若继续使用已泄漏私钥,攻击者仍持有对应身份材料。ACME 账户密钥、DNS token 泄漏还需撤销或轮换对应凭据,不能只处理 TLS 私钥。
证书删除与吊销是不同动作:从某台机器删除文件不会通知客户端该证书已失效。CA 可能通过 CRL、OCSP 等发布吊销信息,客户端采取什么检查策略另有差异;默认 openssl verify 或 curl 成功不意味着已完成所有吊销检查。Let's Encrypt 已不提供 OCSP,不能无条件为所有证书加入 OCSP stapling 配置。签发 profile、实际证书扩展和客户端策略必须对应。吊销操作入口见 Let's Encrypt 撤销文档。
内部 CA 还需考虑签发数据库、CRL 发布、根证书分发和密钥备份恢复。练习中把三层密钥放在同一临时目录是为了学习签发关系;真实根私钥应与在线业务隔离,使用受控密钥设施或离线保护。备份必须加密、控制访问,并验证能够恢复签发服务;发现根密钥泄漏时,处理对象会扩大到该根信任下的证书和客户端。
按失败阶段排查 HTTPS
先确认检查的是哪一个入口
普通请求与指定节点请求使用同一 URL、同一客户端信任设置,只改变连接地址。Linux 客户端可以执行:
DOMAIN='api.example.com'
TARGET_IP='192.0.2.10'
curl -q --noproxy '*' --fail-with-body --verbose \
--connect-timeout 5 --max-time 15 "https://$DOMAIN/tls-health"
curl -q --noproxy '*' --fail-with-body --verbose \
--connect-timeout 5 --max-time 15 --resolve "$DOMAIN:443:$TARGET_IP" \
"https://$DOMAIN/tls-health"把文档地址替换为获准访问的节点。直连受网络策略禁止时,不绕过企业代理,应从许可的探测点进行;经代理请求则要单独记录代理参与的解析、连接和信任行为。
查看 Connected to 对应地址、TLS 验证信息和 HTTP 状态。指定节点成功而普通请求失败,可能是普通解析、另一地址族、其他入口或节点配置不同;需结合实际连接地址缩小范围,不能只凭这一组结果断言 DNS 故障。
名称、时间和链各有不同修复
| 观察结果 | 优先定位 | 修复后如何复测 |
|---|---|---|
hostname mismatch、浏览器名称错误 | 实际 URL、SNI、SAN、默认虚拟主机 | 使用原目标名称新建连接,不能靠换成 IP 或关闭验证通过 |
certificate has expired / not yet valid | 叶子与中间证书的有效期、客户端时钟、在线与本地版本 | 更换正确链或修复时间同步,再检查同一入口 |
unable to get local issuer certificate | 服务端发送链、客户端 CA 集合、TLS 中间代理 | 补正确中间链或经核实的根信任,再用干净客户端验证 |
key values mismatch | 证书公钥与所加载私钥、文件版本 | 校对配对文件,nginx -t 后重载,再读取在线证书 |
Permission denied / cannot load certificate key | 运行用户、目录遍历权限、挂载、SELinux/AppArmor | 精确授权或修复挂载/策略,禁止扩大到全员可读写 |
在线证书读取用前面的严格握手命令,本地文件用 openssl x509 -noout -subject -issuer -dates -ext subjectAltName。若在线证书已过期导致严格命令失败,可额外运行只用于取样的 s_client -showcerts 保存对端发送链,明确把它当作待检查输入,不能把宽松取样的返回码当作信任通过。
检查时间先用 timedatectl status 看本机同步状态,避免在远程生产机上随意手工改钟。检查权限可以用 namei -l /实际私钥路径 逐级看目录,以及 sudo nginx -t 查看准确错误;不要把 nginx -T 的全部配置直接发到公共渠道,其中可能含内部地址或凭据。
签发失败要回到具体挑战
HTTP-01 返回 404,先在授权站点创建无敏感信息的 probe,再从外部 GET 同一路径。若只有一台入口返回正确内容,修复分发或改用适合当前拓扑的挑战方式;若是重定向、认证、WAF 或缓存改变响应,给 challenge 路径配置专门处理。涉及 AAAA 时独立检查 IPv6,不能只看 A。
DNS-01 返回错误 TXT,可先查当前权威,再问权威节点:
ZONE='example.com'
CHALLENGE='_acme-challenge.api.example.com'
dig -r "$ZONE" NS +short
AUTH_NS='ns1.example.net'
dig -r @"$AUTH_NS" "$CHALLENGE" TXT +norecurse
dig -r "$CHALLENGE" CNAME +shortAUTH_NS 必须替换为实际权威名称;存在 CNAME/子区委派时跟到实际管理区域。值不正确就检查插件写入位置、凭据权限和并发挑战,值已正确则结合 CA 错误与缓存继续判断。不要删除同名 RRset 中其他仍在验证的 TXT。修复后重新执行测试签发,避免把所有失败归为“DNS 还没生效”。
CAA、限额、账户授权与 ACME API 网络错误由各自错误类型处理。若 CA API 出站受限,应按组织网络策略配置获准代理或专用签发节点;更换 DNS 记录无法修复 CA API 不可达。
TLS 成功之后仍可能是应用问题
HTTPS 已返回 401、403、404 或 502 时,先看响应由哪层产生。401/403 常与认证授权有关,404 可能是虚拟主机或资源路径,502 可能是上游连接、协议或第二跳 TLS。继续看 Nginx 错误日志与后端请求日志,而不是反复重新签证书。
浏览器中按 F12,使用开发者工具的安全信息入口查看证书与连接;版本不同菜单位置可能变化,也可从地址栏站点信息进入证书查看器。检查 Subject Alternative Name、签发者、有效期及证书路径。Network 面板查看具体失败请求和远端地址,Console 中的 Mixed Content 则提示 HTTPS 页面仍引用 HTTP 子资源。把问题定位到具体脚本、图片或 API URL,再修正生成地址或资源引用。
入口终止 TLS、后端收到 HTTP 时,应用生成绝对跳转需要正确识别原始协议。代理传递 X-Forwarded-Proto 等头,应用只信任获准代理设置的值;放开任意客户端伪造转发头会带来额外风险。详细入口配置可继续阅读 Nginx。
HSTS 让浏览器在后续访问中强制使用 HTTPS,证书失效时也可能无法绕过。应先验证所有相关站点与恢复方案,再逐步增加 max-age;includeSubDomains 和 preload 的影响范围更大,不能在初次实验中随手开启。撤销策略也需要通过有效 HTTPS 送达客户端,详见 HSTS 文档。
权威资料与规范地址
字段定义、版本差异和完整参数可从以下入口查阅;运行时应以实际安装版本及 CA 当前策略为准。
- RFC 8446:TLS 1.3
- RFC 9325:TLS 安全部署
- CA/Browser Forum 基线要求
- RFC 9525:TLS 服务身份匹配
- RFC 5280:X.509 证书与路径验证
- OpenSSL req
- OpenSSL 证书扩展
- OpenSSL verify
- OpenSSL x509
- OpenSSL s_client
- OpenSSL PKCS#12
- curl 参数手册
- Nginx HTTPS 配置
- Nginx SSL 模块
- Nginx HTTP/2 模块
- Nginx proxy 模块
- Nginx 重载与控制
- Certbot 安装入口
- Certbot 使用指南
- Let's Encrypt 挑战方式
- Let's Encrypt profile 与期限
- Let's Encrypt localhost 说明
- Let's Encrypt CAA
- Let's Encrypt 限额
- Let's Encrypt 吊销
- Let's Encrypt CT 日志
- acme.sh 官方仓库
- Caddy Automatic HTTPS
- Caddy tls 指令
- Oracle keytool
- Oracle JSSE
- Spring Boot SSL
- MDN HSTS
