如何剩下一笔SSL(https)证书的钱?


如何剩下一笔 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│
└─────────────────┘     └─────────────────┘
         │
         ▼
┌─────────────────┐
│  飞书每日报告    │
│  本地+远程监控   │
└─────────────────┘

核心流程

  1. acme.sh 通过 DNS-01 挑战申请 *.example.com 通配符证书。
  2. DNS 解析在阿里云,通过 RAM 用户的 AccessKey 自动添加 TXT 记录验证域名。
  3. 证书到期前 30 天自动续期。
  4. 续期成功后自动复制 fullchain.cer.key/etc/nginx/cert/
  5. 执行 nginx -s reload 使新证书生效。
  6. 每天 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 不能乱加 extramarkdown 文本要注意换行符等。建议先用最简单的结构调试通过,再美化。


五、效果

运行一段时间后,飞书每天会收到这样的报告:

  • 域名、本地证书状态、远程 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、运维自动化

相关推荐
2501_915918411 小时前
Rust 程序抓包解密,rustls 不认系统证书的几种办法
开发语言·后端·网络协议·ios·adb·https·rust
喜欢吃燃面2 小时前
网络协议与通信原理深度解析
网络·网络协议
程序员-李俞3 小时前
Coze 工作流调用异步 HTTP API 完整教程:任务 ID、循环轮询、状态判断与结果 URL 提取
网络·人工智能·网络协议·http·aigc·ai编程·ai写作
非凡大爹3 小时前
IPv6扩展首部:为什么IPv6要把功能从IP首部分离出来?IPv6扩展首部解析
网络协议·tcp/ip·计算机网络
Logintern0912 小时前
网络通讯协议—TCP/IP协议
网络·网络协议·tcp/ip
Zguigo1 天前
lesson43-44深入理解 TCP 可靠传输与流量控制:从机制到实战
网络·网络协议·tcp/ip
SendTomo1 天前
基于WebRTC的P2P文件传输IP暴露是正常现象
网络·网络协议·tcp/ip·webrtc·p2p
熊IT1 天前
HTTP 代理检测工具:快速检查代理可用性、出口 IP 与延迟
网络协议·tcp/ip·http
何以解忧,唯有..1 天前
HTTP 与 HTTPS:从明文传输到安全加密的演进
安全·http·https