SSL/TLS证书是互联网安全通信的基础设施,用于加密客户端与服务器之间的数据传输,防止中间人攻击和数据窃听。本指南涵盖SSL证书的原理、类型、申请流程、部署配置及日常运维,帮助开发者和运维人员全面掌握SSL证书的使用。
SSL(Secure Sockets Layer,安全套接层)及其继任者TLS(Transport Layer Security,传输层安全)是用于在两个通信应用程序之间提供保密性和数据完整性的协议。尽管SSL 3.0已被弃用,但"SSL证书"这一术语仍被广泛使用,实际上现代证书均基于TLS协议。
SSL/TLS通过以下机制保障通信安全:
当客户端首次连接HTTPS服务器时,会经历以下握手过程:
理解握手流程有助于排查SSL连接问题。例如,如果客户端和服务器没有共同的加密套件,握手会在Server Hello后失败;如果证书链不完整或根证书不被信任,则会在证书验证阶段报错。
SSL证书采用链式信任模型:
服务器在握手时需要提供完整的证书链(终端证书 + 中间证书),以便客户端能够沿着链验证到受信任的根证书。如果只提供终端证书而缺少中间证书,部分客户端可能因无法构建完整信任链而报错。
Hugo踩坑记录:早期在Nginx上部署Let's Encrypt证书时,曾遇到Android客户端无法连接的问题。排查后发现是Nginx配置中只配置了fullchain.pem中的终端证书,而遗漏了中间证书。Let's Encrypt的fullchain.pem已经包含了终端证书和中间证书,但某些旧版本的Nginx配置示例只引用了cert.pem(仅终端证书)。正确的做法是始终使用fullchain.pem作为ssl_certificate。
| 类型 | 验证内容 | 颁发时间 | 适用场景 | 价格区间 |
|---|---|---|---|---|
| DV(域名验证) | 验证域名所有权 | 分钟级 | 个人博客、小型网站 | 免费~低 |
| OV(组织验证) | 验证域名所有权 + 组织身份 | 1-3天 | 企业官网、电商平台 | 中 |
| EV(扩展验证) | 严格验证组织身份和法律存在 | 3-7天 | 金融机构、大型电商 | 高 |
| 类型 | 覆盖范围 | 示例 | 适用场景 |
|---|---|---|---|
| 单域名证书 | 仅一个域名 | www.example.com |
单一服务 |
| 多域名证书(SAN) | 多个指定域名 | www.example.com, api.example.com |
多个相关服务 |
| 通配符证书 | 一个域名下的所有子域名 | *.example.com |
大量子域名场景 |
| 多域名通配符 | 多个域名的所有子域名 | *.example.com, *.example.org |
大型企业 |
通配符证书可以保护无限数量的同级子域名(如a.example.com、b.example.com),但不能跨级保护(*.example.com不匹配sub.a.example.com)。对于多级子域名场景,需要申请更高级别的通配符或使用SAN扩展。
| 维度 | 免费证书(Let's Encrypt等) | 商业证书 |
|---|---|---|
| 价格 | 免费 | 年费数百至数千元 |
| 有效期 | 90天(需频繁续期) | 1-2年 |
| 验证等级 | 仅DV | DV/OV/EV可选 |
| 通配符支持 | 支持(Let's Encrypt) | 支持 |
| 多域名支持 | 支持(SAN) | 支持 |
| 技术支持 | 社区支持 | 厂商技术支持 |
| 保险/赔付 | 无 | 部分提供 |
| 兼容性 | 99%+设备兼容 | 99.9%+设备兼容 |
选型建议:
Let's Encrypt是由Mozilla、Cisco、Akamai等发起的免费证书颁发机构,通过自动化ACME协议简化了证书申请和续期流程。其核心特点:
申请Let's Encrypt通配符证书需要满足:
Certbot是EFF(电子前哨基金会)官方推荐的ACME客户端,支持多种操作系统和Web服务器。
Debian/Ubuntu:
sudo apt-get update
sudo apt-get install certbot
# 或安装完整插件包
sudo apt-get install certbot python3-certbot-nginx
CentOS/RHEL:
sudo yum install certbot
# 或
sudo dnf install certbot
验证安装:
certbot --version
通配符证书必须使用DNS-01验证,因为HTTP-01验证无法覆盖通配符域名。
手动申请流程:
sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d "example.com"
注意:同时指定
*.example.com和example.com,确保证书同时覆盖根域名和所有子域名。
执行后Certbot会提示:
Please deploy a DNS TXT record under the name:
_acme-challenge.example.com
with the following value:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
在DNS服务商处添加TXT记录后,等待DNS传播(通常几秒到几分钟),然后按回车继续。Certbot验证通过后会颁发证书。
Hugo内部约定:在团队环境中,手动申请流程不适合自动化。我们使用DNS插件实现全自动申请。例如,如果域名托管在阿里云DNS:
# 安装阿里云DNS插件
pip install certbot-dns-aliyun
# 配置认证信息
cat > /etc/letsencrypt/aliyun-credentials.ini <>EOF
# 申请证书(全自动,无需人工干预)
certbot certonly \
--authenticator dns-aliyun \
--dns-aliyun-credentials /etc/letsencrypt/aliyun-credentials.ini \
-d "*.example.com" \
-d "example.com"
Certbot颁发的证书默认存储在:
/etc/letsencrypt/live/example.com/
├── cert.pem # 终端实体证书(仅服务器证书)
├── chain.pem # 中间证书链
├── fullchain.pem # 终端证书 + 中间证书(推荐用于Nginx/Apache)
└── privkey.pem # 私钥文件
关键区别:
cert.pem:仅包含服务器证书,不包含中间证书fullchain.pem:包含服务器证书 + 所有中间证书,推荐用于大多数服务器配置chain.pem:仅包含中间证书privkey.pem:私钥,必须严格保密Hugo踩坑记录:曾经将cert.pem配置为Nginx的ssl_certificate,导致部分移动客户端无法建立连接。原因是这些客户端没有缓存Let's Encrypt的中间证书,无法构建完整信任链。改为fullchain.pem后问题解决。此后团队规范:所有Nginx/Apache配置必须使用fullchain.pem。
将证书部署到Nginx:
# 复制证书到Nginx配置目录(可选,也可以直接引用Let's Encrypt路径)
sudo mkdir -p /etc/nginx/ssl
sudo cp /etc/letsencrypt/live/example.com/fullchain.pem /etc/nginx/ssl/
sudo cp /etc/letsencrypt/live/example.com/privkey.pem /etc/nginx/ssl/
# 设置适当权限
sudo chmod 600 /etc/nginx/ssl/privkey.pem
sudo chmod 644 /etc/nginx/ssl/fullchain.pem
Nginx SSL配置示例:
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# 证书配置(必须使用fullchain.pem)
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# 现代TLS配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS(强制HTTPS)
add_header Strict-Transport-Security "max-age=63072000" always;
# 其他配置...
location / {
proxy_pass http://backend;
}
}
# HTTP重定向到HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
关键配置项说明:
ssl_protocols TLSv1.2 TLSv1.3:禁用TLS 1.0/1.1(存在已知漏洞),仅启用现代版本ssl_prefer_server_ciphers off:允许客户端选择加密套件,优先使用其支持的 strongest cipherssl_ciphers:配置强加密套件列表,ECDHE提供前向保密(Forward Secrecy)HSTS:告知浏览器始终通过HTTPS访问,防止SSL剥离攻击Hugo项目案例:在支付系统的API网关Nginx配置中,我们增加了以下安全头:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
这些头与SSL配置共同构成了传输层和应用层的完整安全策略。
# 检查Nginx配置语法
sudo nginx -t
# 重载Nginx(不中断现有连接)
sudo systemctl reload nginx
# 或
sudo nginx -s reload
Let's Encrypt证书有效期为90天,Certbot会自动安装systemd定时任务或cron任务进行续期。
检查自动续期任务:
# 查看systemd定时器
systemctl list-timers | grep certbot
# 查看cron任务
cat /etc/cron.d/certbot
手动测试续期:
# 模拟续期(不实际更新证书)
sudo certbot renew --dry-run
# 强制续期(正常无需手动执行)
sudo certbot renew --force-renewal
Hugo运维经验:在容器化环境中,Certbot的自动续期需要特殊处理。我们在Kubernetes中使用Cert-Manager组件管理证书,通过Ingress注解自动申请和续期:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- "*.example.com"
secretName: wildcard-tls
rules:
- host: api.example.com
http:
paths:
- path: /
backend:
service:
name: api-service
port:
number: 80
acme.sh是另一款流行的ACME客户端,纯Shell实现,无需依赖Python。
安装:
curl https://get.acme.sh | sh
# 或
wget -O - https://get.acme.sh | sh
申请通配符证书:
# 设置DNS API凭证(以阿里云为例)
export Ali_Key="your-access-key"
export Ali_Secret="your-secret"
# 申请证书
acme.sh --issue --dns dns_ali -d "*.example.com" -d "example.com"
自动部署到Nginx:
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/privkey.pem \
--fullchain-file /etc/nginx/ssl/fullchain.pem \
--reloadcmd "systemctl reload nginx"
acme.sh的优势在于自动续期集成更完善,支持部署钩子(deploy hook),在续期后自动执行重载命令。
对于不方便使用命令行的场景,ZeroSSL等在线平台提供Web界面申请免费证书:
ZeroSSL的免费证书也是90天有效期,但提供邮件提醒续期。
以申请OV证书为例:
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr
| 检查项 | 推荐配置 | 风险 |
|---|---|---|
| 证书链完整 | 使用fullchain.pem | 客户端连接失败 |
| 私钥权限 | 600(仅root可读) | 私钥泄露 |
| TLS版本 | 仅TLS 1.2+ | 协议漏洞 |
| 加密套件 | 强加密套件 + 前向保密 | 弱加密被破解 |
| HSTS | 启用,max-age≥1年 | SSL剥离攻击 |
| OCSP Stapling | 启用 | 连接延迟、隐私泄露 |
| Session复用 | 启用Session Tickets | 握手性能开销 |
# 现代SSL配置(Nginx 1.18+)
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;
# 证书
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# 协议版本
ssl_protocols TLSv1.2 TLSv1.3;
# 加密套件(Mozilla Intermediate配置)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# 会话缓存
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 日志
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP重定向
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/fullchain.pem
SSLCertificateKeyFile /etc/apache2/ssl/privkey.pem
# 现代TLS配置
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
# HSTS
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
# 其他配置...
</VirtualHost>
Docker Compose示例:
version: '3'
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- cert-data:/etc/letsencrypt:ro
depends_on:
- certbot
certbot:
image: certbot/certbot
volumes:
- cert-data:/etc/letsencrypt
- ./certbot-data:/var/lib/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h; done'"
volumes:
cert-data:
证书过期会导致服务完全不可用,必须建立监控机制。
使用OpenSSL检查证书信息:
# 查看证书详情
openssl x509 -in /etc/nginx/ssl/fullchain.pem -noout -text
# 查看有效期
openssl x509 -in /etc/nginx/ssl/fullchain.pem -noout -dates
# 检查远程服务器证书
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates
Prometheus监控脚本:
#!/bin/bash
# ssl_expiry.sh - 输出Prometheus格式的证书过期时间
domain=$1
port=${2:-443}
expiry_date=$(echo | openssl s_client -connect ${domain}:${port} -servername ${domain} 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
expiry_epoch=$(date -d "${expiry_date}" +%s)
current_epoch=$(date +%s)
days_until_expiry=$(( (expiry_epoch - current_epoch) / 86400 ))
echo "ssl_cert_days_until_expiry{domain=\"${domain}\"} ${days_until_expiry}"
Hugo团队实践:我们在Prometheus中配置了SSL证书过期告警,当证书剩余有效期小于14天时触发警告,小于7天时触发紧急告警。同时,Certbot的自动续期日志通过Filebeat收集到ELK,便于排查续期失败问题。
如果私钥泄露或证书信息有误,需要及时吊销证书。
# 检查证书是否被吊销
certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem
# 吊销后删除证书文件
certbot delete --cert-name example.com
证书透明度日志记录了所有颁发的证书,可用于监控未经授权的证书申请。
# 查询CT日志中的证书
curl -s "https://crt.sh/?q=%.example.com&output=json" | jq .
症状:部分客户端(尤其是Android旧版本)无法连接,SSL测试工具(如SSL Labs)评分降低。
解决:确保服务器配置使用fullchain.pem而非cert.pem。
# 检查证书链
openssl s_client -connect example.com:443 -showcerts
症状:浏览器提示"NET::ERR_CERT_COMMON_NAME_INVALID"。
原因:证书中的Subject Alternative Name (SAN)不包含访问的域名。
解决:重新申请证书,确保包含所有需要的域名(包括带www和不带www的版本)。
症状:Certbot提示"DNS problem: NXDOMAIN looking up TXT for _acme-challenge..."
排查步骤:
dig TXT _acme-challenge.example.com排查步骤:
# 查看续期日志
sudo cat /var/log/letsencrypt/letsencrypt.log
# 测试续期
sudo certbot renew --dry-run --verbose
# 检查证书文件权限
ls -la /etc/letsencrypt/live/example.com/
# 检查Nginx配置语法
sudo nginx -t
常见原因:
在零信任架构中,服务器也可以要求客户端提供证书进行身份验证。
server {
listen 443 ssl;
server_name api.internal.example.com;
ssl_certificate /etc/nginx/ssl/server-fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/server-privkey.pem;
# 启用客户端证书验证
ssl_verify_client on;
ssl_client_certificate /etc/nginx/ssl/ca-cert.pem;
ssl_trusted_certificate /etc/nginx/ssl/ca-cert.pem;
# 将客户端证书信息传递给后端
proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Client-I-DN $ssl_client_i_dn;
proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
location / {
proxy_pass http://backend;
}
}
Hugo项目案例:在支付系统的内部API网关中,我们使用mTLS确保只有持有有效客户端证书的服务能够调用敏感接口。每个微服务在部署时通过Vault获取唯一的客户端证书,实现服务间调用的身份认证。
TLS 1.3相比TLS 1.2有显著改进:
# 启用TLS 1.3(Nginx 1.13+ with OpenSSL 1.1.1+)
ssl_protocols TLSv1.2 TLSv1.3;
# 0-RTT会话恢复(谨慎使用,存在重放攻击风险)
ssl_early_data on;
HTTP/3基于QUIC协议,使用TLS 1.3加密,解决了TCP队头阻塞问题。
# Nginx 1.25+实验性支持
listen 443 quic reuseport;
listen 443 ssl;
# 告知客户端支持HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400' always;
| 工具 | 用途 | 链接 |
|---|---|---|
| SSL Labs | 全面SSL配置检测 | https://www.ssllabs.com/ssltest/ |
| SSL Checker | 快速证书信息查询 | https://www.sslchecker.com/ |
| Certbot | 官方ACME客户端 | https://certbot.eff.org/ |
| acme.sh | 轻量级ACME客户端 | https://github.com/acmesh-official/acme.sh |
| CRT.sh | 证书透明度查询 | https://crt.sh/ |
| Mozilla SSL Config | 推荐配置生成器 | https://ssl-config.mozilla.org/ |
Mozilla的SSL Configuration Generator可以根据服务器类型和兼容性需求生成最优配置。
Hugo团队工具:我们维护了一个内部脚本check-ssl.sh,用于批量检查所有生产域名的证书状态:
#!/bin/bash
# 批量检查域名证书过期时间
domains=(
"api.example.com"
"www.example.com"
"pay.example.com"
)
echo "域名 | 过期时间 | 剩余天数"
echo "-----|---------|--------"
for domain in "${domains[@]}"; do
expiry=$(echo | openssl s_client -connect ${domain}:443 -servername ${domain} 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
expiry_epoch=$(date -d "${expiry}" +%s)
days=$(( (expiry_epoch - $(date +%s)) / 86400 ))
echo "${domain} | ${expiry} | ${days}"
done
SSL证书是保障Web服务安全的基础组件。本指南从基础概念到高级实践,覆盖了证书申请、部署、运维的完整生命周期。
核心要点回顾:
fullchain.pem,避免客户端兼容性问题对于个人和小型项目,Let's Encrypt免费证书完全满足需求;对于企业级应用,根据安全要求选择适当的商业证书。无论选择哪种方案,遵循最佳实践配置都是保障服务安全的关键。