之前写过一篇使用 acme.sh 申请 SSL 证书的帖子,最近把这个流程放到了 GitHub Action 上自动化完成,在这儿记录一下要点。

GitHub Action 是托管在 GitHub 上的自动化服务,免费账户的私有仓库每月有 2000 分钟的运行额度,可以用来做很多运维、CI 相关的工作。

要使用 GitHub Action,只需在项目的根目录下的 .github/workflows 中创建一个 .yml 文件即可,比如新建一个 .github/workflows/renew-ssl.yml

基本设置

这个 action 的基本结构类似下面这样:

name: Renew SSL Certificate

on:
  schedule:
    - cron: '0 20 * * 0' # 每周一 UTC 20:00(北京时间 04:00)
  workflow_dispatch: # 支持手动触发

permissions:
  contents: read

jobs:
  renew-cert:
    runs-on: ubuntu-latest
    steps:
      - name: Task name
        run: |
          YOUR SCRIPT

代码很直白,这儿就不多解释了,下面继续看最主要的任务步骤(steps)。

第一步:安装 acme.sh

我们要使用 acme.sh 来申请 SSL 证书,因此第一步需要安装 acme.sh。在以上 YML 文件的 steps 部分,添加以下代码:

      - name: Install acme.sh
        run: |
          curl https://get.acme.sh | sh -s email=${{ secrets.ACME_EMAIL }}

第二步:申请证书

接下来,则是使用 acme.sh 申请域名证书。这儿我们申请了一个泛域名证书:

      - name: Issue or Renew Certificate
        id: issue-cert
        env:
          Ali_Key: ${{ secrets.ALI_KEY }}
          Ali_Secret: ${{ secrets.ALI_SECRET }}
        run: |
          ~/.acme.sh/acme.sh --issue \
            -d oldj.net \
            -d '*.oldj.net' \
            --dns dns_ali \
            --keylength ec-256

你需要将上面代码中的 oldj.net 替换为你自己的域名。申请泛域名证书时,需要同时传入 your-domain.com*.your-domain.com

我的域名是在阿里云上,因此 --dns 参数设置的是 dns_ali 。如果你的域名在其他注册商那里,需要把这个参数改为对应的值,具体可查看 acme.sh 的文档

注意,这儿用到了环境变量 secrets.ALI_KEYsecrets.ALI_SECRET ,这两个值需要你去阿里云后台生成,然后在 GitHub 项目仓库的后台添加。

阿里云后台具体位置是 AccessKey 管理页面,建议创建一个子账号,只授予 AliyunDNSFullAccess 权限即可。

GitHub 添加 Secrets 的位置是:仓库页面 → Settings → Secrets and variables → Actions → New repository secret。

第三步:上传到服务器

证书需要上传到服务器才能发挥作用,在这儿我们使用 SSH 的方式连接服务器。为了能顺利连接服务器,需要你准备一对 SSH 密钥,将公钥上传到服务器的 ~./ssh/authorized_keys 下,同时像上面那样将密钥添加到 GitHub Secrets 中,名称是 SSH_PRIVATE_KEY

接下来,再在 renew-ssl.yml 中继续添加以下代码:

      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          echo "StrictHostKeyChecking no" > ~/.ssh/config

再接下来是正式的上传步骤:

      - name: Deploy to Server-001
        if: steps.issue-cert.outcome == 'success'
        continue-on-error: true
        run: |
          scp ~/.acme.sh/oldj.net_ecc/fullchain.cer \
              ~/.acme.sh/oldj.net_ecc/oldj.net.key \
              ${{ secrets.SERVER_USER_GATEWAY }}@${{ secrets.SERVER_HOST_GATEWAY }}:${{ secrets.CERT_REMOTE_DIR }}/
          ssh ${{ secrets.SERVER_USER_GATEWAY }}@${{ secrets.SERVER_HOST_GATEWAY }} "sudo nginx -s reload"

注意 SERVER_USER_GATEWAYSERVER_HOST_GATEWAY 分别是你的服务器的用户名和地址(IP 或域名),CERT_REMOTE_DIR 是你准备把证书上传到的服务器文件夹,需要先创建对应的文件夹。这三个变量也像上面一样添加到 GitHub Secrets 中。

步骤的第一行 if: steps.issue-cert.outcome == 'success' 是要确保前面申请证书成功才会执行后续的操作。

