跳转至

全链路无人值守自动化 SSL

起因

腾讯云的免费 SSL 证书有效期只有 3 个月,且不支持自动续签,不是很方便(其他云厂商也类似)

解决方案

  • A:每三个月手动更新一次(免费 + 手动续签 + 手动上传)
  • B:使用 Let's Encrypt + acme.sh / Certbot 脚本(免费 + 自动续签 + 自动上传)
  • C:购买付费证书(付费 + 自动续签 + 无需上传)

B(土豪随意)

结合 acme.sh1 + GitHub Actions,撸了一套 全链路无人值守自动化 SSL 脚本,具备以下特色:

功能模块 特色说明
高可靠设计 ✓ 面向长期稳定运行设计,适合云原生自动化部署
全自动运维 ✓ 无需人工干预,实现 SSL 证书生命周期全自动管理
自动签发 ✓ 自动调用 Let’s Encrypt 申请证书
智能续期 ✓ 仅在证书即将过期时续签,避免无效请求
云端集成 ✓ 自动上传证书至腾讯云 SSL 证书管理
CDN 热更新 ✓ 自动更新 CDN HTTPS 配置并立即生效

脚本流程指令

脚本下载

acme-auto.yml
name: Auto SSL Renew & Deploy (Tencent Cloud)

permissions:
  contents: read
  actions: write

on:
  workflow_dispatch:
  schedule:
    # Triggered on the 1st & 16th of each month
    - cron: '0 0 1,16 * *'

# Prevent concurrent: manual trigger + cron
concurrency:
  group: ssl-renew-${{ github.repository }}
  cancel-in-progress: false

jobs:
  issue_cert:
    runs-on: ubuntu-latest

    steps:
      # -------------------------
      # Restore acme state
      # -------------------------
      - name: Restore acme.sh state
        uses: actions/cache@v4
        with:
          path: ~/.acme.sh
          key: acme-state-v1

      # -------------------------
      # Install acme.sh if needed
      # -------------------------
      - name: Install acme.sh
        env:
          ACME_EMAIL: ${{ secrets.ACME_EMAIL }}
        run: |
          export PATH="$HOME/.acme.sh:$PATH"

          if [ -f "$HOME/.acme.sh/acme.sh" ]; then
            echo "✅ acme.sh already installed, skipping reinstall."
          else
            echo "⬇️ Installing acme.sh ..."
            curl -fsSL https://get.acme.sh | sh -s email=$ACME_EMAIL
          fi

          ~/.acme.sh/acme.sh --upgrade --auto-upgrade

      # -------------------------
      # Issue / Renew certificate
      # -------------------------
      - name: Smart issue / renew
        id: renew
        env:
          DOMAIN: ${{ secrets.DOMAIN }}
          DP_Id: ${{ secrets.DP_ID }}
          DP_Key: ${{ secrets.DP_KEY }}
        run: |
          export PATH="$HOME/.acme.sh:$PATH"

          CERT_PATH="$HOME/.acme.sh/${DOMAIN}_ecc/fullchain.cer"

          OLD_HASH=""
          if [ -f "$CERT_PATH" ]; then
            OLD_HASH=$(sha256sum "$CERT_PATH" | cut -d' ' -f1)
            echo "Old cert hash: $OLD_HASH"
          fi

          # First issue, if cert not exist
          if [ ! -f "$CERT_PATH" ]; then
            echo "🎬 First certificate issue..."
            ~/.acme.sh/acme.sh \
              --issue \
              -d $DOMAIN \
              -d www.$DOMAIN \
              --dns dns_dp \
              --ecc \
              --days 30 \
              --server letsencrypt \
              --dnssleep 120
          # If cert exist, use cron to auto check if all certs need to be renewed, if so → renew, otherwise → skip
          else
            echo "⤵ Running cron..."
            ~/.acme.sh/acme.sh --cron
          fi

          NEW_HASH=$(sha256sum "$CERT_PATH" | cut -d' ' -f1)
          echo "New cert hash: $NEW_HASH"

          if [ "$OLD_HASH" != "$NEW_HASH" ]; then
            echo "renewed=true" >> $GITHUB_OUTPUT
            echo "🔔 Certificate updated"
          else
            echo "renewed=false" >> $GITHUB_OUTPUT
            echo "🔕 Certificate unchanged"
          fi

      # -------------------------
      # Prepare certificate
      # -------------------------
      - name: Prepare cert files
        if: steps.renew.outputs.renewed == 'true'
        env:
          DOMAIN: ${{ secrets.DOMAIN }}
        run: |
          mkdir -p certs
          cp ~/.acme.sh/${DOMAIN}_ecc/${DOMAIN}.key certs/
          cp ~/.acme.sh/${DOMAIN}_ecc/fullchain.cer certs/

      # -------------------------
      # Deploy to Tencent Cloud
      # -------------------------
      - name: Upload cert and update CDN
        if: steps.renew.outputs.renewed == 'true'
        env:
          QCLOUD_SECRET_ID: ${{ secrets.QCLOUD_SECRET_ID }}
          QCLOUD_SECRET_KEY: ${{ secrets.QCLOUD_SECRET_KEY }}
          DOMAIN: ${{ secrets.DOMAIN }}
        run: |
          pip install --prefer-binary --disable-pip-version-check tencentcloud-sdk-python==3.1.50

          python3 <<'EOF'
          import os, json
          from tencentcloud.common import credential
          from tencentcloud.common.profile.http_profile import HttpProfile
          from tencentcloud.common.profile.client_profile import ClientProfile
          from tencentcloud.ssl.v20191205 import ssl_client, models
          from tencentcloud.cdn.v20180606 import cdn_client, models as cdn_models

          # ---------- Upload cert ----------
          cred = credential.Credential(
              os.getenv("QCLOUD_SECRET_ID"),
              os.getenv("QCLOUD_SECRET_KEY")
          )

          ssl = ssl_client.SslClient(cred, "ap-guangzhou")

          domain = os.getenv("DOMAIN")

          with open("certs/fullchain.cer") as f:
              pub = f.read()

          with open(f"certs/{domain}.key") as f:
              key = f.read()

          upload_req = models.UploadCertificateRequest()
          upload_req.from_json_string(json.dumps({
              "CertificatePublicKey": pub,
              "CertificatePrivateKey": key,
              "Alias": f"AutoSSL-{domain}"
          }))

          upload_resp = ssl.UploadCertificate(upload_req)
          cert_id = json.loads(upload_resp.to_json_string())["CertificateId"]

          print("✅ Uploaded new cert: ", cert_id)


          # ---------- Update CDN ----------
          domains = [domain, f"www.{domain}"]

          cdn = cdn_client.CdnClient(cred, "ap-guangzhou")
          for d in domains:
              try:
                  print("🔧 Updating CDN for domain: ", d)
                  req = cdn_models.UpdateDomainConfigRequest()
                  req.from_json_string(json.dumps({
                      "Domain": d,
                      "Https": {
                          "Switch": "on",
                          "CertInfo": {"CertId": cert_id}
                      }
                  }))

                  cdn.UpdateDomainConfig(req)
                  print("✅ CDN updated: ", d)
              except Exception as e:
                  print("❌ CDN update failed: ", d, e)
                  raise
          EOF

      # -------------------------
      # Result
      # -------------------------
      - name: Result
        if: always()
        run: |
          if [ "${{ steps.renew.outputs.renewed }}" = "true" ]; then
            echo "✅ SSL renewed and deployed."
          else
            echo "🔹 No renewal needed."
          fi

