一、概述
1.1 背景介绍
2018年Chrome开始把所有HTTP网站标记为"不安全",这个改变直接推动了整个互联网向HTTPS迁移。到2025年,HTTPS已经是网站的标配,没有HTTPS不仅影响用户信任,还会影响SEO排名,很多现代Web API(如地理定位、Service Worker)也必须在HTTPS环境下才能使用。
Let's Encrypt是这场变革的最大功臣。2015年它开始提供免费的SSL/TLS证书,彻底打破了证书费用的门槛。我还记得以前买个单域名证书要几百块,通配符证书要几千块,现在全都免费了。Let's Encrypt的证书有效期只有90天,但配合自动续期工具,根本不用操心。
我见过太多因为证书过期导致的事故了。有一次凌晨3点被叫醒,用户反馈网站打不开,一查是证书过期了。运维同学忘记续费,证书静悄悄地过期了,没有任何告警。那次之后我就把所有项目都迁到了Let's Encrypt,配合自动续期,再也没出过这种问题。
这篇文章会讲清楚HTTPS配置的方方面面:从基础的证书申请到生产级的安全配置,从手动部署到全自动化的续期方案。
1.2 技术特点
HTTPS工作原理
1. TCP三次握手
2. TLS握手:
- Client Hello: 客户端发送支持的加密套件、TLS版本
- Server Hello: 服务端选择加密套件,返回证书
- 密钥交换: 协商对称加密密钥
- 完成握手: 开始加密通信
3. 加密的HTTP通信
Let's Encrypt证书特点
| 特性 | 说明 |
|---|---|
| 价格 | 完全免费 |
| 有效期 | 90天 |
| 域名验证 | DV(Domain Validation)证书 |
| 验证方式 | HTTP-01、DNS-01、TLS-ALPN-01 |
| 支持通配符 | 是(需DNS验证) |
| 每周限制 | 单域名50张/周,子域名证书不限 |
| 兼容性 | 支持99%+的浏览器 |
Certbot工具
Certbot是Let's Encrypt官方推荐的证书管理工具,支持:
-
自动申请证书
-
自动配置Web服务器
-
自动续期
-
多种验证方式
1.3 适用场景
适合Let's Encrypt的场景
-
个人网站和博客
-
中小企业官网
-
开发和测试环境
-
API服务器
-
内部系统(需要HTTPS但不对外)
不适合Let's Encrypt的场景
-
需要EV(扩展验证)证书的金融网站
-
需要OV(组织验证)证书显示公司信息
-
证书有效期要求超过90天的场景
-
离线环境(无法自动续期)
1.4 环境要求
| 组件 | 版本要求 | 说明 |
|---|---|---|
| 操作系统 | Rocky Linux 9 / Ubuntu 24.04 LTS | 64位Linux |
| Nginx | 1.26.x / 1.27.x | 需支持TLS 1.3 |
| Certbot | 2.x | 最新版本 |
| OpenSSL | 3.x | TLS 1.3支持 |
| 域名 | 已解析到服务器IP | DNS生效 |
| 端口 | 80、443 | 防火墙已开放 |
二、详细步骤
2.1 准备工作
安装Certbot
Rocky Linux 9:
# 安装EPEL仓库
dnf install epel-release -y
# 安装Certbot和Nginx插件
dnf install certbot python3-certbot-nginx -y
# 验证安装
certbot --version
# certbot 2.x.x
Ubuntu 24.04:
# 安装Certbot
apt update
apt install certbot python3-certbot-nginx -y
# 验证安装
certbot --version
检查Nginx配置
# 确保Nginx已安装并运行
nginx -v
systemctl status nginx
# 检查配置语法
nginx -t
# 确认80端口可访问
curl -I http://your-domain.com
# 应该返回200或301/302
DNS检查
# 检查域名解析
dig +short your-domain.com
# 应该返回你的服务器IP
# 检查从互联网是否可访问
curl -I http://your-domain.com
防火墙配置
# Rocky Linux (firewalld)
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
# Ubuntu (ufw)
ufw allow 80/tcp
ufw allow 443/tcp
ufw reload
2.2 核心配置
方式一:Certbot自动配置(推荐新手)
# 自动申请证书并配置Nginx
certbot --nginx -d example.com -d www.example.com
# 交互过程:
# 1. 输入邮箱(用于紧急通知和续期提醒)
# 2. 同意服务条款
# 3. 选择是否重定向HTTP到HTTPS(推荐选择2)
# Certbot会自动:
# - 验证域名所有权
# - 申请证书
# - 配置Nginx
# - 设置自动续期
方式二:仅申请证书(推荐有经验的运维)
# 只申请证书,不修改Nginx配置
certbot certonly --nginx -d example.com -d www.example.com
# 证书存放位置:
# /etc/letsencrypt/live/example.com/fullchain.pem # 证书链
# /etc/letsencrypt/live/example.com/privkey.pem # 私钥
# /etc/letsencrypt/live/example.com/cert.pem # 证书
# /etc/letsencrypt/live/example.com/chain.pem # 中间证书
方式三:Webroot验证(适合不想停服务)
# 先在Nginx配置验证目录
# /etc/nginx/conf.d/example.conf
server {
listen 80;
server_name example.com www.example.com;
# Let's Encrypt验证目录
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# 创建验证目录
mkdir -p /var/www/certbot
nginx -s reload
# 使用webroot方式申请
certbot certonly --webroot -w /var/www/certbot -d example.com -d www.example.com
方式四:DNS验证(申请通配符证书)
# 通配符证书必须使用DNS验证
certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com
# 过程中会要求添加DNS TXT记录
# _acme-challenge.example.com TXT "随机字符串"
# 添加后等待DNS生效,再继续
# 自动化DNS验证(以阿里云为例,需要安装插件)
pip install certbot-dns-aliyun
certbot certonly --authenticator dns-aliyun \
--dns-aliyun-credentials /etc/letsencrypt/aliyun.ini \
-d "*.example.com" -d example.com
生产级Nginx HTTPS配置
# /etc/nginx/conf.d/example-ssl.conf
# 生产环境HTTPS完整配置
# HTTP重定向到HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Let's Encrypt验证(保留)
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# 其他请求重定向到HTTPS
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS服务
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # Nginx 1.25.1+的新语法
server_name example.com www.example.com;
# ==================== SSL证书配置 ====================
# Let's Encrypt证书
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 可信CA(用于OCSP Stapling)
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# ==================== SSL协议配置 ====================
# 只启用TLS 1.2和1.3
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会话配置 ====================
# 会话缓存(提高性能)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# 禁用会话票证(更安全,但影响0-RTT)
ssl_session_tickets off;
# ==================== OCSP Stapling ====================
# 启用OCSP Stapling(减少客户端验证时间)
ssl_stapling on;
ssl_stapling_verify on;
# DNS解析器(用于OCSP)
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# ==================== DH参数 ====================
# 自定义DH参数(更安全)
# 生成命令: openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
# ==================== 安全头配置 ====================
# HSTS(HTTP严格传输安全)
# max-age=31536000 表示一年
# includeSubDomains 包含所有子域名
# preload 可提交到浏览器预加载列表
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 防止点击劫持
add_header X-Frame-Options "SAMEORIGIN" always;
# 防止MIME类型嗅探
add_header X-Content-Type-Options "nosniff" always;
# XSS过滤
add_header X-XSS-Protection "1; mode=block" always;
# 引用策略
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 内容安全策略(根据实际需求调整)
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;
# ==================== 站点配置 ====================
root /var/www/html;
index index.html;
# 日志
access_log /var/log/nginx/example-ssl.access.log;
error_log /var/log/nginx/example-ssl.error.log;
# 站点内容
location / {
try_files $uri $uri/ =404;
}
# 其他配置...
}
生成DH参数
# 创建目录
mkdir -p /etc/nginx/ssl
# 生成DH参数(这个过程比较慢,可能需要几分钟)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
# 设置权限
chmod 600 /etc/nginx/ssl/dhparam.pem
2.3 启动和验证
验证配置
# 检查Nginx配置
nginx -t
# 重载配置
nginx -s reload
# 检查监听端口
ss -tlnp | grep nginx
# 应该看到80和443端口
测试HTTPS
# 命令行测试
curl -I https://example.com
# HTTP/2 200
# strict-transport-security: max-age=31536000; includeSubDomains; preload
# 检查证书信息
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
# notBefore=Jan 7 00:00:00 2025 GMT
# notAfter=Apr 7 23:59:59 2025 GMT
# 检查证书链
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -issuer
在线检测工具
-
SSL Labs: https://www.ssllabs.com/ssltest/
-
Mozilla Observatory: https://observatory.mozilla.org/
-
Security Headers: https://securityheaders.com/
目标是在SSL Labs获得A+评分。
三、示例代码和配置
3.1 完整配置示例
多站点HTTPS配置
# /etc/nginx/conf.d/ssl-params.conf
# 通用SSL参数(被其他配置include)
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: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_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
# /etc/nginx/conf.d/security-headers.conf
# 通用安全头
add_header Strict-Transport-Security "max-age=31536000; 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;
# /etc/nginx/conf.d/www.example.com.conf
# 主站配置
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://www.example.com$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
include /etc/nginx/conf.d/ssl-params.conf;
# 裸域名重定向到www
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
include /etc/nginx/conf.d/ssl-params.conf;
include /etc/nginx/conf.d/security-headers.conf;
root /var/www/www.example.com;
index index.html;
access_log /var/log/nginx/www.example.com.access.log;
error_log /var/log/nginx/www.example.com.error.log;
location / {
try_files $uri $uri/ =404;
}
}
# /etc/nginx/conf.d/api.example.com.conf
# API站点配置
server {
listen 80;
listen [::]:80;
server_name api.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
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_trusted_certificate /etc/letsencrypt/live/api.example.com/chain.pem;
include /etc/nginx/conf.d/ssl-params.conf;
include /etc/nginx/conf.d/security-headers.conf;
# API特殊配置
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
通配符证书配置
# /etc/nginx/conf.d/wildcard.example.com.conf
# 通配符证书配置(一个证书服务所有子域名)
# 动态子域名处理
server {
listen 80;
listen [::]:80;
server_name *.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name *.example.com;
# 通配符证书
ssl_certificate /etc/letsencrypt/live/example.com-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com-wildcard/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com-wildcard/chain.pem;
include /etc/nginx/conf.d/ssl-params.conf;
include /etc/nginx/conf.d/security-headers.conf;
# 根据子域名路由
set $subdomain "";
if ($host ~* ^([^.]+)\.example\.com$) {
set $subdomain $1;
}
# 不同子域名不同root
root /var/www/subdomains/$subdomain;
location / {
try_files $uri $uri/ =404;
}
}
3.2 实际应用案例
案例一:自动续期配置
# Certbot自动续期原理:
# 安装时自动创建了systemd timer或cron job
# 每天检查两次,证书有效期<30天时自动续期
# 查看systemd timer(推荐方式)
systemctl list-timers | grep certbot
# certbot.timer
# 查看timer配置
systemctl cat certbot.timer
# 手动测试续期(dry-run)
certbot renew --dry-run
# 查看证书状态
certbot certificates
自定义续期后操作(reload Nginx)
# 方式一:使用deploy-hook
# 创建hook脚本
cat > /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh << 'EOF'
#!/bin/bash
# 证书续期后重载Nginx
nginx -t && systemctl reload nginx
echo "$(date): Nginx reloaded after cert renewal" >> /var/log/letsencrypt/renewal.log
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/nginx-reload.sh
# 方式二:在certbot命令中指定
certbot renew --deploy-hook "nginx -s reload"
案例二:证书监控和告警
#!/bin/bash
# /usr/local/bin/check_ssl_expiry.sh
# SSL证书过期检查和告警脚本
# 配置
DOMAINS=("example.com" "api.example.com" "www.example.com")
WARN_DAYS=30 # 提前30天告警
ALERT_EMAIL="ops@example.com"
SLACK_WEBHOOK="https://hooks.slack.com/services/xxx/yyy/zzz"
check_cert() {
local domain=$1
local port=${2:-443}
# 获取证书过期时间
local expiry_date=$(echo | openssl s_client -connect ${domain}:${port} -servername ${domain} 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$expiry_date" ]; then
echo "ERROR: Failed to get certificate for $domain"
return 1
fi
# 转换为时间戳
local expiry_epoch=$(date -d "$expiry_date" +%s)
local now_epoch=$(date +%s)
local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
echo "$domain: $days_left days left (expires: $expiry_date)"
if [ $days_left -lt $WARN_DAYS ]; then
return 2 # 需要告警
fi
return 0
}
send_alert() {
local message=$1
# 发送邮件
echo "$message" | mail -s "SSL Certificate Alert" $ALERT_EMAIL
# 发送Slack通知
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\": \"$message\"}" \
$SLACK_WEBHOOK
}
# 主逻辑
echo "=== SSL Certificate Expiry Check $(date) ==="
alerts=""
for domain in "${DOMAINS[@]}"; do
result=$(check_cert $domain)
status=$?
echo "$result"
if [ $status -eq 2 ]; then
alerts="$alerts\n$result"
elif [ $status -eq 1 ]; then
alerts="$alerts\nERROR: Cannot check $domain"
fi
done
if [ -n "$alerts" ]; then
send_alert "SSL Certificate Warning:$alerts"
fi
# 添加定时任务,每天检查
crontab -e
0 9 * * * /usr/local/bin/check_ssl_expiry.sh >> /var/log/ssl-check.log 2>&1
案例三:多服务器证书同步
#!/bin/bash
# /usr/local/bin/sync_certs.sh
# 主服务器续期后,同步证书到其他服务器
CERT_PATH="/etc/letsencrypt/live"
REMOTE_SERVERS=("10.0.1.2" "10.0.1.3" "10.0.1.4")
REMOTE_USER="deploy"
DOMAINS=("example.com" "api.example.com")
for domain in "${DOMAINS[@]}"; do
echo "Syncing certificates for $domain..."
for server in "${REMOTE_SERVERS[@]}"; do
echo " -> $server"
# 同步证书文件
rsync -avz --delete \
$CERT_PATH/$domain/ \
$REMOTE_USER@$server:$CERT_PATH/$domain/
# 重载远程Nginx
ssh $REMOTE_USER@$server "sudo nginx -t && sudo systemctl reload nginx"
done
done
echo "Certificate sync completed at $(date)"
# 在主服务器的deploy-hook中调用
cat > /etc/letsencrypt/renewal-hooks/deploy/sync-and-reload.sh << 'EOF'
#!/bin/bash
# 先重载本地Nginx
nginx -t && systemctl reload nginx
# 同步到其他服务器
/usr/local/bin/sync_certs.sh >> /var/log/cert-sync.log 2>&1
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/sync-and-reload.sh
案例四:Docker环境下的HTTPS配置
# docker-compose.yml
version: '3.8'
services:
nginx:
image: nginx:1.27
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./certbot/www:/var/www/certbot:ro
- ./certbot/conf:/etc/letsencrypt:ro
depends_on:
- app
networks:
- web
certbot:
image: certbot/certbot:latest
container_name: certbot
volumes:
- ./certbot/www:/var/www/certbot
- ./certbot/conf:/etc/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- web
app:
image: your-app:latest
container_name: app
networks:
- web
networks:
web:
driver: bridge
# 首次申请证书(Docker环境)
docker compose run --rm certbot certonly \
--webroot \
--webroot-path /var/www/certbot \
-d example.com \
-d www.example.com \
--email your@email.com \
--agree-tos \
--no-eff-email
# 重启Nginx加载证书
docker compose restart nginx
四、最佳实践和注意事项
4.1 最佳实践
1. 始终使用fullchain.pem
# 正确:使用完整证书链
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
# 错误:只用证书,没有中间证书
# ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
# 会导致某些客户端验证失败
2. 启用HSTS并提交Preload
# HSTS配置
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 提交到HSTS Preload列表:https://hstspreload.org/
# 提交后浏览器会硬编码你的域名必须使用HTTPS
# 注意:提交后很难撤销,确保所有子域名都支持HTTPS
3. 禁用旧版TLS协议
# 只启用TLS 1.2和1.3
ssl_protocols TLSv1.2 TLSv1.3;
# 不要使用:
# ssl_protocols SSLv3 TLSv1 TLSv1.1;
# 这些协议已有已知漏洞
4. 正确配置OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# 验证OCSP Stapling是否生效
echo | openssl s_client -connect example.com:443 -status 2>/dev/null | grep -A 17 "OCSP Response Status"
# 应该显示: OCSP Response Status: successful
5. 证书透明度(Certificate Transparency)
Let's Encrypt证书自动包含SCT(Signed Certificate Timestamp),符合Chrome的CT要求,不需要额外配置。
6. 多证书备份
# 备份证书(虽然可以随时重新申请,但备份更安全)
tar -czf letsencrypt-backup-$(date +%Y%m%d).tar.gz \
/etc/letsencrypt/live \
/etc/letsencrypt/archive \
/etc/letsencrypt/renewal
4.2 注意事项
常见错误表
| 错误 | 原因 | 解决方案 |
|---|---|---|
| Challenge failed | 验证目录不可访问 | 检查防火墙、Nginx配置 |
| Too many requests | 达到速率限制 | 等待1小时后重试 |
| DNS problem | 域名解析错误 | 检查DNS配置 |
| Certificate not trusted | 没用fullchain | 使用fullchain.pem |
| ERR_SSL_PROTOCOL_ERROR | TLS配置错误 | 检查ssl_protocols |
| HSTS导致无法访问 | HTTP子域名被强制HTTPS | 确保所有子域名都支持HTTPS |
速率限制注意
# Let's Encrypt速率限制:
# - 每个注册域名50张证书/周
# - 重复证书5张/周
# - 失败验证5次/小时/账户/主机名
# - 每3小时10个pending authorizations
# 测试时使用staging环境(无速率限制)
certbot certonly --staging -d test.example.com
# 测试成功后再用生产环境
certbot certonly -d test.example.com
安全注意事项
# 1. 保护私钥
chmod 600 /etc/letsencrypt/live/*/privkey.pem
chown root:root /etc/letsencrypt/live/*/privkey.pem
# 2. 不要把证书提交到Git
echo "*.pem" >> .gitignore
# 3. 定期检查证书状态
certbot certificates
# 4. 监控证书过期
# 配置告警脚本(见案例二)
兼容性考虑
# 如果需要支持老旧客户端(不推荐)
# 可以添加TLS 1.0和1.1支持
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
# 添加更多加密套件
# ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:!DSS';
五、故障排查和监控
5.1 故障排查
问题一:证书申请失败
# 查看详细错误
certbot certonly --nginx -d example.com -v
# 常见原因和解决方案:
# 1. HTTP验证失败
# 检查80端口是否可访问
curl -I http://example.com/.well-known/acme-challenge/test
# 2. DNS解析问题
dig +short example.com
# 确保解析到正确的服务器IP
# 3. 防火墙问题
# Rocky Linux
firewall-cmd --list-all
# Ubuntu
ufw status
# 4. Nginx配置问题
nginx -T | grep -A 20 "location /.well-known"
问题二:证书不被信任
# 1. 检查证书链是否完整
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -text | grep -A 2 "Certificate chain"
# 2. 检查是否使用fullchain.pem
nginx -T | grep ssl_certificate
# 3. 检查中间证书
curl https://example.com -v 2>&1 | grep "SSL certificate problem"
# 4. 在线检测
# https://www.ssllabs.com/ssltest/
问题三:HTTPS连接失败
# 1. 检查端口监听
ss -tlnp | grep 443
# 2. 测试TLS连接
openssl s_client -connect example.com:443 -servername example.com
# 3. 检查加密套件
nmap --script ssl-enum-ciphers -p 443 example.com
# 4. 查看Nginx错误日志
tail -f /var/log/nginx/error.log
问题四:自动续期失败
# 1. 手动测试续期
certbot renew --dry-run
# 2. 查看Certbot日志
cat /var/log/letsencrypt/letsencrypt.log
# 3. 检查systemd timer
systemctl status certbot.timer
journalctl -u certbot
# 4. 检查权限
ls -la /etc/letsencrypt/
# 确保certbot有写入权限
# 5. 检查磁盘空间
df -h /etc/letsencrypt
调试脚本
#!/bin/bash
# debug_ssl.sh - SSL配置调试脚本
DOMAIN=$1
if [ -z "$DOMAIN" ]; then
echo "Usage: $0 <domain>"
exit 1
fi
echo "=== SSL Debug for $DOMAIN ==="
echo ""
echo "--- DNS Resolution ---"
dig +short $DOMAIN
echo ""
echo "--- Port Check ---"
nc -zv $DOMAIN 443 2>&1
echo ""
echo "--- Certificate Info ---"
echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -dates -subject -issuer
echo ""
echo "--- Certificate Chain ---"
echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | grep -E "(Certificate chain|^\s*[0-9]+ s:|depth=)"
echo ""
echo "--- TLS Version ---"
for v in tls1 tls1_1 tls1_2 tls1_3; do
result=$(echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN -$v 2>&1 | grep "Protocol")
echo "$v: $result"
done
echo ""
echo "--- OCSP Stapling ---"
echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN -status 2>/dev/null | grep "OCSP Response Status"
echo ""
echo "--- Security Headers ---"
curl -sI https://$DOMAIN | grep -iE "(strict-transport|x-frame|x-content-type|x-xss|referrer-policy)"
5.2 性能监控
TLS握手时间监控
# 测试TLS握手时间
curl -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nTotal: %{time_total}s\n" -o /dev/null -s https://example.com
# 期望值:
# TLS握手 < 100ms (同城)
# TLS握手 < 300ms (跨地域)
证书监控Prometheus指标
# prometheus.yml
scrape_configs:
- job_name: 'ssl_exporter'
static_configs:
- targets:
- example.com:443
- api.example.com:443
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: ssl_exporter:9219
Grafana告警规则
# 证书过期告警
- alert: SSLCertExpiringSoon
expr: ssl_cert_not_after - time() < 86400 * 14 # 14天内过期
for: 1h
labels:
severity: warning
annotations:
summary: "SSL certificate expiring soon"
description: "Certificate for {{ $labels.instance }} expires in {{ $value | humanizeDuration }}"
- alert: SSLCertExpired
expr: ssl_cert_not_after < time()
for: 0m
labels:
severity: critical
annotations:
summary: "SSL certificate expired"
description: "Certificate for {{ $labels.instance }} has expired"
5.3 备份与恢复
完整备份脚本
#!/bin/bash
# backup_letsencrypt.sh
BACKUP_DIR="/backup/letsencrypt"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/letsencrypt_$DATE.tar.gz"
mkdir -p $BACKUP_DIR
# 备份Let's Encrypt目录
tar -czf $BACKUP_FILE \
/etc/letsencrypt \
/etc/nginx/conf.d/*ssl*.conf \
/etc/nginx/ssl
# 记录证书信息
certbot certificates > $BACKUP_DIR/certificates_$DATE.txt
# 保留30天
find $BACKUP_DIR -name "letsencrypt_*.tar.gz" -mtime +30 -delete
find $BACKUP_DIR -name "certificates_*.txt" -mtime +30 -delete
echo "Backup completed: $BACKUP_FILE"
ls -lh $BACKUP_FILE
恢复流程
#!/bin/bash
# restore_letsencrypt.sh
BACKUP_FILE=$1
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 <backup_file>"
exit 1
fi
# 停止Nginx
systemctl stop nginx
# 备份当前配置
mv /etc/letsencrypt /etc/letsencrypt.old.$(date +%Y%m%d)
# 恢复备份
tar -xzf $BACKUP_FILE -C /
# 检查配置
nginx -t
if [ $? -ne 0 ]; then
echo "Config test failed!"
exit 1
fi
# 启动Nginx
systemctl start nginx
# 验证证书
certbot certificates
echo "Restore completed"
六、总结
6.1 技术要点回顾
HTTPS配置的核心要点:
-
证书管理:使用Let's Encrypt免费证书,配置自动续期
-
安全配置:只用TLS 1.2+,启用HSTS,配置安全头
-
性能优化:OCSP Stapling,会话缓存,HTTP/2
-
监控告警:证书过期监控,TLS握手时间监控
关键配置清单:
# 必备配置
ssl_certificate fullchain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_stapling on;
add_header Strict-Transport-Security "max-age=31536000";
# 自动续期
certbot renew --deploy-hook "nginx -s reload"
6.2 参考资料
-
Let's Encrypt官网:https://letsencrypt.org/
-
Certbot文档:https://eff-certbot.readthedocs.io/
-
Mozilla SSL配置生成器:https://ssl-config.mozilla.org/
-
SSL Labs测试:https://www.ssllabs.com/ssltest/
-
HSTS Preload:https://hstspreload.org/
附录
A. 命令速查表
| 命令 | 说明 |
|---|---|
certbot --nginx -d domain |
自动申请并配置 |
certbot certonly --webroot |
只申请证书 |
certbot certificates |
查看证书状态 |
certbot renew |
续期所有证书 |
certbot renew --dry-run |
测试续期 |
certbot revoke --cert-path |
吊销证书 |
certbot delete --cert-name |
删除证书 |
openssl s_client -connect host:443 |
测试SSL连接 |
openssl x509 -noout -dates |
查看证书有效期 |
B. 配置参数详解
| 参数 | 推荐值 | 说明 |
|---|---|---|
| ssl_protocols | TLSv1.2 TLSv1.3 | 支持的TLS版本 |
| ssl_ciphers | Mozilla Intermediate | 加密套件 |
| ssl_prefer_server_ciphers | off | TLS 1.3下应关闭 |
| ssl_session_cache | shared:SSL:10m | 会话缓存 |
| ssl_session_timeout | 1d | 会话超时 |
| ssl_session_tickets | off | 更安全但影响性能 |
| ssl_stapling | on | OCSP Stapling |
| ssl_stapling_verify | on | 验证OCSP响应 |
| ssl_dhparam | 2048位 | DH参数 |
C. 术语表
| 术语 | 解释 |
|---|---|
| Let's Encrypt | 免费的证书颁发机构(CA) |
| Certbot | Let's Encrypt官方客户端工具 |
| ACME | 自动证书管理协议 |
| DV证书 | 域名验证证书,只验证域名所有权 |
| HSTS | HTTP严格传输安全 |
| OCSP | 在线证书状态协议 |
| OCSP Stapling | 服务器代替客户端查询证书状态 |
| CT | 证书透明度,防止错误签发 |
| SNI | 服务器名称指示,同IP多证书 |
| TLS 1.3 | 最新TLS版本,更快更安全 |
| fullchain.pem | 包含证书和中间证书的完整链 |
| privkey.pem | 私钥文件 |