
用时间戳 + 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 位,方便人工输入
评论
评论加载中…