只有负责决定是否允许访问的系统检查了适用截止时间,凭据才真的会到期。在资产清单里写一个日期,不会自动改变数据库或接口接受什么。较短有效期可以压缩被盗凭据继续建立新访问的时间窗口,但具体效果取决于认证机制,以及此前已经建立的会话、签发的其他凭据如何处理。秘密不是因为自身附带一行日期,就能独立让所有访问失效。

坏掉的样子

到期不会撤销已经发生的行为。复制出去的数据仍在外面,截止时间之前建立的会话也可能继续工作。攻击者还可能在原本有效的权限下建立另一条访问路径。因此,有效期只是限制后续使用的一种手段,还要结合权限范围、撤销机制、会话管理和对实际影响的调查。它并不能为一次泄漏造成的全部损失设定通用上限,也不能把过去的访问自动变成没有发生过。

PostgreSQL 的 VALID UNTIL 控制角色密码的有效时间,不控制角色是否存在,也不覆盖所有认证方式。使用非密码方式建立连接,就不会获得同样的截止检查。如果通过本地信任规则来测试密码到期,便可能误以为到期功能失效。判断结果之前,应先确认客户端实际命中了哪条认证路径,不能只看两次连接用了同一个用户名。

轮换与续期是两个不同变化。在 PostgreSQL 中,仅修改 PASSWORD 会保留 VALID UNTIL,仅修改 VALID UNTIL 也会保留原密码。更新流程如果只修改其中一项,可能导致应用依然无法重新连接,也可能让原值继续有效。一个连接池里的旧连接查询成功,不足以证明新凭据能够完成新的认证,因为它可能根本没有重新交出密码接受检查。

快速验证

准备隔离的 Linux 实验环境,让 PostgreSQL 18 程序位于 PATH 中,以非 root 用户运行,并确认回环端口 55477 未被使用。将下列内容保存为脚本,用 sh 执行。它初始化一次性数据库集群,只使用明确虚构的密码,TCP 仅监听回环地址,退出时删除临时目录。本地信任认证只为展示另一条认证路径而启用,不应照搬到真实服务:

#!/bin/sh
set -eu
lab_root=$(mktemp -d /tmp/credential-expiry.XXXXXX)
cleanup() {
  if test -f "$lab_root/data/postmaster.pid"; then
    pg_ctl -D "$lab_root/data" -m immediate -w stop >/dev/null
  fi
  rm -rf "$lab_root"
}
trap cleanup EXIT
initdb -D "$lab_root/data" --locale=C --auth-local=trust --auth-host=scram-sha-256 >/dev/null
pg_ctl -D "$lab_root/data" -l "$lab_root/server.log" \
  -o "-k $lab_root -p 55477 -c listen_addresses=127.0.0.1" -w start >/dev/null
admin() {
  psql -X -qAt -v ON_ERROR_STOP=1 -h "$lab_root" -p 55477 -d postgres "$@"
}
# Both passwords below are invented, disposable lab fixtures.
admin <<'SQL'
SET password_encryption='scram-sha-256';
CREATE ROLE article_expiry LOGIN PASSWORD 'LAB-ONLY-OLD-PASSWORD';
SQL
mkfifo "$lab_root/input"
PGPASSWORD=LAB-ONLY-OLD-PASSWORD psql -X -qAt -v ON_ERROR_STOP=1 \
  -h 127.0.0.1 -p 55477 -U article_expiry -d postgres \
  <"$lab_root/input" >"$lab_root/held.out" 2>"$lab_root/held.err" &
held_pid=$!
exec 3>"$lab_root/input"
printf "SELECT 'ready';\n" >&3
attempt=0
until grep -qx ready "$lab_root/held.out"; do
  attempt=$((attempt + 1))
  test "$attempt" -lt 100
  sleep 0.05
done
admin <<'SQL'
SELECT format('ALTER ROLE article_expiry VALID UNTIL %L',
              clock_timestamp()+interval '1 second') \gexec
SELECT pg_sleep(1.2);
SELECT 'deadline passed='||(rolvaliduntil<clock_timestamp())
FROM pg_authid WHERE rolname='article_expiry';
SQL
if PGPASSWORD=LAB-ONLY-OLD-PASSWORD psql -X -qAt -h 127.0.0.1 -p 55477 \
  -U article_expiry -d postgres -c 'SELECT 1' >"$lab_root/new.out" 2>"$lab_root/new.err"; then
  exit 1
