对于一个有很多服务器的人来说,在不同服务器上同步 SSL 证书是一件麻烦事。笔者尝试过很多种方式,最后在 Menci 的推荐下选定了使用 GitHub Actions 来自动申请、续期 SSL 证书,并自动推送到各个服务器上。
本博客的证书也是使用这种方式进行签发、部署的,可以点击浏览器地址栏上的按钮查看证书。
申请证书
前期准备
首先请在本地(或自己的服务器上)成功使用 acme.sh 的 DNS-01 验证方式成功申请一次证书,
- 向 CA 注册 ACME 账户(如果使用 Let’s Encrypt 则会自动进行,详细步骤请参阅 acme.sh 的 Wiki)。
- 通过环境变量指定 DNS 提供商的凭据,用于添加/删除 ACME DNS-01 认证所需的 TXT 记录。
- 确认证书申请可以成功,为后续调试排除可能的问题。
第一次申请证书后,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@v1
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 连接,还有两个方法可以将证书部署到内网服务器上:
- 将证书先部署到有部署条件的服务器上,然后再在内网服务器上使用 rsync 从部署好的服务器上拉取证书。
- 将证书上传到 Azure Key Vault 等托管服务中,再在服务器上按照 Menci 的文章 中的教程拉取即可。
阿里云
阿里云的 SSL 证书服务 支持上传自定义证书,该证书可以用于 阿里云 CDN。阿里云暂未提供将证书部署至 OSS 的 API,建议 OSS 用户使用 CDN 回源 OSS 来代替。
使用 Menci/deploy-certificate-to-aliyun 将证书部署到阿里云:
# 部署到阿里云
deploy-to-server:
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-domain
指定需要将该证书部署到的 CDN 域名列表(用空白字符隔开)。
建议使用子账户 Access Key,为其赋予以下权限(并按需使用资源组隔离):
- AliyunYundunCertFullAccess
- AliyunCDNFullAccess
- AliyunPCDNFullAccess
- AliyunSCDNFullAccess
- AliyunDCDNFullAccess
完整例子
这个 Action 完成了以下操作:
- 申请证书,并上传到仓库的
certs
分支。 - 在申请证书后将
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 按钮即可。
评论 (0)