47天证书时代下的国密双证书自动化部署:基于 Nginx + acme.sh 的 SM2/RSA 方案实战
前言
2026 年 3 月 15 日,CA/Browser Forum 正式将 TLS 证书最长有效期从 398 天压缩至 200 天。这不是终点——2027 年 3 月将降至 100 天,2029 年 3 月最终定格在 47 天。
对于只需要管理 RSA 证书的团队来说,这已经是巨大的运维压力。但对于需要同时部署 SM2 国密证书 + RSA 国际证书 的企业(政府、金融、运营商),挑战是双倍的:
- 两套证书、两套私钥、两条证书链
- 两台服务器或两个端口需要分别配置
- 续期周期可能不同步(国密 CA 和国际 CA 的证书有效期不完全一致)
- Nginx 的双证书配置需要同时维护两套
ssl_certificate路径
- 使用
acme.sh从支持 SM2 的 CA 自动申请和续期双证书 - Nginx 配置 SM2/RSA 双证书自适应部署
- 证书自动reload 与服务零停机续期
- 证书到期监控与告警
环境准备
系统要求
- Linux(CentOS 7+ / Ubuntu 18.04+ / 统信 UOS / 麒麟 V10)
- Nginx 1.16+(推荐 Tengine 2.3+ 或基于 Tongsuo 的 Nginx 以获得完整国密支持)
- Python 3.8+(用于监控脚本)
国密 Nginx 环境
如果你使用的是标准 Nginx,需要编译支持国密的模块。推荐方案:
| 方案 | 适用场景 | 特点 |
|---|---|---|
| Tengine + Tongsuo | 生产环境 | 阿里维护,国密支持完善,活跃更新 |
| Nginx + GmSSL | 信创环境 | 纯国密协议栈,适合密评合规 |
| 标准 Nginx + 双证书 | 兼容优先 | 无需改造 Nginx,通过 SNI + 端口区分 |
acme.sh 的 curl 方式验证域名。安装 acme.sh
# 安装 acme.sh(支持 RFC 8555 ACME v2 协议)
curl https://get.acme.sh | sh -s email=admin@yourdomain.com
# 使 acme.sh 在 PATH 中可用
source ~/.bashrc
# 验证安装
acme.sh --version关于 acme.sh 的 CA 选择:Let's Encrypt 和 ZeroSSL 目前不支持签发 SM2 证书。对于国密证书,你需要选择支持 SM2 的 CA 机构,如:
- 沃通(WoSign)CA
- 亚洲诚信(TrustAsia)
- CFCA(中国金融认证中心)
第一步:申请 SM2 国密证书
生成 SM2 密钥对
# 安装 GmSSL(国密算法工具)
# Ubuntu
sudo apt-get install -y libgmssl-dev gmssl
# 或使用 pip 安装 Python 绑定
pip3 install gmssl >= 3.2.0
# 生成 SM2 密钥对(SM2 推荐曲线为 sm2p256v1)
gmssl ecparam -genkey -name sm2p256v1 -out /etc/ssl/private/sm2-key.pem
gmssl req -new -sm3 -key /etc/ssl/private/sm2-key.pem \
-out /etc/ssl/certs/sm2-csr.pem \
-subj "/C=CN/ST=Beijing/L=Beijing/O=Example Inc/CN=yourdomain.com"通过 acme.sh 申请 SM2 证书
# 设置 CA 为沃通(支持 SM2)
acme.sh --set-default-ca --server https://api.wosign.com/acme/directory
# DNS 验证方式申请(推荐自动化场景)
# 以 Cloudflare DNS 为例
export CF_Key="your-cloudflare-api-key"
export CF_Email="admin@yourdomain.com"
# 申请 SM2 证书
acme.sh --issue \
--server https://api.wosign.com/acme/directory \
--keylength ec-256 \
--curve sm2p256v1 \
--dns dns_cf \
-d yourdomain.com \
-d www.yourdomain.com \
--key-file /etc/ssl/private/sm2-key.pem \
--fullchain-file /etc/ssl/certs/sm2-fullchain.pem \
--reloadcmd "systemctl reload nginx"申请 RSA 证书(标准流程)
# RSA 证书使用标准 ACME 流程
acme.sh --issue \
--keylength 2048 \
--dns dns_cf \
-d yourdomain.com \
-d www.yourdomain.com \
--key-file /etc/ssl/private/rsa-key.pem \
--fullchain-file /etc/ssl/certs/rsa-fullchain.pem \
--reloadcmd "systemctl reload nginx"⚠️ 踩坑记录:acme.sh的早期版本不支持--curve sm2p256v1参数。如果你使用的 acme.sh 版本低于 3.0,需要手动修改源码或升级。验证方法:acme.sh --issue --help | grep curve,如果没有输出说明不支持 SM2 曲线参数。
第二步:Nginx 双证书配置
核心配置
Nginx 双证书部署的关键在于:通过 SNI(Server Name Indication)判断客户端能力,自动选择对应的证书。
对于标准 Nginx(非国密改造版),最实用的方案是:
- 443 端口:同时配置 SM2 和 RSA 证书,Nginx 会根据客户端的
signature_algorithms扩展自动选择 - 446 端口(可选):专用于国密 TLS(GMSSL),需要客户端显式连接
# 共享的 SSL 优化参数
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# 国密加密套件(仅在支持国密的客户端生效)
# ECDHE-SM2-WITH-SM4-SM3 是 GM/T 0024 定义的套件
# 国际套件作为降级选项
ssl_ciphers 'ECDHE-SM2-WITH-SM4-SM3:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:AES256-GCM-SHA384';
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;
# RSA 证书(默认,适配所有浏览器)
ssl_certificate /etc/ssl/certs/rsa-fullchain.pem;
ssl_certificate_key /etc/ssl/private/rsa-key.pem;
# SM2 国密证书(国密浏览器优先选择)
# 注意:标准 Nginx 不支持同时配置两个 ssl_certificate
# 需要使用 Tengine 的 dual_cert 指令,或通过 map 指令动态选择
# 见下方"方案 B:Tengine 双证书"
root /var/www/html;
location / {
try_files $uri $uri/ =404;
}
}方案 A:标准 Nginx(单证书 + 端口分流)
标准 Nginx 不支持在同一个 server 块中配置两个 ssl_certificate。变通方案:
# RSA 证书 — 443 端口(默认)
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/ssl/certs/rsa-fullchain.pem;
ssl_certificate_key /etc/ssl/private/rsa-key.pem;
# ... 其他配置
}
# SM2 证书 — 446 端口(国密专用)
# 需要客户端显式访问 https://yourdomain.com:446
# 或通过 301 重定向将国密浏览器引导至此端口
server {
listen 446 ssl;
server_name yourdomain.com;
ssl_certificate /etc/ssl/certs/sm2-fullchain.pem;
ssl_certificate_key /etc/ssl/private/sm2-key.pem;
# ... 与 443 端口相同配置
}此方案的局限性:国密浏览器需要访问 446 端口,用户体验不够透明。推荐使用方案 B。
方案 B:Tengine 双证书(推荐生产环境)
Tengine(阿里维护的 Nginx 分支)支持 ssl_certificate 配置多个证书,并能根据客户端能力自动选择:
# Tengine 双证书配置
server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;
# Tengine 支持配置多个 ssl_certificate
# 第一个为默认证书,后续为国密证书
ssl_certificate /etc/ssl/certs/rsa-fullchain.pem;
ssl_certificate_key /etc/ssl/private/rsa-key.pem;
# 国密证书(Tengine 扩展指令)
ssl_certificate /etc/ssl/certs/sm2-fullchain.pem;
ssl_certificate_key /etc/ssl/private/sm2-key.pem;
# 启用双证书自动选择
# Tengine 会根据客户端 ClientHello 中的 signature_algorithms 判断
# 如果客户端声明支持 SM2 且服务器配置了 SM2 证书,则使用 SM2
# 否则回退到 RSA
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# 加密套件配置(国密优先)
ssl_ciphers 'ECDHE-SM2-WITH-SM4-SM3:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
# 启用国密协议(需要 Tongsuo/OpenSSL 国密版)
# ssl_conf_command CipherString "ECDHE-SM2-WITH-SM4-SM3"
root /var/www/html;
location / {
try_files $uri $uri/ =404;
}
}⚠️ 踩坑记录:Tengine 的双证书功能需要底层的 OpenSSL 支持 SM2 算法。如果你使用的是标准 OpenSSL 1.1.1,它不支持 SM2。需要使用 Tongsuo(铜锁)或 GmSSL 作为底层加密库。验证方法:openssl ciphers -v | grep SM2,如果没有输出说明 OpenSSL 不支持 SM2。
方案 C:通过 map 动态选择证书(纯标准 Nginx)
如果你不能更换到 Tengine,可以用 Nginx 的 map 指令根据客户端 cipher 支持情况动态选择:
# 在 http {} 块中
map $ssl_client_cipher $ssl_certificate_file {
default /etc/ssl/certs/rsa-fullchain.pem;
~ECDHE-SM2 /etc/ssl/certs/sm2-fullchain.pem;
}
map $ssl_client_cipher $ssl_certificate_key_file {
default /etc/ssl/private/rsa-key.pem;
~ECDHE-SM2 /etc/ssl/private/sm2-key.pem;
}
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate $ssl_certificate_file;
ssl_certificate_key $ssl_certificate_key_file;
# ... 其他配置
}⚠️ 注意:此方案依赖 $ssl_client_cipher 变量,但该变量在 Nginx 1.11.6+ 才可用,且实际行为可能因 Nginx 版本不同而有差异。生产环境建议充分测试。
第三步:自动化续期脚本
acme.sh 自动续期
acme.sh 安装后会自动创建 cron 任务,每 60 天检查并续期即将过期的证书。验证:
# 查看 cron 任务
crontab -l | grep acme
# 预期输出类似:
# 0 0 * * * "/home/username/.acme.sh"/acme.sh --cron --home "/home/username/.acme.sh" > /dev/null续期后同步脚本
由于 SM2 和 RSA 证书可能分开续期,我们需要一个同步脚本确保 Nginx 总是加载最新的证书文件:
#!/bin/bash
# /usr/local/bin/sync-certs.sh
# 用途:将 acme.sh 签发的证书同步到 Nginx 配置目录
# 频率:每天执行一次(通过 cron)
set -euo pipefail
ACME_DIR="$HOME/.acme.sh"
NGINX_SSL_DIR="/etc/ssl"
DOMAINS=("yourdomain.com" "www.yourdomain.com")
LOG_FILE="/var/log/cert-sync.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
}
sync_cert() {
local domain=$1
local cert_type=$2 # rsa or sm2
local src_dir="$ACME_DIR/${domain}"
local dest_dir="$NGINX_SSL_DIR"
if [ "$cert_type" = "rsa" ]; then
local key_src="$src_dir/${domain}.key"
local cert_src="$src_dir/fullchain.cer"
local key_dest="$dest_dir/private/rsa-key.pem"
local cert_dest="$dest_dir/certs/rsa-fullchain.pem"
else
local key_src="$src_dir_sm2/${domain}.key"
local cert_src="$src_dir_sm2/fullchain.cer"
local key_dest="$dest_dir/private/sm2-key.pem"
local cert_dest="$dest_dir/certs/sm2-fullchain.pem"
fi
# 检查源文件是否存在
if [ ! -f "$key_src" ] || [ ! -f "$cert_src" ]; then
log "WARN: Source cert files not found for $domain ($cert_type)"
return 1
fi
# 检查文件是否已更新(比较 MD5)
local src_cert_md5=$(md5sum "$cert_src" | awk '{print $1}')
local dest_cert_md5=$(md5sum "$cert_dest" 2>/dev/null | awk '{print $1}' || echo "none")
if [ "$src_cert_md5" = "$dest_cert_md5" ]; then
log "INFO: No changes for $domain ($cert_type)"
return 0
fi
# 同步文件并设置权限
cp "$key_src" "$key_dest"
cp "$cert_src" "$cert_dest"
chmod 600 "$key_dest"
chmod 644 "$cert_dest"
chown root:root "$key_dest" "$cert_dest"
log "INFO: Synced $domain ($cert_type) - cert fingerprint changed"
# 测试 Nginx 配置并优雅重载
if nginx -t 2>/dev/null; then
systemctl reload nginx
log "INFO: Nginx reloaded successfully"
else
log "ERROR: Nginx config test failed! Not reloading."
# 回滚
exit 1
fi
}
# 同步所有域名
for domain in "${DOMAINS[@]}"; do
# 检查 RSA 证书
sync_cert "$domain" "rsa" || true
# 检查 SM2 证书(路径可能不同,取决于 CA)
sync_cert "$domain" "sm2" || true
done
# 清理过期证书(保留最近 2 个版本)
find "$NGINX_SSL_DIR/certs" -name "*.pem" -mtime +180 -delete 2>/dev/null || true
find "$NGINX_SSL_DIR/private" -name "*.pem" -mtime +180 -delete 2>/dev/null || true
log "INFO: Cert sync completed"设置 cron:
chmod +x /usr/local/bin/sync-certs.sh
# 每天凌晨 2:00 检查并同步
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/sync-certs.sh >> /var/log/cert-sync.log 2>&1") | crontab -第四步:Kubernetes 环境(cert-manager)
对于使用 Kubernetes 的团队,可以通过 cert-manager 实现 SM2/RSA 双证书的自动化管理。
安装 cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml创建 ClusterIssuer(ACME + DNS 验证)
# ClusterIssuer for RSA 证书
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-rsa
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@yourdomain.com
privateKeySecretRef:
name: letsencrypt-rsa-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
<hr>
# ClusterIssuer for SM2 证书(沃通 CA)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: wosign-sm2
spec:
acme:
server: https://api.wosign.com/acme/directory
email: admin@yourdomain.com
privateKeySecretRef:
name: wosign-sm2-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token创建双证书 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dual-cert-ingress
annotations:
# RSA 证书
cert-manager.io/cluster-issuer: "letsencrypt-rsa"
cert-manager.io/duration: "200d"
# SM2 证书(通过 additionalOutputFormats)
cert-manager.io/issuer: "wosign-sm2"
spec:
tls:
- hosts:
- yourdomain.com
secretName: rsa-tls-secret
- hosts:
- yourdomain.com
secretName: sm2-tls-secret
rules:
- host: yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: your-service
port:
number: 80⚠️ 踩坑记录:cert-manager 1.14+ 对 SM2 证书的支持仍在完善中。--curve sm2p256v1参数可能需要通过cert-manager.io/privateKeyAlgorithm: ECDSA+cert-manager.io/privateKeyEncoding: PKCS8配合实现。实际效果取决于 cert-manager 版本和底层 ACME CA 的支持程度。如果 cert-manager 不支持 SM2,建议继续使用 acme.sh 方案。
第五步:监控与告警
没有监控的自动化等于埋下定时炸弹。以下脚本检查所有证书的到期时间,并在阈值内发出告警:
#!/usr/bin/env python3
"""
证书到期监控脚本
检查 /etc/ssl/certs/ 下所有 PEM 证书的到期时间
支持 SMTP 邮件告警和 Webhook 告警
"""
import os
import ssl
import sys
import json
import socket
import smtplib
import logging
from datetime import datetime, timedelta
from email.mime.text import MIMEText
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.backends import default_backend
# 配置
CERT_DIRS = ["/etc/ssl/certs", "/etc/ssl/private"]
ALERT_DAYS = [60, 30, 14, 7, 3, 1] # 到期前告警天数
LOG_FILE = "/var/log/cert-monitor.log"
# 告警方式
SMTP_CONFIG = {
"enabled": False, # 设为 True 启用邮件告警
"host": "smtp.example.com",
"port": 587,
"user": "alert@example.com",
"password": "your-password",
"from": "alert@example.com",
"to": ["admin@example.com"]
}
WEBHOOK_URL = "" # 企业微信/钉钉 Webhook URL
# 证书标签映射(用于日志和告警)
CERT_LABELS = {
"rsa-fullchain.pem": "RSA 证书 (yourdomain.com)",
"sm2-fullchain.pem": "SM2 国密证书 (yourdomain.com)",
}
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def load_certificate(cert_path: str) -> x509.Certificate:
"""从 PEM 文件加载证书"""
with open(cert_path, 'rb') as f:
cert_data = f.read()
return x509.load_pem_x509_certificate(cert_data, default_backend())
def check_remote_cert(hostname: str, port: int = 443) -> dict:
"""检查远程服务器的证书"""
context = ssl.create_default_context()
try:
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert_bin = ssock.getpeercert(binary_form=True)
cert = x509.load_der_x509_certificate(cert_bin, default_backend())
return {
"hostname": hostname,
"port": port,
"subject": cert.subject.rfc4514_string(),
"issuer": cert.issuer.rfc4514_string(),
"not_after": cert.not_valid_after,
"days_left": (cert.not_valid_after - datetime.utcnow()).days
}
except Exception as e:
return {
"hostname": hostname,
"port": port,
"error": str(e)
}
def check_local_certs() -> list:
"""检查本地证书文件"""
results = []
for cert_dir in CERT_DIRS:
path = Path(cert_dir)
if not path.exists():
continue
for cert_file in path.glob("*.pem"):
if "private" in cert_dir and cert_file.name.endswith("-key.pem"):
continue # 跳过私钥文件
try:
cert = load_certificate(str(cert_file))
days_left = (cert.not_valid_after - datetime.utcnow()).days
label = CERT_LABELS.get(cert_file.name, cert_file.name)
results.append({
"label": label,
"file": str(cert_file),
"subject": cert.subject.rfc4514_string(),
"issuer": cert.issuer.rfc4514_string(),
"not_after": cert.not_valid_after.strftime("%Y-%m-%d %H:%M:%S UTC"),
"days_left": days_left
})
except Exception as e:
logger.error(f"无法解析证书 {cert_file}: {e}")
return results
def send_alert(message: str, level: str = "WARNING"):
"""发送告警通知"""
if SMTP_CONFIG["enabled"]:
send_email_alert(message, level)
if WEBHOOK_URL:
send_webhook_alert(message, level)
def send_email_alert(message: str, level: str):
"""发送邮件告警"""
try:
msg = MIMEText(message, 'plain', 'utf-8')
msg['Subject'] = f"[证书告警 {level}] 证书即将过期"
msg['From'] = SMTP_CONFIG["from"]
msg['To'] = ', '.join(SMTP_CONFIG["to"])
with smtplib.SMTP(SMTP_CONFIG["host"], SMTP_CONFIG["port"]) as server:
server.starttls()
server.login(SMTP_CONFIG["user"], SMTP_CONFIG["password"])
server.sendmail(SMTP_CONFIG["from"], SMTP_CONFIG["to"], msg.as_string())
logger.info("邮件告警发送成功")
except Exception as e:
logger.error(f"邮件告警发送失败: {e}")
def send_webhook_alert(message: str, level: str):
"""发送企业微信/钉钉 Webhook 告警"""
import urllib.request
# 企业微信格式
payload = {
"msgtype": "text",
"text": {
"content": f"[证书{level}]\n{message}"
}
}
try:
req = urllib.request.Request(
WEBHOOK_URL,
data=json.dumps(payload).encode('utf-8'),
headers={'Content-Type': 'application/json'}
)
urllib.request.urlopen(req, timeout=10)
logger.info("Webhook 告警发送成功")
except Exception as e:
logger.error(f"Webhook 告警发送失败: {e}")
def main():
"""主函数"""
logger.info("=" * 60)
logger.info("证书到期检查开始")
alerts = []
# 检查本地证书
local_results = check_local_certs()
for cert in local_results:
days = cert["days_left"]
logger.info(f" [{cert['label']}] 剩余 {days} 天 (到期: {cert['not_after']})")
# 检查是否需要告警
for alert_day in ALERT_DAYS:
if days <= alert_day:
alert_msg = (f"证书: {cert['label']}\n"
f"文件: {cert['file']}\n"
f"颁发者: {cert['issuer']}\n"
f"到期时间: {cert['not_after']}\n"
f"剩余天数: {days} 天")
alerts.append((alert_msg, "CRITICAL" if days <= 7 else "WARNING"))
break # 只触发最紧急的告警级别
# 检查远程证书
remote_checks = [
("yourdomain.com", 443, "yourdomain.com HTTPS"),
]
for hostname, port, label in remote_checks:
result = check_remote_cert(hostname, port)
if "error" in result:
logger.error(f" [{label}] 远程检查失败: {result['error']}")
alerts.append((f"远程证书检查失败: {label}\n错误: {result['error']}", "ERROR"))
else:
days = result["days_left"]
logger.info(f" [{label}] 远程剩余 {days} 天")
if days <= 30:
alerts.append((f"远程证书: {label}\n剩余天数: {days} 天", "WARNING"))
# 发送告警
if alerts:
full_message = "\n\n---\n\n".join([msg for msg, _ in alerts])
max_level = "CRITICAL" if any(l == "CRITICAL" for _, l in alerts) else "WARNING"
send_alert(full_message, max_level)
logger.warning(f"发现 {len(alerts)} 个告警")
else:
logger.info("所有证书状态正常")
logger.info("证书到期检查完成")
if __name__ == "__main__":
main()设置定时检查:
# 安装依赖
pip3 install cryptography
# 每天 9:00 检查证书到期情况
(crontab -l 2>/dev/null; echo "0 9 * * * /usr/bin/python3 /usr/local/bin/cert-monitor.py >> /var/log/cert-monitor.log 2>&1") | crontab -验证与测试
验证双证书是否正常工作
# 1. 验证 RSA 证书
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null | \
openssl x509 -noout -subject -issuer -dates
# 2. 验证 SM2 证书(如果使用 446 端口)
echo | gmssl s_client -connect yourdomain.com:446 -servername yourdomain.com 2>/dev/null | \
gmssl x509 -noout -subject -issuer -dates
# 3. 验证 TLS 握手(RSA)
openssl s_client -connect yourdomain.com:443 -tls1_2
# 4. 验证 TLS 握手(SM2,需要国密浏览器或 gmssl)
gmssl s_client -connect yourdomain.com:443 -sm2 -servername yourdomain.com模拟到期测试
在告警脚本中临时修改 ALERT_DAYS 为 [365],让即将到期的证书立即触发告警:
# 快速测试告警流程(不实际替换证书)
ALERT_DAYS=365 python3 /usr/local/bin/cert-monitor.py常见错误与排查
问题 1:acme.sh 申请 SM2 证书失败
现象:acme.sh --issue --keylength ec-256 --curve sm2p256v1 报错 unsupported curve
原因:acme.sh 底层调用的 openssl ecparam 不支持 SM2 曲线
解决:
# 检查 openssl 是否支持 SM2
openssl ecparam -list_curves | grep -i sm2
# 如果没有输出,需要安装支持 SM2 的 OpenSSL(如 Tongsuo/GmSSL)
# 或者使用 GmSSL 替代
gmssl ecparam -list_curves | grep SM2问题 2:Nginx 启动报错 SSL_CTX_use_certificate:ca md too weak
现象:Nginx 无法加载证书,报错证书签名哈希太弱
原因:证书使用 SM3 签名,而 Nginx 底层的 OpenSSL 版本对 SM3 的 OID 识别有问题
解决:升级到支持国密的 OpenSSL 版本(Tongsuo 1.1.1+ 或 GmSSL 3.0+)
问题 3:国密浏览器仍使用 RSA 证书
现象:国密浏览器(如 360 国密版)访问网站时仍使用 RSA 加密
原因:Nginx 的 ssl_ciphers 配置中国密套件优先级不够高,或者底层的 OpenSSL 不支持 SM2 密钥交换
解决:
# 确保国密套件在列表最前面
ssl_ciphers 'ECDHE-SM2-WITH-SM4-SM3:!ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;问题 4:续期后 Nginx 未加载新证书
现象:acme.sh --cron 成功续期,但网站仍使用旧证书
原因:
--reloadcmd未正确执行- 证书同步脚本有 bug
- Nginx 配置中证书路径指向了固定文件而非符号链接
# 验证证书是否已续期
openssl x509 -in ~/.acme.sh/yourdomain.com/fullchain.cer -noout -dates
# 验证 Nginx 使用的证书
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null | \
openssl x509 -noout -dates
# 如果两者不一致,手动重载 Nginx
nginx -t && systemctl reload nginx总结
证书有效期缩短至 47 天是不可逆转的行业趋势。对于需要同时维护 SM2 国密证书和 RSA 证书的企业,自动化是唯一可行的策略。
本文方案的四个核心要点:
- acme.sh + DNS 验证:实现双证书的自动申请和续期,零人工干预
- Nginx/Tengine 双证书配置:确保国密浏览器和主流浏览器都能正确加密
- 同步脚本 + Cron:证书续期后自动部署并优雅重载服务
- 监控告警:在证书到期前 60/30/14/7/3/1 天分级告警,杜绝意外过期
- 国密证书生态仍在发展中,不是所有 ACME CA 都支持 SM2 签发。建议提前确认你的 CA 是否支持
- Tengine 的双证书方案是目前生产环境最稳定的选择,标准 Nginx 方案适合测试和过渡
- 所有自动化脚本上线前,务必在非生产环境验证完整的续期→同步→重载链路
- 保留手动回滚能力:旧证书在过期前不要立即删除,以防新证书出问题时快速回退
参考来源
- DigiCert - TLS Certificate Lifetimes Will Officially Reduce to 47 Days
- Akamai - TLS 证书有效期缩短到 47 天,企业如何平稳过渡
- Keyfactor - What are 47-Day Certificates
- GlobalSign - 2026 证书新规落地:有效期缩短,自动化管理刻不容缓
- 天威诚信 - SSL证书有效期:2026年缩短至200天
- CFCA - SM2算法与RSA双加密方案实操
- Akamai - How to Automate Your Security Posture for the 47-Day Certificate Era
- Tengine + Tongsuo 国密使用教程
- RFC 8555 - Automatic Certificate Management Environment (ACME)
- GM/T 0024-2014 - SSL VPN 技术规范