fi
grep -q 'password authentication failed' "$lab_root/new.err"
printf '%s\n' 'fresh SCRAM login rejected after expiry'
printf "SELECT 'held after expiry='||current_user;\n" >&3
psql -X -qAt -v ON_ERROR_STOP=1 -h "$lab_root" -p 55477 \
  -U article_expiry -d postgres -c "SELECT 'local trust login='||current_user"
admin <<'SQL'
SET password_encryption='scram-sha-256';
ALTER ROLE article_expiry PASSWORD 'LAB-ONLY-NEW-PASSWORD';
SQL
if PGPASSWORD=LAB-ONLY-NEW-PASSWORD psql -X -qAt -h 127.0.0.1 -p 55477 \
  -U article_expiry -d postgres -c 'SELECT 1' >"$lab_root/rotated.out" 2>"$lab_root/rotated.err"; then
  exit 1
fi
grep -q 'password authentication failed' "$lab_root/rotated.err"
printf '%s\n' 'rotating password alone preserves expired deadline'
admin <<'SQL'
SELECT format('ALTER ROLE article_expiry VALID UNTIL %L',
              clock_timestamp()+interval '1 hour') \gexec
SQL
if PGPASSWORD=LAB-ONLY-OLD-PASSWORD psql -X -qAt -h 127.0.0.1 -p 55477 \
  -U article_expiry -d postgres -c 'SELECT 1' >"$lab_root/old.out" 2>"$lab_root/old.err"; then
  exit 1
fi
grep -q 'password authentication failed' "$lab_root/old.err"
printf '%s\n' 'old password rejected after rotation'
PGPASSWORD=LAB-ONLY-NEW-PASSWORD psql -X -qAt -v ON_ERROR_STOP=1 \
  -h 127.0.0.1 -p 55477 -U article_expiry -d postgres \
  -c "SELECT 'new password login='||current_user"
printf "SELECT 'held after rotation='||current_user;\n" >&3
printf '\\q\n' >&3
exec 3>&-
wait "$held_pid"
grep -qx 'held after expiry=article_expiry' "$lab_root/held.out"
grep -qx 'held after rotation=article_expiry' "$lab_root/held.out"
cat "$lab_root/held.out"

此前 PostgreSQL 18.6 记录中的输出确认截止时间已经过去;本轮没有重跑认证实验。脚本把期限设为数据库当前时间的一秒之后,然后等待数据库时钟越过它,所以这里实际经历了到期,而不是只拿一个编造日期做字符串比较。随后,通过 SCRAM 密码认证建立的新连接被拒绝,验证了真正决定认证结果的服务端规则。

到期之前打开的连接仍然可用。输入管道让同一个客户端和数据库会话跨过截止时间与密码替换过程,最后的输出确认它在两件事发生后都还能以该角色执行查询。如果需求是终止既有会话,就必须建立超出这个密码属性的明确策略;只观察新登录失败,并不能宣称此前的连接也已全部失去访问。

新的本地信任登录依然成功,这符合不同认证方式适用不同规则的文档契约。单独替换密码后,新的密码登录还是失败,因为原截止时间没有被重置。脚本再提供一个未来期限,替换后的密码才可以连接,而原密码被拒绝。这样分别验证了凭据值、有效日期和会话存续,不会把三件事合并成一个模糊的“轮换成功”。

实验最终明确了三项独立观察:期限影响新的密码认证;认证方式决定这条规则是否适用;已经建立的会话不会自动终止。示例没有实现生产凭据代理,也没有证明所有访问路径都能被统一撤销。实验完成后,专用集群与两条虚构密码一起被丢弃,不把这套仅为观察行为而设计的认证配置留在运行环境中。

在消费者需要重新连接之前完成更新

选择有效期时,应为获取替换值、分发、刷新消费进程,以及从有限中断中恢复预留时间。监控验证方剩余有效时间,也要观察每个消费者实际载入值的版本或年龄。管理清单显示已经产生新版本,并不证明运行中的应用已经使用新值,更不能代替一次真正的新连接测试。

平台的切换模型允许重叠时,应先用替换凭据完成新的认证,再退休旧版本。之后确认原凭据无法建立新访问,并单独决定是否排空或终止既有会话。发生泄漏时,紧急撤销可能优先于无中断切换;不能因为期限将来会到,就推迟当下应实施的撤销动作。流程目标是控制实际接受行为,而不是维持一个看起来整齐的轮换日历。

保持时钟同步,避免续期失败就悄悄延长有效期。记录被测主体、版本、验证方和期限,不记录秘密值。有效证据应包含预期边界前能接受、边界后在目标路径上被拒绝,以及对既有会话影响的清楚认识。管理界面显示了一个日期,只说明配置里存在这个字段,还没有证明系统执行了相应的访问限制。

延伸阅读

来源