← 中文资源

简体中文版本

凭据轮换(Secret Rotation)

凭据轮换按计划或在泄露后更换密码、API Key 等凭据,通过重叠有效期让使用方无中断切换。

阅读英文原文

轮换是一套流程

生成新口令只是最简单的一步。轮换是完整闭环:创建新版本、分发、让使用方改用、确认流量健康,最后才撤销旧版本。跳过中间步骤,结果不是故障,就是凭据其实从未真正更换。

工作机制

轮换任务向密钥存储申请新版本,更新目标系统,并写入 AWSCURRENT 之类的版本标签。旧值转为 AWSPREVIOUS,在重叠窗口内继续有效。使用方在启动时以及收到刷新信号时读取当前版本,然后重连。

数据库最安全的做法是为同一角色创建第二个凭据,部署后等待连接更替,再删除第一个。API Key 可以同时接受两个,并公布哪个是主用。签名密钥则在用新密钥签名的同时,用两个密钥验证。

何时使用与取舍

长期凭据按固定周期轮换;怀疑泄露后立即轮换;有权限的人员离职时轮换。高价值密钥可以数小时一轮,低风险内部密钥可以按季度。轮换不能替代短期凭据,后者能彻底消除问题。

高频轮换提高重载失败的概率,尤其是带缓存客户端和长连接时。重叠窗口让两个密钥同时有效,略微扩大暴露面。自动轮换增加基础设施,并要求应用配合;没有重载路径时,自动化只会制造漂移。

示例与常见误解

某服务启动时缓存数据库口令,之后从不刷新。自动化轮换后,下一次连接失败。加入重载钩子和双凭据窗口后,同样的轮换对用户无感。

常见误解是密钥管理器会轮换应用。它轮换的是值,采用新值必须由应用完成。相关概念:密钥管理(Secret Management)、密钥轮换、最小权限。

代码示例

import hmac
import secrets

ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'


def api_key(prefix: str = 'sk') -> str:
    body = ''.join(secrets.choice(ALPHABET) for _ in range(40))
    return f'{prefix}_{body}'


def url_token(nbytes: int = 32) -> str:
    return secrets.token_urlsafe(nbytes)


def otp_code(digits: int = 6) -> str:
    # 朴素做法 random.randint 可预测,必须用 secrets
    floor = 10 ** (digits - 1)
    return str(floor + secrets.randbelow(9 * floor))


def constant_time_match(candidate: str, expected: str) -> bool:
    if len(candidate) != len(expected):
        return False
    return hmac.compare_digest(
        candidate.encode(), expected.encode()
    )


def scoped_token(scope: str, nbytes: int = 16) -> dict:
    return {
        'token': url_token(nbytes),
        'scope': scope,
        'hint': f'scope={scope}',
    }


key = api_key()
code = otp_code()
print('key length:', len(key), 'otp:', code)
print('compare ok:', constant_time_match(code, code))
print('compare tampered:', constant_time_match(code, '123456'))
package main

import (
	"crypto/sha256"
	"crypto/subtle"
	"fmt"
)

func ConstantTimeEqual(a, b string) bool {
	left := sha256.Sum256([]byte(a))
	right := sha256.Sum256([]byte(b))
	return subtle.ConstantTimeCompare(left[:], right[:]) == 1
}

func main() {
	secret := "s3cr3t-token"
	fmt.Println(ConstantTimeEqual(secret, "s3cr3t-token"))
	fmt.Println(ConstantTimeEqual(secret, "s3cr3t-tokeX"))
	fmt.Println(ConstantTimeEqual(secret, "short"))
}