惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Schneier on Security
The Last Watchdog
The Last Watchdog
Cyberwarzone
Cyberwarzone
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
L
Lohrmann on Cybersecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
The Cloudflare Blog
V
V2EX
博客园_首页
博客园 - 聂微东
Vercel News
Vercel News
人人都是产品经理
人人都是产品经理
G
GRAHAM CLULEY
T
Tenable Blog
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
SecWiki News
SecWiki News
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
N
News | PayPal Newsroom
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
T
Troy Hunt's Blog
博客园 - 【当耐特】
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
A
About on SuperTechFans
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
D
DataBreaches.Net
P
Privacy & Cybersecurity Law Blog
Schneier on Security
Schneier on Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Jina AI
Jina AI
D
Docker
P
Proofpoint News Feed

宝硕博客

LLM 工程化在福 uu 中的落地实践 —— 假期调课的智能解析 - 宝硕博客 实现一个 CSS 词法分析器(Lexer) - 宝硕博客 向着璀璨的未来进发 —— 我的 2024 年度总结 - 宝硕博客 愿此去前路皆坦途 —— 我的 2023 年度总结 - 宝硕博客 如何创建一个打印友好型的网页 - 宝硕博客 我的 OI 生涯 —— 一名退役竞赛生的回忆录 - 宝硕博客 向 #define int long long 说不 - 宝硕博客 再见,2022 —— 我的 2022 年度总结 - 宝硕博客 我的新冠阳性日记 - 宝硕博客 USTC Hackergame 2022 Write Up - 宝硕博客 OIerDb NG —— 新一代的 OIerDb - 宝硕博客 拥抱 Atomic CSS-in-JS - 宝硕博客 强制卸载三星应用分身中的残留应用 - 宝硕博客 2022 年常中集训游记 - 宝硕博客
使用 GitHub Actions 自动申请与部署 SSL 证书 - 宝硕博客
宝硕 · 2022-05-15 · via 宝硕博客

对于一个有很多服务器的人来说,在不同服务器上同步 SSL 证书是一件麻烦事。笔者尝试过很多种方式,最后在 Menci 的推荐下选定了使用 GitHub Actions 来自动申请、续期 SSL 证书,并自动推送到各个服务器上。

本博客的证书也是使用这种方式进行签发、部署的,可以点击浏览器地址栏上的按钮查看证书。

申请证书

前期准备

首先请在本地(或自己的服务器上)成功使用 acme.shDNS-01 验证方式成功申请一次证书,如果不会操作的话可以参考 烧饼博客的教程 来进行。这个过程包括:

  1. 向 CA 注册 ACME 账户(如果使用 Let’s Encrypt 则会自动进行,详细步骤请参阅 acme.sh 的 Wiki)。
  2. 通过环境变量指定 DNS 提供商的凭据,用于添加/删除 ACME DNS-01 认证所需的 TXT 记录。
  3. 确认证书申请可以成功,为后续调试排除可能的问题。

第一次申请证书后,CA 的 ACME 账户凭据将被存储到 ~/.acme.sh/ca 中,DNS 提供商的凭据将被存储到 ~/.acme.sh/account.conf 中。将它们打包并使用 Base64 编码存储,以备在 GitHub Actions 中使用:

cd ~/.acme.sh
tar cz ca account.conf | base64 -w0

将输出内容添加到 GitHub 仓库的 Secrets 中。注意不要复制输出中的多余信息。

自动化

如果没有特殊需求,可以使用 Menci/acme 来简单地申请证书:

# 全局环境变量
env:
  # Checkout 到的目录
  CERTS_OUTPUT_BASE: certs
  # 证书输出目录
  CERTS_OUTPUT_DIRECTORY: example.com
  # 证书文件名
  FILE_FULLCHAIN: fullchain.pem
  # 私钥文件名
  FILE_KEY: privatekey.key

