本文参考(
复制)自:使用 GitHub Actions 自动申请与部署 ACME SSL 证书
准备
首先请在本地(或自己的服务器上)成功使用 acme.sh 的 DNS-01 验证方式成功申请一次证书。这个过程包括:
- 向 CA 注册 ACME 账户(如果使用 Let's Encrypt 则会自动进行,如果使用 ZeroSSL 或者 GTS 则需要手动注册)。
- 通过环境变量指定 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 中
ACME Action
使用 Action Menci/acme 可以通过 acme.sh 申请 SSL 证书(使用 DNS-01 验证)。需要以下参数:
- account-tar:用于指定 ACME 客户端的凭据。使用刚刚生成好的 Secret 即可。
- domains-file:指定一个文件,其中包含需要为其申请证书的域名列表,用空白字符隔开。
- 亦可直接使用 domain 参数来指定域名列表,但不推荐直接将域名列表硬编码在 Workflow 文件中。
- 默认将包含它们对应的 wildcard 域名(即 example.com 会包含 *.example.com),如果不希望自动添加,则可以设置 append-wildcard 为 false。
- arguments-file:指定一个文件,其中包含向 acme.sh 传递的参数列表。主要用于指定 --dns 和 --server 参数。如 --dns dns_cf --server letsencrypt。
- 亦可直接使用 arguments 参数来指定参数列表。
- 其它常用参数如 --valid-to 和 --challenge-alias 也可以在此设定。
- output-fullchain:输出 PEM 格式的全链证书文件的路径(如果目录不存在将自动创建,下同)。
- output-key:输出 PEM 格式的证书私钥文件的路径。
- output-pfx:输出 PFX 格式的全链证书文件的路径(如果不需要可以留空,上同)。
- output-pfx-password 指定 PFX 文件的密码,随意指定即可。
另外,建议通过 version 参数指定 acme.sh 的版本(版本号或者 commit)来保持稳定性。
上传证书
因为我们将在不同的 Job 中执行接下来的证书部署操作,我们需要在申请证书的 Job 的最后将证书 push 到仓库中
首先创建一个空的分支:
mkdir /tmp/empty-repo && cd /tmp/empty-repo && git init # 创建一个临时 repo
git commit --allow-empty -m "Initial commit" # 创建一个空的 commit
git push https://github.com/<username>/<repo> HEAD:certs-main # push 到certs-main分支上
申请到的证书会放到这个分支下
部署证书
在申请证书的 Job 执行完成后,我们执行一系列 Job 来将证书部署到各个云服务。
由于我这边只用到又拍云,所以需要其他云服务的请参考原文
点击查看原文内容
Azure Key Vault
Key Vault 是 Azure 提供的密钥存储服务,可以用于存储 SSL 证书。并可以用于在 Front Door 中使用。
使用 Azure CLI(azure/CLI) 将证书部署到 Key Vault:
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Upload certificate
uses: azure/CLI@v1
with:
inlineScript: |
az keyvault certificate import --subscription "$AZURE_SUBSCRIPTION" \
--vault-name "$AZURE_KEY_VAULT_NAME" \
--file "$CERTS_OUTPUT_DIRECTORY/$FILE_PFX" \
--name "$AZURE_KEY_CERTIFICATE_NAME" \
--password "$PFX_PASSWORD"
env:
AZURE_SUBSCRIPTION: ${{ matrix.subscription }}
AZURE_KEY_VAULT_NAME: ${{ matrix.vault-name }}
AZURE_KEY_CERTIFICATE_NAME: ${{ matrix.certificate-name }}
在 Front Door 中指定来自 Key Vault 的证书后,证书将自动更新。
所使用的 Service Principal 需要拥有该 Key Vault 的 Certificate → Import 权限。使用以下命令可以创建一个 Service Principal:
$az ad sp create-for-rbac --name SSLCertificateUploader
{
"appId": "019d7f61-a969-4daa-b53f-f2341f6f4705",
"displayName": "SSLCertificateUploader",
"password": "vCGS5BQMm.BI~6BEjjCa-j6tZ2fCxJsENA",
"tenant": "72f988bf-86f1-41af-91ab-2d7cd011db47"
}
输出的内容即为该 Service Principal 的凭据(建议删除换行以免 { 和 } 被认为是 Secret,导致在 Actions 的输出中被遮盖)。在 Azure Portal 中为其赋予权限即可。
Azure App Service
App Service(Function App 同理)是 Azure 提供的 Web 应用服务。App Service 绑定的域名需要手动上传 SSL 证书(也可从 Key Vault 中导入,但导入是一次性的,不会像 Front Door 一样自动更新)。
使用 Menci/deploy-certificate-to-azure-web-app 将证书部署到 App Service:
- name: Deploy certificate
uses: Menci/deploy-certificate-to-azure-web-app@beta-v2
with:
azcliversion: 2.28.0
creds: ${{ secrets.AZURE_CREDENTIALS }}
subscription: ${{ matrix.subscription }}
resource-group: ${{ matrix.resource-group }}
webapp-name: ${{ matrix.webapp-name }}
certificate-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_PFX }}
certificate-password: ${{ env.PFX_PASSWORD }}
delete-old-certificates: true
该 Action 会将新的证书上传到 App Service 所在的 Resource Group 中,并为该 App Service 所绑定的所有匹配的域名应用该证书。当 delete-old-certificates 参数为 true 时,将自动删除该 App Service 之前使用的所有证书(包含非本 Action 上传的证书)。
所使用的 Service Principal 需要拥有 Resource Group 级别(而非 Resource 级别,因为同一个 Resource Group 的 SSL 证书是共享的)的 Website Contributor 权限。在 Azure Portal 中为其赋予权限即可。
阿里云
阿里云的 SSL 证书服务支持上传自定义证书,该证书可以用于阿里云 CDN。阿里云暂未提供将证书部署至 OSS 的 API,建议 OSS 用户使用 CDN 回源 OSS 来代替。
使用 Menci/deploy-certificate-to-aliyun 将证书部署到阿里云:
- name: Deploy certificate
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: ${{ matrix.certificate-name }}
cdn-domains: ${{ join(matrix.cdn-domains, ' ') }}
其中 certificate-name 指定上传的证书在证书服务中的名称(将自动替换旧版本),cdn-domain 指定需要将该证书部署到的 CDN 域名列表(用空白字符隔开)。
建议使用子账户 Access Key,为其赋予以下权限(并按需使用资源组隔离):
- AliyunYundunCertFullAccess
- AliyunCDNFullAccess
- AliyunPCDNFullAccess
- AliyunSCDNFullAccess
- AliyunDCDNFullAccess
服务器
通过 GitHub Actions 自动向服务器部署证书较为困难,特别是对于位于 NAT 后的服务器而言。我的解决方法是让服务器自动从 Azure Key Vault 中拉取证书。如果你不使用 Azure Key Vault,也可以考虑直接从 GitHub 仓库中拉取证书(使用只读的 Deploy Keys 可以达到权限最小化,但只能控制到仓库级别,无法控制到分支级别)。
下载证书所用的 Service Principal 需要拥有该 Key Vault 的 Secret → Get 权限(而非 Certificate → Get 权限,该权限仅可读取证书本身,不可读取证书私钥)。与上文同理,创建一个 Service Principal 并赋予权限(建议使用不同的 Service Principal 上传和下载证书),并在服务器上登录(其中 username 即为 appId):
$az login --service-principal \
--username 218acab1-8f2d-4f0c-ab30-8931332058d3 \
--password 'fbrDxvS0~mWQ8QcHVLQzqAuJhOHDR9-YW4' \
--tenant 72f988bf-86f1-41af-91ab-2d7cd011db47
登录 Azure CLI 后,使用以下脚本自动从 Azure Key Vault 中拉取证书(记得chmod +x):
#!/bin/bash -e
AZURE_CERT_URI="https://<你的 Key Vault 名称>.vault.azure.net/secrets/<你的 Key Vault 中证书的名称>"
INSTALL_KEY="/etc/nginx/ssl/ssl.key" # PEM 私钥的目标位置
INSTALL_CERT="/etc/nginx/ssl/ssl.crt" # PEM 全链的目标位置
INSTALL_CMD="systemctl reload nginx" # 应用证书的命令
rm -rf /tmp/update-cert
mkdir /tmp/update-cert
cd /tmp/update-cert
# 下载 PFX 格式的证书
az keyvault secret download --id "$AZURE_CERT_URI" --encoding base64 --file cert.pfx
# 转换为 PEM 格式
openssl pkcs12 -in cert.pfx -nocerts -out key-enc.pem -passin pass: -passout pass:pass
openssl pkcs12 -in cert.pfx -nokeys -out cert.pem -passin pass:
openssl rsa -in key-enc.pem -out key.pem -passin pass:pass
cp key.pem "$INSTALL_KEY"
cp cert.pem "$INSTALL_CERT"
$INSTALL_CMD
将该脚本添加到 cron 即可定期运行,如:
0 20 * * * /root/update-cert.sh
又拍云
又拍云的证书服务支持上传自定义证书,该证书可以用于又拍云所有服务。
使用 Menci/deploy-certificate-to-upyun 将证书部署到又拍云:
- name: Deploy certificate
uses: Menci/deploy-certificate-to-upyun@beta-v2
with:
subaccount-username: ${{ secrets.UPYUN_SUBACCOUNT_USERNAME }}
subaccount-password: ${{ secrets.UPYUN_SUBACCOUNT_PASSWORD }}
fullchain-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
key-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
domains: ${{ join(matrix.domains, ' ') }}
delete-unused-certificates: true
其中 domains 指定需要将该证书部署到的域名列表(用空白字符隔开)。
不需要修改此处,请查看下面的demo
当 delete-unused-certificates 为 true 时,将自动删除所有未使用的旧证书(包含非本 Action 上传的证书,以及在又拍云申请的证书)。
由于又拍云没有提供证书相关的公开 API,该 Action 使用控制台 API 实现,所以需要提供子账户的用户名和密码,而非 Access Key 和 Secret Key。
完整DEMO
这里给出一个完整的例子.
仓库文件结构:
├─.github
│ └─workflows
│ workflows.yml
└─main
deploy.json
domains
options
在 main/domains
文件中指定域名列表(一行一个,把想在证书上展示的域名写在第一位)(根域名包含泛域名,如不需要请在上文配置项修改)
example.com
example.net
在 main/options
文件中指定参数(即:第一次运行acme的参数-去掉域名)
--issue --dns dns_dp --server https://acme.freessl.cn/xxxxxxxx
在 main/deploy.json
中指定部署目标,每一类部署目标可以有多个
{
"upyun": [
{
"name": "github_AutoSSL",
"domains": [
"example.com",
"example.net",
"*.example.com",
"*.example.net",
"www.example.com",
"abc.example.net"
// !这里需要填写所有域名,使用泛域名也需要填写子域名
]
}
]
}
GitHub Workflow 文件(每两个月自动更新):
此文件注意配置ACCOUNT_TAR
name: ACME自动化
on:
workflow_dispatch:
schedule:
- cron: '0 0 1 */2 *' # 每两个月执行一次
# - cron: '0 0 * * WED'
env:
TARGET: main # main分支-文件夹名字
CERTS_OUTPUT_DIRECTORY: certs-output
FILE_FULLCHAIN: fullchain.pem # 分支存储的证书文件名,下三同
FILE_KEY: key.pem
FILE_PFX: certificate.pfx
PFX_PASSWORD: qwq # 证书密码,无所谓
jobs:
issue-push:
name: 申请并推送证书
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: main
- name: Checkout output branch
uses: actions/checkout@v2
with:
ref: certs-${{ env.TARGET }} # 分支的名字 certs-main !分支必须存在
path: ${{ env.CERTS_OUTPUT_DIRECTORY }}
- name: 申请证书
uses: Menci/acme@main
with:
version: 83da01a2e1f5384ed997f9e023ea4a813dcac1f0
account-tar: ${{ secrets.ACCOUNT_TAR }} # ACME的base64密钥
domains-file: ${{ env.CONFIG_DOMAINS }}
arguments-file: ${{ env.CONFIG_OPTIONS }}
output-fullchain: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
output-key: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
output-pfx: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_PFX }}
output-pfx-password: ${{ env.PFX_PASSWORD }}
env:
CONFIG_DOMAINS: ${{ env.TARGET }}/domains # 对应上面 main分支main文件夹下的domains文件,一行一个域名,只写根域名即可包含泛域名
CONFIG_OPTIONS: ${{ env.TARGET }}/options # 对应上面 main分支main文件夹下的options文件,主要写入你的server配置,即:第一次运行acme的参数-去掉域名
- name: 推送到Github分支
run: |
git config --global user.name $(git show -s --format='%an' HEAD)
git config --global user.email $(git show -s --format='%ae' HEAD)
cd "$CERTS_OUTPUT_DIRECTORY"
git add "$FILE_KEY" "$FILE_FULLCHAIN" "$FILE_PFX"
git commit -m "[Actions/${{ github.workflow }}] Upload certificates on $(date '+%Y-%m-%d %H:%M:%S')"
git push
env:
TZ: Asia/Shanghai
deployment-config:
name: 加载Deploy配置文件
runs-on: ubuntu-latest
outputs:
config: ${{ steps.read-deployment-config.outputs.config }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: main
- name: Read Deployment Config
id: read-deployment-config
run: |
echo "::set-output name=config::$(jq -c < "$CONFIG_FILE")"
env:
CONFIG_FILE: ${{ env.TARGET }}/deploy.json # 对应上面 main分支main文件夹下的deploy.json文件
deploy-upyun:
name: 部署到又拍云 - (${{ matrix.name }})
runs-on: ubuntu-latest
needs: [issue-push, deployment-config]
if: ${{ fromJson(needs.deployment-config.outputs.config).upyun }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.deployment-config.outputs.config).upyun }}
steps:
- name: Checkout output branch
uses: actions/checkout@v2
with:
ref: certs-${{ env.TARGET }}
path: ${{ env.CERTS_OUTPUT_DIRECTORY }}
- name: Deploy certificate
uses: Menci/deploy-certificate-to-upyun@main
with:
subaccount-username: ${{ secrets.UPYUN_SUBACCOUNT_USERNAME }} # 又拍云账户用户名
subaccount-password: ${{ secrets.UPYUN_SUBACCOUNT_PASSWORD }} # 又拍云账户密码
fullchain-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
key-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
domains: ${{ join(matrix.domains, ' ') }}
delete-unused-certificates: true
全部部署完成后,仓库至少有一个分支
,然后运行一下workflow,体验一下全自动部署吧 :)
Q.E.D.