如何剩下一笔 SSL(HTTPS)证书的钱?
前言
做过网站运维的同学都知道,给域名上 HTTPS 是刚需。但每年买 DV、OV、EV SSL 证书少则几百,多则几千,多域名、通配符证书更贵。其实,对于绝大多数中小型网站、个人博客、企业官网来说,完全可以用 Let's Encrypt 免费证书替代商业证书,一年能省下几百到几千块。
本文分享一套我自己在生产环境落地的方案:Python + acme.sh + 阿里云 DNS + 飞书告警,实现通配符证书的自动申请、自动续期、自动部署、每日监控。
一、为什么要用 Let's Encrypt?
| 对比项 | 商业 DV 证书 | Let's Encrypt |
|---|---|---|
| 价格 | 300~2000/年/域名 | 免费 |
| 通配符 | 800~5000/年 | 免费 |
| 有效期 | 1~2 年 | 90 天 |
| 自动续期 | 多数不支持 | 原生支持 |
| 浏览器信任 | 99% 信任 | 99% 信任 |
| 适用场景 | 金融/政府/强品牌 | 网站/API/小程序 |
结论:只要不是必须使用 OV/EV 证书的场景,Let's Encrypt 完全够用。唯一麻烦的是每 90 天要续期,但这个问题可以通过自动化脚本彻底解决。
二、方案架构
┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐
│ ACME 客户端 │────▶│ DNS 服务商 │────▶│ CA(Let's Encrypt)│
│ (acme.sh) │ │ (阿里云 DNS) │ │ │
└─────────────────┘ └─────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ 证书文件存储 │────▶│ 自动复制到 Nginx │
│ (/data/certs) │ │ + nginx -s reload│
└─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 飞书每日报告 │
│ 本地+远程监控 │
└─────────────────┘
核心流程:
acme.sh通过 DNS-01 挑战申请*.example.com通配符证书。- DNS 解析在阿里云,通过 RAM 用户的 AccessKey 自动添加 TXT 记录验证域名。
- 证书到期前 30 天自动续期。
- 续期成功后自动复制
fullchain.cer和.key到/etc/nginx/cert/。 - 执行
nginx -s reload使新证书生效。 - 每天 9 点发送飞书报告,显示每个域名的本地证书和远程 443 端口状态。
三、关键代码实现
1. 配置结构(config.yaml)
yaml
acme:
default_ca: letsencrypt
acme_sh_path: "/root/.acme.sh/acme.sh"
certs_dir: "/data/ssl-auto-manager/certs"
dns_hook: "dns_ali"
env:
Ali_Key: "LTAIxxxxxxxxxxxx"
Ali_Secret: "xxxxxxxxxxxxxxxx"
renew_before_days: 30
domains:
- domain: "example.com"
auto_issue: true
san:
- "example.com"
- "*.example.com"
alert_days: 30
# 仅监控,不自动申请
- domain: "oa.example.com"
auto_issue: false
san:
- "oa.example.com"
alert_days: 30
lark:
webhook_url: "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx"
schedule:
cron: "0 9 * * *"
timezone: "Asia/Shanghai"
deploy:
enabled: true
target_dir: "/etc/nginx/cert"
reload_cmd: "nginx -s reload"
2. 证书申请/续期核心逻辑
python
class AcmeManager(object):
def __init__(self, config):
self.config = config
def issue(self, domain, san, dns_hook=None, env=None):
cmd = [
self.config.acme_sh_path,
"--issue",
"--cert-home", self.config.certs_dir,
"--dns", dns_hook or self.config.dns_hook,
"-d", domain,
]
for s in san:
cmd.extend(["-d", s])
env_vars = os.environ.copy()
env_vars.update(self.config.acme_env)
env_vars.update(env or {})
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
env=env_vars,
)
success = result.returncode == 0
if success:
self.deploy(domain)
return {"domain": domain, "action": "issue", "success": success}
def renew(self, domain):
cmd = [
self.config.acme_sh_path,
"--renew",
"--force",
"--cert-home", self.config.certs_dir,
"-d", domain,
]
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
success = result.returncode == 0
if success:
self.deploy(domain)
return {"domain": domain, "action": "renew", "success": success}
3. 自动部署到 Nginx
python
def deploy(self, domain):
if not self.config.deploy_enabled:
return None
cert_dir = self._domain_cert_dir(domain)
if not cert_dir:
logger.error("证书目录不存在: %s", domain)
return None
src_key = os.path.join(cert_dir, domain + ".key")
src_fullchain = os.path.join(cert_dir, "fullchain.cer")
target_dir = self.config.deploy_target_dir
target_key = os.path.join(target_dir, domain + ".key")
target_pem = os.path.join(target_dir, domain + ".pem")
shutil.copy2(src_key, target_key)
shutil.copy2(src_fullchain, target_pem)
os.chmod(target_key, 0o600)
os.chmod(target_pem, 0o644)
subprocess.run(
self.config.deploy_reload_cmd.split(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
logger.info("证书已部署: %s -> %s", domain, target_dir)
return {"domain": domain, "action": "deploy", "success": True}
4. 本地证书监控
python
def get_cert_days_left(cert_path):
with open(cert_path, "rb") as f:
cert = x509.load_pem_x509_certificate(f.read(), default_backend())
not_after = cert.not_valid_after
if not_after.tzinfo is None:
not_after = not_after.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
return (not_after - now).days
5. 远程 443 端口监控
python
def check_remote(domain, port=443, timeout=10):
try:
ctx = ssl.create_default_context()
with socket.create_connection((domain, port), timeout=timeout) as sock:
with ctx.wrap_socket(sock, server_hostname=domain) as ssock:
cert = ssock.getpeercert()
expire = cert["notAfter"]
expire_dt = datetime.strptime(expire, "%b %d %H:%M:%S %Y %Z")
expire_dt = expire_dt.replace(tzinfo=timezone.utc)
days_left = (expire_dt - datetime.now(timezone.utc)).days
return {"ok": True, "days_left": days_left, "expire": expire}
except Exception as e:
return {"ok": False, "error": str(e)}
6. 调度入口
python
def run_scheduler(config_path):
config = Config(config_path)
cron = config.schedule_cron # "0 9 * * *"
hour, minute = _parse_cron_time(cron)
schedule.every().day.at("{:02d}:{:02d}".format(hour, minute)).do(
run_once, config_path
)
logger.info("调度器已启动,cron=%s,执行时间=%02d:%02d", cron, hour, minute)
while True:
schedule.run_pending()
time.sleep(60)
def _parse_cron_time(cron):
parts = cron.split()
return int(parts[1]), int(parts[0])
7. systemd 服务文件
ssl-auto-manager.service:
ini
[Unit]
Description=SSL Auto Manager
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/data/ssl-auto-manager
ExecStart=/data/ssl-auto-manager/venv/bin/python /data/ssl-auto-manager/main.py --mode daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
启动命令:
bash
sudo cp ssl-auto-manager.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable ssl-auto-manager
sudo systemctl start ssl-auto-manager
四、踩过的坑
1. Python 3.5 兼容问题
服务器如果是 Ubuntu 16.04,默认 Python 3.5,很多新语法不能用:
- 不能用
subprocess.run(text=True),要用universal_newlines=True - 不能用
dataclasses pathlib行为和老版本有差异cryptography新版本不再支持 Python 3.5
解决:锁定依赖版本 + 代码降级。
2. 时区问题
cryptography 在 Python 3.5 下返回的 not_valid_after 是 naive datetime,和 datetime.now(timezone.utc) 相减会报错。
解决 :判断是否为 naive,统一加上 tzinfo=timezone.utc。
3. acme.sh 的 _ecc 后缀
ECC 证书默认会生成 example.com_ecc 目录,代码里找证书时要兼容:
python
for suffix in ["", "_ecc"]:
cert_dir = os.path.join(certs_dir, domain + suffix)
if os.path.exists(cert_dir):
return cert_dir
4. 证书链不完整导致浏览器报不安全
Nginx 必须配置 fullchain.cer(域名证书 + 中间证书),不能只配 .cer。否则 SSL Checker 会报 Incomplete Certificate Chain。
5. 飞书卡片格式
飞书交互卡片的字段校验很严格,比如 div 不能乱加 extra,markdown 文本要注意换行符等。建议先用最简单的结构调试通过,再美化。
五、效果
运行一段时间后,飞书每天会收到这样的报告:
- 域名、本地证书状态、远程 443 状态
- 剩余天数、过期时间
- 自动 issue / renew / deploy 操作记录
一旦某个域名快过期或远程异常,脚本会自动重新申请并部署,完全不需要人工介入。
六、适用场景总结
| 场景 | 是否推荐 Let's Encrypt |
|---|---|
| 个人博客/网站 | ✅ 强烈推荐 |
| 企业官网/API | ✅ 推荐 |
| 微信小程序/APP 后端 | ✅ 推荐 |
| 需要显示绿色企业名的 EV 证书 | ❌ 必须买商业证书 |
| 政府/银行合规要求 | ❌ 按合规要求采购 |
七、写在最后
SSL 证书的本质是加密传输 + 身份验证。对于大多数应用来说,Let's Encrypt 提供的加密强度已经足够了。把买证书的钱省下来,投入到代码自动化和监控告警上,才是更划算的选择。
如果你也在用 Nginx + 多域名 + 通配符证书的场景,不妨试试这套方案。有任何问题欢迎在评论区交流。
关键词:SSL 证书、HTTPS、Let's Encrypt、免费证书、acme.sh、通配符证书、自动续期、Nginx、DNS-01、阿里云 DNS、运维自动化