执行流程

脚本执行流程如下:

trigger workflow
restore ~/.acme.sh state
install acme.sh if needed
if cert_not_exist
    issue
else
    cron (renew or skip)
check cert hash
      ├── unchanged ── ▶︎ exit
      └── changed
        upload cert
        update CDN

签发指令

命令 作用 是否需要验证参数 使用场景
issue 申请新证书(也能续期) 需要 第一次申请 / 重新签发
renew 只续期已有证书 不需要 已有证书续期
cron 自动检测并续期所有证书 智能续期,自动做 expiry check 和 renew

配置使用

脚本以腾讯云为样例,如需使用其他云厂商,只需把脚本中的步骤 Deploy to Tencent Cloud 代码替换为对应的版本

DNSPod Token 配置

腾讯云 → DNSPod 「账号中心 → API密钥」,选择 DNSPod Token → 创建密钥,记下 ID 和 Token

ID:668000
Token:1md3646b5c4d7ba5969c48e521629ird

API 密钥配置

腾讯云 → DNSPod 「账号中心 → API密钥」,选择 腾讯云 API 密钥 → 新建密钥,记下 SecretId 和 SecretKey

SecretId:A******441jEXeZJp9LL8kA22Bl9k8******
SecretKey:B*****FG4e1B7u52LSbcTHzdDl7******

自动化配置

可使用 GitHub Actions 工作流来触发定时任务完成证书的自动签发、上传与更新(省去云服务器)

创建 GitHub 仓库

在 GitHub 创建一个仓库

设置仓库 secrets

仓库 → Settings → Secrets and variables → Actions → New repository secret,分别添加:

Secret 名称 内容 来源
DP_Id DNSPod ID DNSPod 控制台
DP_Key DNSPod Token DNSPod 控制台
QCLOUD_SECRET_ID 腾讯云 SecretId 腾讯云访问管理
QCLOUD_SECRET_KEY 腾讯云 SecretKey 腾讯云访问管理
DOMAIN 你的备案域名 备案域名
ACME_EMAIL 随意一个真实邮箱 手动填写

创建工作流

仓库首页 → Add file → Create a new file: .github/workflows/acme-auto.yml,内容见 脚本下载

运行与测试

仓库 → Actions → 选中左侧 Auto SSL Renew & Deploy → 点击右侧 Run workflow,等待运行完成,查看腾讯云:

  • SSL 证书 → 我的证书,查看是否存在刚上传的 AutoSSL-{domain} 证书(记下ID)
  • 内容分发网络 CDN → 证书配置,查看域名对应的证书是否已更新为最新证书(看证书ID)

  1. acme.sh 是一个用于 SSL / TLS 证书自动化的纯 Unix shell 脚本,实现了 acme 协议,可以从 ZeroSSL,Let's Encrypt 等 CA 生成免费的证书