如何实现零停机密钥轮换
How to implement zero-downtime key rotation
我在 AWS 中有几个微服务 运行,其中一些相互通信,一些有外部客户端或作为外部服务的客户端。
为了实现我的服务,我需要一些秘密(RSA 密钥对 sign/verify 令牌、对称密钥、API 密钥等)。为此,我正在使用 AWS SecretsManager,它工作正常,但我现在正在实施对密钥轮换的适当支持,我有一些想法。
- 我正在使用 AWS SecretsManager,定期(约 5 分钟)获取机密并将它们缓存在本地。
- 我正在使用 AWS SecretsManager 的版本阶段功能来根据需要引用 AWSCURRENT 和 AWSPREVIOUS 版本。
假设服务 A 需要服务 B 的密钥 K:
- 假设在开始时,K 具有当前值 K1 和先前值 K0。
- 服务 A 将始终使用(并在本地缓存)K 的 AWSCURRENT 版本与 B 通信,因此在本例中为 K1
- 服务 B 将在其本地缓存中保留版本 AWSCURRENT 和 AWSPREVIOUS 并接受两者 [K1, K0]
- 在轮换 K 时,我首先确保轮换服务 B 使用的秘密,以便在刷新间隔结束后,服务 B 的所有实例都接受 [K2, K1] 而不是 [K1, K0]。在刷新间隔结束之前,A 的所有实例仍使用 K1。
- 当刷新间隔结束时,意味着 B 的所有实例必须已获取 K2,我轮换服务密钥,以便 A 将使用 K1 或 K2 直到刷新间隔结束,然后仅使用 K2。
- 至此密钥轮换完成(但如果认为K1被泄露,我们可以再次轮换B的秘密,推出K1并得到[K3,K2])。
这是最好的方法还是有其他方法可以考虑?
然后,在某些情况下,我有一个在同一服务中使用的对称密钥 J,例如用于加密某些会话的密钥。因此,在对服务 C 的一个请求中,会话使用密钥 J1 加密,然后需要在稍后阶段使用 J1 解密。我有多个 C 服务实例。
这里的问题是,如果加密和解密都使用相同的秘密,旋转它会变得更加混乱 - 如果密钥被旋转为具有值 J2 并且一个实例已经刷新以便它将使用 J2 加密,而另一个实例仍然没有看到 J2,解密将失败。
我可以在这里看到一些方法:
用单独的轮换方案拆分成两个秘密,一次轮换一个,与上述类似。这增加了处理额外秘密的开销,具有相同的值(除了它们之间有一段时间轮换)
让解密在失败时强制刷新秘密:
- 加密始终使用 AWSCURRENT(J1 或 J2,取决于是否刷新)
- 解密将尝试 AWSCURRENT 然后 AWSPREVIOUS,如果两者都失败(因为另一个实例使用 J2 和 [J1, J0] 的加密被存储)将请求手动刷新秘密([J2, J1] 现在被存储),然后再次尝试 AWSCURRENT 和 AWSPREVIOUS。
在密钥 window 中使用三个密钥并始终使用中间的密钥加密,因为它应该始终在所有其他实例的 window 中(除非它被轮换几次,比刷新间隔快)。这增加了复杂性。
还有哪些其他选择?这似乎是一个标准的用例,但我仍在努力寻找最佳方法。
编辑 ------------------
根据 JoeB 的回答,到目前为止我想出的算法是这样的:
假设最初秘密的当前值为 K1,待定值为 null。
正常运行
- 所有服务定期(每 T 秒)向 SecretsManager 查询
AWSCURRENT
、AWSPENDING
和自定义标签 ROTATING
并全部接受(如果存在)-> 所有服务接受 [ AWSCURRENT
=K1]
- 所有客户端使用
AWSCURRENT
=K1
密钥轮换
- 为 PENDING 阶段输入一个新值 K2
- 等待 T 秒 -> 所有服务现在接受 [
AWSCURRENT
=K1,AWSPENDING
=K2]
- 将
ROTATING
添加到K1版本+将AWSCURRENT
移动到K2版本+从K2删除AWSPENDING
标签(似乎没有标签的原子交换)。直到 T 秒过去,一些客户端将使用 K2 和一些 K1,但所有服务都接受两者
- 等待 T 秒 -> 所有服务仍然接受 [
AWSCURRENT
=K2, AWSPENDING
=K1] 并且所有客户端都使用 AWSCURRENT
=K2
- 从 K1 中删除
ROTATING
阶段。请注意,K1 仍将具有 AWSPREVIOUS
阶段。
- T秒后,所有服务将只接受[
AWSCURRENT
=K2],K1有效死亡
这应该适用于单独的秘密和用于加密和解密的对称秘密。
不幸的是,我不知道如何为此使用内置的旋转机制,因为它需要几个步骤,中间有延迟。一个想法是发明一些自定义步骤并让 setSecret
步骤创建一个 CloudWatch cron 事件,该事件将在 T 秒后再次调用该函数,使用步骤 swapPending
和 removePending
调用它。如果 SecretsManager 可以自动支持这一点,那就太棒了,例如通过支持函数 returns 一个指示下一步应该在 T 秒后调用的值。
对于您的凭据问题,只要服务 B 支持两个活动凭据,您就不必在应用程序中同时保留当前和以前的凭据。为此,您必须确保凭证在准备就绪之前未标记为 AWSCURRENT。然后应用程序总是获取并使用 AWSCURRENT 凭证。要在旋转 lambda 中执行此操作,您将采取以下步骤:
- 使用阶段标签 AWSPENDING 将新凭证存储在机密管理器中(如果您通过了创建机密的阶段,则机密未标记为 AWSCURRENT)。在创建密钥时也使用提供给 lambda 的幂等性令牌,这样您就不会在重试时创建重复项。
- 将存储在secrets manager 中AWSPENDING 阶段的secret 作为凭证添加到服务B 中。
- 确认您可以使用 AWSPENDING 凭证登录服务 B。
- 将 AWSPENDING 凭证的阶段更改为 AWSCURRENT。
这些是机密管理器在创建多用户 RDS 轮换 lambda 时所采取的相同步骤。请务必使用 AWSPENDING 标签,因为 secrets manager 会对其进行特殊处理。如果服务 B 不支持两个活动凭据或多个用户共享数据,则可能无法执行此操作。请参阅此 secrets manager rotation docs。
此外,Secrets Manager 轮换引擎是异步的,并且会在失败后重试(这就是为什么每个 Lambda 步骤都必须是幂等的)。有一组初始重试(大约 5 次),然后是一些每日重试。您可以通过在满足传播条件之前通过异常使第三步(测试秘密)失败来利用这一点。或者,您可以将 Lambda 执行时间增加到 15 minutes 并休眠适当的时间以等待传播完成。不过,睡眠方法的缺点是会不必要地占用资源。
请记住,一旦您移除挂起阶段或将 AWSCURRENT 移至挂起阶段,轮换引擎就会停止。如果应用程序 B 接受当前和未决(或当前、未决和先前,如果你想更安全的话),如果你添加你描述的延迟,上面的四个步骤将起作用。您还可以查看 AWS Secrets Manager Sample Lambdas 以获取有关如何为数据库轮换操作阶段的示例。
对于您的加密问题,我见过的最好的方法是将加密密钥的标识符与加密数据一起存储。因此,当您使用密钥 J1 加密数据 D1 时,您可以存储或以其他方式传递给下游应用程序,例如秘密 ARN 和应用程序的版本(比如 V)。如果服务 A 在消息 M(...) 中向服务 B 发送加密数据,它将按如下方式工作:
- A 获取阶段 AWSCURRENT 的密钥 J1(由 ARN 和版本 V1 标识)。
- A使用密钥J1将数据D1加密为E1,并在消息M1(ANR, V1, E1)中发送给B。
- 稍后将 J1 旋转到 J2,并将 J2 标记为 AWSCURRENT。
- A 获取阶段 AWSCURRENT 的密钥 J2(由 ARN 和 V2 标识)。
- A使用密钥J2将数据D2加密为E2,并在消息M2(ANR, V2, E2)中发送给B。
- B收到M1,指定ARN取密钥(J1),V1解密E1得到D1
- B收到M2,通过指定ARN、V2取出密钥(J2),解密E2得到D2。
请注意,A和B都可以缓存密钥。如果要长期存储加密数据,则必须确保在加密数据不再存在或它不再存在之前不会删除密钥使用当前密钥重新加密。您还可以通过传递不同的 ARN 来使用多个密钥(而不是版本)。
另一种方法是使用KMS 进行加密。服务 A 将发送加密的 KMS 数据密钥而不是密钥标识符以及加密的有效负载。加密后的KMS数据密钥可以由B通过调用KMS进行解密,然后使用数据密钥解密payload。
我在 AWS 中有几个微服务 运行,其中一些相互通信,一些有外部客户端或作为外部服务的客户端。
为了实现我的服务,我需要一些秘密(RSA 密钥对 sign/verify 令牌、对称密钥、API 密钥等)。为此,我正在使用 AWS SecretsManager,它工作正常,但我现在正在实施对密钥轮换的适当支持,我有一些想法。
- 我正在使用 AWS SecretsManager,定期(约 5 分钟)获取机密并将它们缓存在本地。
- 我正在使用 AWS SecretsManager 的版本阶段功能来根据需要引用 AWSCURRENT 和 AWSPREVIOUS 版本。
假设服务 A 需要服务 B 的密钥 K:
- 假设在开始时,K 具有当前值 K1 和先前值 K0。
- 服务 A 将始终使用(并在本地缓存)K 的 AWSCURRENT 版本与 B 通信,因此在本例中为 K1
- 服务 B 将在其本地缓存中保留版本 AWSCURRENT 和 AWSPREVIOUS 并接受两者 [K1, K0]
- 在轮换 K 时,我首先确保轮换服务 B 使用的秘密,以便在刷新间隔结束后,服务 B 的所有实例都接受 [K2, K1] 而不是 [K1, K0]。在刷新间隔结束之前,A 的所有实例仍使用 K1。
- 当刷新间隔结束时,意味着 B 的所有实例必须已获取 K2,我轮换服务密钥,以便 A 将使用 K1 或 K2 直到刷新间隔结束,然后仅使用 K2。
- 至此密钥轮换完成(但如果认为K1被泄露,我们可以再次轮换B的秘密,推出K1并得到[K3,K2])。
这是最好的方法还是有其他方法可以考虑?
然后,在某些情况下,我有一个在同一服务中使用的对称密钥 J,例如用于加密某些会话的密钥。因此,在对服务 C 的一个请求中,会话使用密钥 J1 加密,然后需要在稍后阶段使用 J1 解密。我有多个 C 服务实例。
这里的问题是,如果加密和解密都使用相同的秘密,旋转它会变得更加混乱 - 如果密钥被旋转为具有值 J2 并且一个实例已经刷新以便它将使用 J2 加密,而另一个实例仍然没有看到 J2,解密将失败。
我可以在这里看到一些方法:
用单独的轮换方案拆分成两个秘密,一次轮换一个,与上述类似。这增加了处理额外秘密的开销,具有相同的值(除了它们之间有一段时间轮换)
让解密在失败时强制刷新秘密:
- 加密始终使用 AWSCURRENT(J1 或 J2,取决于是否刷新)
- 解密将尝试 AWSCURRENT 然后 AWSPREVIOUS,如果两者都失败(因为另一个实例使用 J2 和 [J1, J0] 的加密被存储)将请求手动刷新秘密([J2, J1] 现在被存储),然后再次尝试 AWSCURRENT 和 AWSPREVIOUS。
在密钥 window 中使用三个密钥并始终使用中间的密钥加密,因为它应该始终在所有其他实例的 window 中(除非它被轮换几次,比刷新间隔快)。这增加了复杂性。
还有哪些其他选择?这似乎是一个标准的用例,但我仍在努力寻找最佳方法。
编辑 ------------------
根据 JoeB 的回答,到目前为止我想出的算法是这样的: 假设最初秘密的当前值为 K1,待定值为 null。
正常运行
- 所有服务定期(每 T 秒)向 SecretsManager 查询
AWSCURRENT
、AWSPENDING
和自定义标签ROTATING
并全部接受(如果存在)-> 所有服务接受 [AWSCURRENT
=K1] - 所有客户端使用
AWSCURRENT
=K1
密钥轮换
- 为 PENDING 阶段输入一个新值 K2
- 等待 T 秒 -> 所有服务现在接受 [
AWSCURRENT
=K1,AWSPENDING
=K2] - 将
ROTATING
添加到K1版本+将AWSCURRENT
移动到K2版本+从K2删除AWSPENDING
标签(似乎没有标签的原子交换)。直到 T 秒过去,一些客户端将使用 K2 和一些 K1,但所有服务都接受两者 - 等待 T 秒 -> 所有服务仍然接受 [
AWSCURRENT
=K2,AWSPENDING
=K1] 并且所有客户端都使用AWSCURRENT
=K2 - 从 K1 中删除
ROTATING
阶段。请注意,K1 仍将具有AWSPREVIOUS
阶段。 - T秒后,所有服务将只接受[
AWSCURRENT
=K2],K1有效死亡
这应该适用于单独的秘密和用于加密和解密的对称秘密。
不幸的是,我不知道如何为此使用内置的旋转机制,因为它需要几个步骤,中间有延迟。一个想法是发明一些自定义步骤并让 setSecret
步骤创建一个 CloudWatch cron 事件,该事件将在 T 秒后再次调用该函数,使用步骤 swapPending
和 removePending
调用它。如果 SecretsManager 可以自动支持这一点,那就太棒了,例如通过支持函数 returns 一个指示下一步应该在 T 秒后调用的值。
对于您的凭据问题,只要服务 B 支持两个活动凭据,您就不必在应用程序中同时保留当前和以前的凭据。为此,您必须确保凭证在准备就绪之前未标记为 AWSCURRENT。然后应用程序总是获取并使用 AWSCURRENT 凭证。要在旋转 lambda 中执行此操作,您将采取以下步骤:
- 使用阶段标签 AWSPENDING 将新凭证存储在机密管理器中(如果您通过了创建机密的阶段,则机密未标记为 AWSCURRENT)。在创建密钥时也使用提供给 lambda 的幂等性令牌,这样您就不会在重试时创建重复项。
- 将存储在secrets manager 中AWSPENDING 阶段的secret 作为凭证添加到服务B 中。
- 确认您可以使用 AWSPENDING 凭证登录服务 B。
- 将 AWSPENDING 凭证的阶段更改为 AWSCURRENT。
这些是机密管理器在创建多用户 RDS 轮换 lambda 时所采取的相同步骤。请务必使用 AWSPENDING 标签,因为 secrets manager 会对其进行特殊处理。如果服务 B 不支持两个活动凭据或多个用户共享数据,则可能无法执行此操作。请参阅此 secrets manager rotation docs。
此外,Secrets Manager 轮换引擎是异步的,并且会在失败后重试(这就是为什么每个 Lambda 步骤都必须是幂等的)。有一组初始重试(大约 5 次),然后是一些每日重试。您可以通过在满足传播条件之前通过异常使第三步(测试秘密)失败来利用这一点。或者,您可以将 Lambda 执行时间增加到 15 minutes 并休眠适当的时间以等待传播完成。不过,睡眠方法的缺点是会不必要地占用资源。
请记住,一旦您移除挂起阶段或将 AWSCURRENT 移至挂起阶段,轮换引擎就会停止。如果应用程序 B 接受当前和未决(或当前、未决和先前,如果你想更安全的话),如果你添加你描述的延迟,上面的四个步骤将起作用。您还可以查看 AWS Secrets Manager Sample Lambdas 以获取有关如何为数据库轮换操作阶段的示例。
对于您的加密问题,我见过的最好的方法是将加密密钥的标识符与加密数据一起存储。因此,当您使用密钥 J1 加密数据 D1 时,您可以存储或以其他方式传递给下游应用程序,例如秘密 ARN 和应用程序的版本(比如 V)。如果服务 A 在消息 M(...) 中向服务 B 发送加密数据,它将按如下方式工作:
- A 获取阶段 AWSCURRENT 的密钥 J1(由 ARN 和版本 V1 标识)。
- A使用密钥J1将数据D1加密为E1,并在消息M1(ANR, V1, E1)中发送给B。
- 稍后将 J1 旋转到 J2,并将 J2 标记为 AWSCURRENT。
- A 获取阶段 AWSCURRENT 的密钥 J2(由 ARN 和 V2 标识)。
- A使用密钥J2将数据D2加密为E2,并在消息M2(ANR, V2, E2)中发送给B。
- B收到M1,指定ARN取密钥(J1),V1解密E1得到D1
- B收到M2,通过指定ARN、V2取出密钥(J2),解密E2得到D2。
请注意,A和B都可以缓存密钥。如果要长期存储加密数据,则必须确保在加密数据不再存在或它不再存在之前不会删除密钥使用当前密钥重新加密。您还可以通过传递不同的 ARN 来使用多个密钥(而不是版本)。
另一种方法是使用KMS 进行加密。服务 A 将发送加密的 KMS 数据密钥而不是密钥标识符以及加密的有效负载。加密后的KMS数据密钥可以由B通过调用KMS进行解密,然后使用数据密钥解密payload。