如何用 GitHub Actions + 动态密码实现全自动安全部署

部署,是自动化的最后一公里,也是最容易出事的一步

自动化构建 CI 已经成了现代开发流程的标配,但在部署环节,很多人仍然在用:

  • 明文密码
  • SSH 密钥硬编码
  • 凭证长期有效、难以轮换

这一切都让自动化部署变成一把双刃剑:一边提升效率,一边埋下隐患。

在上一篇文章中,我们介绍了一种基于 HMAC 和 UTC 时间戳构建的 一次性动态密码机制,它能在本地和服务器之间生成一致的临时口令,并定期失效,最大限度减少泄露风险。

这一篇,我们就来讲讲:如何把这套机制整合进 GitHub Actions 中,构建一条真正安全、零明文、自动化的部署链路。

总体思路:一套最小改动、最大安全收益的 CI 流程

我们期望实现的效果是:

  • 每次 push 到主分支,自动构建项目
  • 如果是正式发布(通过 commit message 判断),触发发布流程
  • 使用 GitHub Actions 自动部署构建产物
  • ✅ 整个过程不暴露任何明文密码
  • ✅ 密码在上传结束后立即失效,构建机器也无法长期使用

GitHub Actions 的整体结构

  • 动态生成密码(使用 HMAC + 时间戳命令)
  • 写入临时文件 rsync.password
  • 使用 rsync --password-file 上传构建文件
  • 上传完成后清理临时密码文件

✅ 为了防止上传开始时正好跨过时间窗口(导致服务器验证失败),我们使用带有自动重试的 retry 逻辑,在失败后重新生成密码并重试上传。

CI 中动态生成密码的做法

我们不在 Secrets 中保存密码,而是保存生成密码的命令

1
2
3
echo "RSYNC_PASSWORD $(echo -n 'deploy_user' | \
openssl dgst -sha256 -hmac \"HIDEDKEY_$(( $(date -u +%s) / 300 * 300 ))\" | \
awk '{print $2}')"

CI 中这样提取:

1
2
3
4
5
6
- name: Generate Rsync Password
run: |
set +x
PASSWORD=$(eval "${{ secrets.RSYNC_PASSWORD_COMMAND }}" | awk '{ print $2 }')
echo "$PASSWORD" > /tmp/rsync.password
chmod 600 /tmp/rsync.password

GitHub Secrets 本身是加密存储,但如果被误输出到日志或调试信息中,仍可能造成泄露。
使用动态密码机制,即使命令被泄露,生成的密码也只在几分钟内有效,且无法复用
极大降低风险。

安全细节:自动清理、IP 隐私、防重放

我们在部署过程中引入了三项安全强化措施:

  • 使用 sed 清洗日志中的 IP 输出,避免泄露服务器 IP:
1
rsync ... 2>&1 | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/[REDACTED-IP]/g'
  • 时间窗口重试机制(Retry)

由于密码基于当前 UTC 时间窗口生成(如每 5 分钟一次),如果部署正好跨过窗口边界,可能导致密码失效。
为此我们使用 nick-fields/retry 自动重试,并在每次 retry 时重新生成一次性密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name: Deploy to Server
uses: nick-fields/retry@v3
with:
max_attempts: 3
timeout_minutes: 5
command: |
rsync -avz --password-file=/tmp/rsync.password ./dist/ ${{ secrets.RSYNC_ADDRESS }} \
--delete --port=${{ secrets.RSYNC_PORT }} 2>&1 | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/[REDACTED-IP]/g'
on_retry_command: |
rm /tmp/rsync.password
set +x
PASSWORD=$(eval "${{ secrets.RSYNC_PASSWORD_COMMAND }}" | awk '{print $2}')
echo "$PASSWORD" > /tmp/rsync.password
chmod 600 /tmp/rsync.password

在 retry 操作内我们重新生成了密码文件,并设置好文件权限,使用 set +x 关闭 Shell 输出,避免敏感命令打印到日志。

✅ 即使 CI 日志被误打印,密码也只在几分钟内有效,且每次部署独立,重放失败

  • 成功上传后删除临时密码文件,避免密码文件泄露
1
2
- name: Cleanup Password File
run: rm -f /tmp/rsync.password

完整示例结构

你可以设计如下的 Secrets:

  • RSYNC_ADDRESS = user@host::module
  • RSYNC_PORT = 873
  • RSYNC_PASSWORD_COMMAND = 上面那一整行生成命令

配合 .github/workflows/deploy.yml 中的判断逻辑,自动触发部署。

总结

这一篇我们实现了一个几乎零泄露风险的自动部署流程

  • 明文密码消失了 ✅
  • 凭证只在当前构建生效 ✅
  • 服务器也不需要任何认证服务端口额外配置 ✅

下一篇我们将介绍:如何用 commit message 自动触发版本发布、打 tag、发 Release、部署一整套流程,实现”一次提交,全部完成”的智能发布策略。