jobs:
  issue-ssl-certificate:
    name: Issue SSL certificate
    runs-on: ubuntu-latest
    steps:
      - uses: Menci/acme@v2
        with:
          # 指定 acme.sh 的版本
          version: 3.0.2

          # 上方保存的以 Base64 编码存储的凭据
          account-tar: ${{ secrets.ACME_SH_ACCOUNT_TAR }}

          # 域名列表,以空格分隔
          domains: example.com example.net example.org example.edu
          # 是否申请通配符
          append-wildcard: true

          # 传递给 acme.sh 的额外参数
          arguments: --dns dns_cf --challenge-alias example.com

          # 导出的证书路径
          output-fullchain: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
          output-key: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

如果需要高度自定义 acme.sh 的参数,比如为不同的域名设置不同的 DNS 提供商,可以使用下面的方式手动编写命令来执行:

# 全局环境变量
env:
  # Checkout 到的目录
  CERTS_OUTPUT_BASE: certs
  # 证书输出目录
  CERTS_OUTPUT_DIRECTORY: example.com
  # 证书文件名
  FILE_FULLCHAIN: fullchain.pem
  # 私钥文件名
  FILE_KEY: privatekey.key

jobs:
  issue-ssl-certificate:
    name: Issue SSL certificate
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          ref: master

      - name: Checkout output branch
        uses: actions/checkout@v2
        with:
          ref: certs
          path: ${{ env.CERTS_OUTPUT_BASE }}

      # 安装 acme.sh
      - name: Install acme.sh
        shell: bash
        run: curl -s https://get.acme.sh | sh

      # 解压 acme.sh 配置信息
      - name: Extract account files for acme.sh
        shell: bash
        run: |
          echo "$ACME_SH_ACCOUNT_TAR" | base64 -d | tar -C ~/.acme.sh -xz
        env:
          # Base64 编码的 acme.sh 配置信息
          ACME_SH_ACCOUNT_TAR: ${{ secrets.ACME_SH_ACCOUNT_TAR }}

      # 申请证书
      - name: Issue SSL certificates
        shell: bash
        run: |
          ~/.acme.sh/acme.sh --issue        \
            -d "example.com"   --dns dns_cf \
            -d "*.example.com" --dns dns_cf \
            -d "example.net"   --dns dns_dp \
            -d "*.example.net" --dns dns_dp \
            --server letsencrypt

      # 导出证书
      - name: Copy certificate to output paths
        shell: bash
        run: |
          ACME_SH_TEMP_DIR="$(mktemp -d)"
          ACME_SH_TEMP_FILE_FULLCHAIN="$ACME_SH_TEMP_DIR/fullchain.pem"
          ACME_SH_TEMP_FILE_KEY="$ACME_SH_TEMP_DIR/key.pem"

          ~/.acme.sh/acme.sh --install-cert -d "$ACME_SH_FIRST_DOMAIN" --fullchain-file "$ACME_SH_TEMP_FILE_FULLCHAIN" --key-file "$ACME_SH_TEMP_FILE_KEY"

          [[ -z "$ACME_SH_OUTPUT_FULLCHAIN" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_FULLCHAIN")" && cp "$ACME_SH_TEMP_FILE_FULLCHAIN" "$ACME_SH_OUTPUT_FULLCHAIN")
          [[ -z "$ACME_SH_OUTPUT_KEY" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_KEY")" && cp "$ACME_SH_TEMP_FILE_KEY" "$ACME_SH_OUTPUT_KEY")

          rm -rf "$ACME_SH_TEMP_DIR"
        env:
          # 修改此处的 example.com 为申请时填写的第一个域名
          ACME_SH_FIRST_DOMAIN: example.com
          ACME_SH_OUTPUT_FULLCHAIN: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
          ACME_SH_OUTPUT_KEY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

上传证书至仓库

# 上传证书
- name: Push to GitHub
  run: |
    git config --global user.name "BaoshuoBot"
    git config --global user.email "79077260+BaoshuoBot@users.noreply.github.com"

    cd "$CERTS_DIRECTORY"

    git add "$FILE_FULLCHAIN" "$FILE_KEY"
    git commit -m "Upload certificates on $(date '+%Y-%m-%d %H:%M:%S')"
    git push
  env:
    TZ: Asia/Shanghai
    CERTS_DIRECTORY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}

部署证书

在申请证书的 Job 执行完成后,可以执行一系列其他的 Job 来将证书部署到各个服务器或云服务。

服务器

可以使用 easingthemes/ssh-deploy 来使用 rsync 将证书同步到服务器上。同步完成后再使用 appleboy/ssh-action 远程执行命令重载 Nginx / Apache。

# 部署到服务器
deploy-to-server:
  name: Deploy Certificate to Server
  runs-on: ubuntu-latest
  needs: issue-ssl-certificate

  strategy:
    matrix:
      host:
        - 174.136.239.1 # Server 1
        - 174.136.239.2 # Server 2
        # ...
        - 174.136.239.254 # Server N

  steps:
    - name: Checkout
      uses: actions/checkout@v2
      with:
        ref: certs

    # 上传证书
    - name: Upload certificate to server
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
        SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        ARGS: '-avz --delete'
        REMOTE_HOST: ${{ matrix.host }}
        REMOTE_USER: ${{ secrets.REMOTE_USER }}
        SOURCE: ${{ env.CERTS_OUTPUT_DIRECTORY }}/
        TARGET: /path/to/ssl/certs/${{ env.CERTS_OUTPUT_DIRECTORY }}/

    # 重载 Nginx
    - name: Force-reload nginx
      uses: appleboy/ssh-action@v0.1.4
      with:
        host: ${{ matrix.host }}
        username: ${{ secrets.REMOTE_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          sudo /opt/hooks/reload-nginx.sh

需要注意的是,重载 Nginx / Apache 的命令需要 root 权限才能执行,可以采用只允许部署用户以 root 权限执行重载脚本的方式来避免出现安全问题。

/opt/hooks 目录下新建一个文件 reload-nginx.sh,内容如下:

#!/bin/bash
sudo systemctl force-reload nginx

然后新建一个名为 actions-cert 的用户,然后在 /etc/sudoers 文件中添加以下内容:

actions-cert ALL=(ALL) NOPASSWD: /opt/hooks/reload-nginx.sh

这个配置可以使 actions-cert 用户免密码以 root 用户的权限执行 /opt/hooks/reload-nginx.sh

最后使用 chmod 755 /opt/hooks/reload-nginx.sh 命令将 reload-nginx.sh 文件设置为可执行,同时禁止非所有者对其进行写入操作。

如果服务器位于 NAT 后,或者禁止了 SSH 连接,还有两个方法可以将证书部署到内网服务器上:

  1. 将证书先部署到有部署条件的服务器上,然后再在内网服务器上使用 rsync 从部署好的服务器上拉取证书。
  2. 将证书上传到 Azure Key Vault 等托管服务中,再在服务器上按照 Menci 的文章 中的教程拉取即可。

阿里云

阿里云的 SSL 证书服务 支持上传自定义证书,该证书可以用于 阿里云 CDN。阿里云暂未提供将证书部署至 OSS 的 API,建议 OSS 用户使用 CDN 回源 OSS 来代替。

使用 Menci/deploy-certificate-to-aliyun 将证书部署到阿里云:

# 部署到阿里云
deploy-to-aliyun:
  name: Deploy Certificate to Aliyun
  runs-on: ubuntu-latest
  needs: issue-ssl-certificate

  steps:
    # 拉取证书存储分支
    - name: Checkout
      uses: actions/checkout@v2
      with:
        ref: certs

    # 上传证书
    - name: Deploy certificate to aliyun
      uses: Menci/deploy-certificate-to-aliyun@beta-v1
      with:
        access-key-id: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
        access-key-secret: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
        fullchain-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
        key-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
        certificate-name: example.com
        cdn-domains: |
          example.com
          example.net

其中 certificate-name 指定上传的证书在证书服务中的名称(将自动替换旧版本),cdn-domains 指定需要将该证书部署到的 CDN 域名列表(用空白字符隔开)。

建议使用子账户 Access Key,为其赋予以下权限(并按需使用资源组隔离):

  • AliyunYundunCertFullAccess
  • AliyunCDNFullAccess
  • AliyunPCDNFullAccess
  • AliyunSCDNFullAccess
  • AliyunDCDNFullAccess

腾讯云

使用 renbaoshuo/deploy-certificate-to-tencentcloud 将证书部署至腾讯云 CDN:

deploy-to-qcloud-cdn:
  name: Deploy certificate to Tencent Cloud CDN
  runs-on: ubuntu-latest
  needs: issue-ssl-certificate

  steps:
    - name: Check out
      uses: actions/checkout@v2
      with:
        # If you just commited and pushed your newly issued certificate to this repo in a previous job,
        # use `ref` to make sure checking out the newest commit in this job
        ref: ${{ github.ref }}

    - uses: renbaoshuo/deploy-certificate-to-tencentcloud@v1
      with:
        # Use Access Key
        secret-id: ${{ secrets.QCLOUD_SECRET_ID }}
        secret-key: ${{ secrets.QCLOUD_SECRET_KEY }}

        # Specify PEM fullchain file
        fullchain-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
        # Specify PEM private key file
        key-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

        # Deploy to CDN
        cdn-domains: |
          cdn1.example.com
          cdn2.example.com

其中 cdn-domains 指定需要将该证书部署到的 CDN 域名列表(用空白字符隔开)。

建议使用子账户 API 密钥,为其赋予以下权限(并按需使用资源组隔离):

  • QcloudCDNFullAccess

自建 GoEdge CDN

使用 renbaoshuo/deploy-certificate-to-goedge 将证书部署至自建的 GoEdge CDN:

deploy-to-goedge-cdn:
  name: Deploy certificate to GoEdge CDN
  runs-on: ubuntu-latest
  steps:
    - name: Check out
      uses: actions/checkout@v2
      with:
        # If you just commited and pushed your newly issued certificate to this repo in a previous job,
        # use `ref` to make sure checking out the newest commit in this job
        ref: ${{ github.ref }}
    - uses: renbaoshuo/deploy-certificate-to-goedge@beta-v1
      with:
        # GoEdge API endpoint
        api-endpoint: https://cdn.api.baoshuo.dev

        # Use Access Key
        access-key-type: user
        access-key-id: ${{ secrets.GOEDGE_ACCESS_KEY_ID }}
        access-key: ${{ secrets.GOEDGE_ACCESS_KEY }}

        # GoEdge certificate ID
        cert-id: ${{ secrets.GOEDGE_CERT_ID }}

        # Specify PEM fullchain file
        fullchain-file: ${{ env.FILE_FULLCHAIN }}
        # Specify PEM private key file
        key-file: ${{ env.FILE_KEY }}

注:在部署前需要手动上传一次证书以便获取证书 ID。证书 ID 可以在「证书文件下载」处的 URL 参数中找到。

完整例子

这个 Action 完成了以下操作:

  1. 申请证书,并上传到仓库的 certs 分支。
  2. 在申请证书后将 certs 分支中的证书部署到服务器上。
# 名称
name: Issue SSL Certificates

# 触发条件
on:
  # 手动运行
  workflow_dispatch:
  # 定时运行
  schedule:
    # 每两个月运行一次
    - cron: '0 0 1 */2 *'

# 全局环境变量
env:
  # Checkout 到的目录
  CERTS_OUTPUT_BASE: certs
  # 证书输出目录
  CERTS_OUTPUT_DIRECTORY: example.com
  # 证书文件名
  FILE_FULLCHAIN: fullchain.pem
  # 私钥文件名
  FILE_KEY: privatekey.key

jobs:
  issue-ssl-certificate:
    # 申请证书并 push 到 certs 分支
    name: Issue SSL certificate
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          ref: master

      - name: Checkout output branch
        uses: actions/checkout@v2
        with:
          ref: certs
          path: ${{ env.CERTS_OUTPUT_BASE }}

      # 安装 acme.sh
      - name: Install acme.sh
        shell: bash
        run: curl -s https://get.acme.sh | sh

      # 解压 acme.sh 配置信息
      - name: Extract account files for acme.sh
        shell: bash
        run: |
          echo "$ACME_SH_ACCOUNT_TAR" | base64 -d | tar -C ~/.acme.sh -xz
        env:
          # Base64 编码的 acme.sh 配置信息
          ACME_SH_ACCOUNT_TAR: ${{ secrets.ACME_SH_ACCOUNT_TAR }}

      # 申请证书
      - name: Issue SSL certificates
        shell: bash
        run: |
          ~/.acme.sh/acme.sh --issue            \
            -d "example.com" -d "*.example.com" \
            --dns dns_cf --server letsencrypt

      # 导出证书
      - name: Copy certificate to output paths
        shell: bash
        run: |
          ACME_SH_TEMP_DIR="$(mktemp -d)"
          ACME_SH_TEMP_FILE_FULLCHAIN="$ACME_SH_TEMP_DIR/fullchain.pem"
          ACME_SH_TEMP_FILE_KEY="$ACME_SH_TEMP_DIR/key.pem"

          # 不要忘记修改这里的 -d 参数值为上方的第一个域名
          ~/.acme.sh/acme.sh --install-cert -d "example.com" --fullchain-file "$ACME_SH_TEMP_FILE_FULLCHAIN" --key-file "$ACME_SH_TEMP_FILE_KEY"

          [[ -z "$ACME_SH_OUTPUT_FULLCHAIN" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_FULLCHAIN")" && cp "$ACME_SH_TEMP_FILE_FULLCHAIN" "$ACME_SH_OUTPUT_FULLCHAIN")
          [[ -z "$ACME_SH_OUTPUT_KEY" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_KEY")" && cp "$ACME_SH_TEMP_FILE_KEY" "$ACME_SH_OUTPUT_KEY")

          rm -rf "$ACME_SH_TEMP_DIR"
        env:
          ACME_SH_OUTPUT_FULLCHAIN: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
          ACME_SH_OUTPUT_KEY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

      # 上传证书
      - name: Push to GitHub
        run: |
          git config --global user.name "BaoshuoBot"
          git config --global user.email "79077260+BaoshuoBot@users.noreply.github.com"

          cd "$CERTS_DIRECTORY"

          git add "$FILE_FULLCHAIN" "$FILE_KEY"
          git commit -m "Upload certificates on $(date '+%Y-%m-%d %H:%M:%S')"
          git push
        env:
          TZ: Asia/Shanghai
          CERTS_DIRECTORY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}

  # 部署证书到服务器
  deploy-to-server:
    name: Deploy Certificate to Server
    runs-on: ubuntu-latest
    needs: issue-ssl-certificate

    strategy:
      matrix:
        host:
          - 174.136.239.1 # Server 1
          - 174.136.239.2 # Server 2
          # ...
          - 174.136.239.254 # Server N

    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          ref: certs

      # 上传证书
      - name: Upload certificate to server
        uses: easingthemes/ssh-deploy@v2.1.5
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          ARGS: '-avz --delete'
          REMOTE_HOST: ${{ matrix.host }}
          REMOTE_USER: ${{ secrets.REMOTE_USER }}
          SOURCE: ${{ env.CERTS_OUTPUT_DIRECTORY }}/
          TARGET: /path/to/ssl/certs/${{ env.CERTS_OUTPUT_DIRECTORY }}/

      # 重载 Nginx
      - name: Force-reload nginx
        uses: appleboy/ssh-action@v0.1.4
        with:
          host: ${{ matrix.host }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            sudo /opt/hooks/reload-nginx.sh

杂项

部分情况下,GitHub Actions 中的 GITHUB_TOKEN 只有 Read repository contents permission,而本文中的 Actions 要求这个 Token 具有 Read and write permissions,那么需要在仓库的 Settings > Actions > General 页面的底部赋予其写入权限,如图所示:

设置好后点击 Save 按钮即可。

参考资料

  1. 使用 GitHub Actions 自动申请与部署 ACME SSL 证书,Menci,2022 年 5 月 11 日。对原文章内容的使用已经过作者同意。
  2. 使用 acme.sh 配置自动续签 SSL 证书,烧饼博客,2022 年 2 月 3 日。

文章头图由 Menci 制作,使用已经过授权,在此表示感谢。