用时间戳 + HMAC 给 rsync 生成一次性密码

自动部署的最后一步是把构建产物上传到服务器,用的是 rsync、scp 或者 ftp。要做到无人值守,密码就得写在某个地方:CI 的 Secrets、环境变量、或者 --password-file 指向的文件

这个密码是长期有效的。Secrets 本身是加密存储的,但只要日志里多输出了一行,或者哪次调试把环境变量打印出来,它就泄露了,而且泄露之后一直能用

我想要的是:部署脚本里不存密码,每次使用的密码几分钟就过期,客户端和服务端能各自计算出同一个值

从 TOTP 借的思路

TOTP 就是这么做的:共享一个密钥,把当前时间对齐到固定窗口,用 HMAC 生成一个短时有效的验证码。手机上的两步验证用的就是它

部署场景里照搬这个思路,用 Bash 和 openssl 就能实现一个轻量版本:

  • 密钥:用户名 + 窗口时间戳 + 一个固定的盐
  • 消息:用户名本身
  • 输出:HMAC-SHA256 的十六进制串,直接作为密码
USER="user1"
TIMESTAMP=$(( $(date -u +%s) / 300 * 300 ))
HMAC_KEY="${USER}${TIMESTAMP}SALT"

PASSWORD=$(echo -n "$USER" | openssl dgst -sha256 -hmac "$HMAC_KEY" | awk '{print $2}')

时间戳按 300 秒对齐,所以密码每 5 分钟换一次。不同用户名算出来的密码不同。真正需要保密的只有 SALT,两端必须一致

date -u 不能省略。两端如果一边用本地时间一边用 UTC,窗口就对不上了

服务端

rsyncd 的密码认证读取的是 secrets file,格式是每行一条 用户名:密码。服务端不需要改动任何认证逻辑,只要有个定时任务每 5 分钟把这个文件重写一遍:

#!/bin/bash
USERS=("user1" "user2")
TIMESTAMP=$(( $(date -u +%s) / 300 * 300 ))
SECRETS_FILE="/path/to/rsyncd.secrets"

> "$SECRETS_FILE"

for USER in "${USERS[@]}"; do
  HMAC_KEY="${USER}${TIMESTAMP}SALT"
  SECRET=$(echo -n "$USER" | openssl dgst -sha256 -hmac "$HMAC_KEY" | awk '{print $2}')
  echo "$USER:$SECRET" >> "$SECRETS_FILE"
done

chmod 600 "$SECRETS_FILE"
*/5 * * * * /path/to/generate_tmp_password.sh

rsyncd.conf 里指向这个文件:

auth users = user1,user2
secrets file = /path/to/rsyncd.secrets

这个方案对现有系统结构几乎没有侵入。rsyncd 该怎么配置还怎么配置,只是密码文件变成了定时生成的

跨窗口的问题

上传如果正好跨过 5 分钟边界,服务端已经换了密码,客户端会认证失败。我在 CI 里的处理是失败后重新生成密码再重试一次,见 用 GitHub Actions + 动态密码部署构建产物

可扩展性

虽然这套机制是为 rsync 设计的,但只要目标系统支持密码认证,它就能工作。适用的场景包括:

  • SFTP,配合 expect 脚本输入密码
  • HTTP Webhook 验签
  • 临时 API Token
  • 内网机器之间的定时传输任务

也可以在此基础上加入:

  • 多时间窗口容错,同时接受 ±1 个时间片的密码
  • 每个用户使用独立的密钥
  • 截断 HMAC 结果,只取前 8 到 12 位,方便人工输入

评论

评论加载中…

输入关键词开始搜索。

选择Enter 打开Esc 关闭CtrlK 唤起