最后一行的 sudo nginx -s reload 作用是让服务器上的 Nginx 重新加载配置以及证书,没有这一句,即使证书文件已经上传了,Nginx 仍会继续使用老证书,直到下一次重载或重启。

到这儿,证书的申请 → 上传 → 应用流程就基本完整了,如果你有多台服务器在使用这个证书,可以继续添加上传任务。

更新腾讯云上的证书

我在使用腾讯云 CDN,所以也研究了一下如何自动更新腾讯云的上传证书。其他云服务商应该也有类似的接口。

另外,腾讯云的 EdgeOne 已经可以自动申请和更新免费证书了,无需再自己上传,不过传统的 CDN 服务目前似乎还需要自己管理免费证书。

相关代码如下:

      - name: Upload to Tencent Cloud
        if: steps.issue-cert.outcome == 'success'
        continue-on-error: true
        run: |
          pip install tccli

          # 配置腾讯云 CLI
          tccli configure set secretId ${{ secrets.TENCENT_SECRET_ID }}
          tccli configure set secretKey ${{ secrets.TENCENT_SECRET_KEY }}
          tccli configure set region ap-guangzhou

          CERT=$(cat ~/.acme.sh/oldj.net_ecc/fullchain.cer | base64 -w 0)
          KEY=$(cat ~/.acme.sh/oldj.net_ecc/oldj.net.key | base64 -w 0)

          # 上传新证书
          RESULT=$(tccli ssl UploadCertificate \
            --CertificatePublicKey "$(cat ~/.acme.sh/oldj.net_ecc/fullchain.cer)" \
            --CertificatePrivateKey "$(cat ~/.acme.sh/oldj.net_ecc/oldj.net.key)" \
            --Alias "oldj.net-wildcard-$(date +%Y%m%d)")

          NEW_CERT_ID=$(echo "$RESULT" | jq -r '.CertificateId')
          echo "New certificate ID: $NEW_CERT_ID"

          # 如果有旧证书 ID,自动替换关联资源
          if [ -n "${{ secrets.TENCENT_OLD_CERT_ID }}" ]; then
            tccli ssl UpdateCertificateInstance \
              --OldCertificateId ${{ secrets.TENCENT_OLD_CERT_ID }} \
              --ResourceTypes '["cdn"]' \
              --CertificateId "$NEW_CERT_ID"
            echo "Associated resources updated from ${{ secrets.TENCENT_OLD_CERT_ID }} to $NEW_CERT_ID"
          fi

          # 自动更新 Secret 中的证书 ID,供下次续期使用
          echo "$NEW_CERT_ID" | gh secret set TENCENT_OLD_CERT_ID
          echo "TENCENT_OLD_CERT_ID updated to: $NEW_CERT_ID"
        env:
          GH_TOKEN: ${{ secrets.GH_PAT }}
          GH_REPO: ${{ github.repository }}

请注意将代码中的证书路径(本例中是 oldj.net_ecc)替换为你的实际路径。

这段代码中使用了腾讯云的 tccli 来实现证书上传和更新工作,和上面类似,你需要先设置一些 Secrets:

  • TENCENT_SECRET_ID 腾讯云 API SecretId

  • TENCENT_SECRET_KEY 腾讯云 API SecretKey

  • TENCENT_OLD_CERT_ID 当前正在使用的证书 ID(如果仅上传,不需要自动替换老证书,可不填此项)

其中 TENCENT_SECRET_*可以前往腾讯云 API 密钥管理页面生成,建议创建子账号,授予 QcloudSSLFullAccess 权限,以及你需要更新的资源的权限,比如 QcloudCDNFullAccess

上面的 --ResourceTypes 用于指定要更新的资源,这儿我只写了 CDN,你可以根据需要调整。

首次运行前,需要你先设置一下要更新的证书 ID。由于每次自动更新证书之后,证书 ID 都会变化,为了实现完全自动化,这儿添加了一个 GH_PAT 参数,用于记录你的 GitHub Personal Access Token

这个 GitHub Personal Access Token 的创建方式如下:

  1. 进入 GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens

  2. 选择对应的组织以及仓库,权限勾选 Secrets → Read and Write

  3. 生成后,将 Token 存入仓库 Secret 中,名称是 GH_PAT

这样自动更新腾讯云上的证书的流程便也自动化了。