【问题标题】:How to implement zero-downtime key rotation如何实现零停机密钥轮换
【发布时间】:2019-01-25 09:44:52
【问题描述】:

我在 AWS 中运行了几个微服务,其中一些相互通信,其中一些具有外部客户端或作为外部服务的客户端。

为了实现我的服务,我需要一些秘密(用于签名/验证令牌的 RSA 密钥对、对称密钥、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,解密将失败。

我可以在这里看到一些方法:

  1. 使用单独的轮换方案将密钥拆分为两个密钥,并一次轮换一个,与上述类似。这增加了要处理的额外机密的开销,具有相同的值(除了它们之间有一段时间轮换)

  2. 让解密在失败时强制刷新秘密:

    • 加密始终使用 AWSCURRENT(J1 或 J2,取决于是否刷新)
    • 解密将尝试 AWSCURRENT 然后 AWSPREVIOUS,如果两者都失败(因为另一个实例使用 J2 和 [J1, J0] 进行加密)将请求手动刷新密钥([J2, J1] 现在已存储),然后再次尝试 AWSCURRENT 和 AWSPREVIOUS。
  3. 在密钥窗口中使用三个密钥并始终使用中间的一个加密,因为它应该始终位于所有其他实例的窗口中(除非它被旋转多次,比刷新间隔更快)。这增加了复杂性。

还有哪些其他选择?这似乎是一个标准用例,但我仍然在努力寻找最佳方法。

编辑 ------------------

根据 JoeB 的回答,到目前为止我提出的算法是这样的: 假设最初秘密的 CURRENT 值为 K1,PENDING 值为 null。

正常操作

  • 所有服务定期(每 T 秒)向 SecretsManager 查询 AWSCURRENTAWSPENDING 和自定义标签 ROTATING 并全部接受(如果存在)-> 所有服务都接受 [AWSCURRENT=K1]李>
  • 所有客户端都使用AWSCURRENT=K1

按键轮换

  1. 为 PENDING 阶段设置一个新值 K2
  2. 等待 T 秒 -> 现在所有服务都接受 [AWSCURRENT=K1, AWSPENDING=K2]
  3. ROTATING 添加到K1 版本+ 将AWSCURRENT 移动到K2 版本+ 从K2 中删除AWSPENDING 标签(似乎没有标签的原子交换)。直到 T 秒过去,一些客户端将使用 K2 和一些 K1,但所有服务都接受两者
  4. 等待 T 秒 -> 所有服务仍然接受 [AWSCURRENT=K2, AWSPENDING=K1] 并且所有客户端使用 AWSCURRENT=K2
  5. 从 K1 中移除 ROTATING 阶段。请注意,K1 仍将具有 AWSPREVIOUS 阶段。
  6. T 秒后,所有服务将只接受 [AWSCURRENT=K2],K1 实际上已死。

这应该适用于单独的秘密和用于加密和解密的对称秘密。

不幸的是,我不知道如何为此使用内置的旋转机制,因为它需要几个步骤,中间有延迟。一种想法是发明一些自定义步骤并让setSecret 步骤创建一个 CloudWatch cron 事件,该事件将在 T 秒后再次调用该函数,并使用步骤 swapPendingremovePending 调用它。如果 SecretsManager 可以自动支持这一点,那就太棒了,例如支持函数返回一个值,指示应该在 T 秒后调用下一步。

【问题讨论】:

    标签: encryption secret-key aws-secrets-manager shared-secret


    【解决方案1】:

    对于您的凭据问题,只要服务 B 支持两个活动凭据,您就不必在应用程序中保留当前和以前的凭据。为此,您必须确保在准备好之前不会将凭证标记为 AWSCURRENT。然后应用程序总是获取并使用 AWSCURRENT 凭证。要在旋转 lambda 中执行此操作,您需要执行以下步骤:

    1. 使用阶段标签 AWSPENDING 将新凭证存储在 Secrets Manager 中(如果您在创建密钥时通过阶段,则未标记 AWSCURRENT)。还可以使用在创建密钥时提供给 lambda 的幂等性令牌,这样您就不会在重试时创建重复项。
    2. 获取 AWSPENDING 阶段下存储在 Secrets Manager 中的密钥,并将其添加为服务 B 中的凭据。
    3. 验证您是否可以使用 AWSPENDING 凭据登录到服务 B。
    4. 将 AWSPENDING 凭证的阶段更改为 AWSCURRENT。

    这些步骤与 Secrets Manager 在创建多用户 RDS 轮换 lambda 时所采取的步骤相同。请务必使用 AWSPENDING 标签,因为 Secrets Manager 对此进行了特殊处理。如果服务 B 不支持两个活动凭据或多个用户共享数据,则可能没有办法做到这一点。请参阅secrets manager rotation docs

    此外,Secrets Manager 轮换引擎是异步的,并且会在失败后重试(这就是为什么每个 Lambda 步骤必须是幂等的)。有一组初始重试(大约 5 次),然后每天进行一些重试。您可以通过异常失败第三步(测试秘密)来利用这一点,直到满足传播条件。或者,您可以将 Lambda 执行时间提高到 15 minutes 并休眠适当的时间以等待传播完成。但是 sleep 方法的缺点是会不必要地占用资源。

    请记住,一旦您删除挂起阶段或将 AWSCURRENT 移动到挂起阶段,轮换引擎就会停止。如果应用程序 B 接受当前和待处理(或当前、待处理和上一个,如果您想更加安全),如果您添加您描述的延迟,上述四个步骤将起作用。您还可以查看AWS Secrets Manager Sample Lambdas,了解如何为数据库轮换操作阶段的示例。

    对于您的加密问题,我见过的最佳方法是将加密密钥的标识符与加密数据一起存储。因此,当您使用密钥 J1 加密数据 D1 时,您可以存储或以其他方式传递给下游应用程序,例如秘密 ARN 和应用程序的版本(例如 V)。如果服务 A 在消息 M(...) 中向服务 B 发送加密数据,它将按如下方式工作:

    1. A 获取阶段 AWSCURRENT 的密钥 J1(由 ARN 和版本 V1 标识)。
    2. A 使用密钥 J1 将数据 D1 加密为 E1,并在消息 M1(ANR, V1, E1) 中将其发送给 B。
    3. 后来 J1 旋转到 J2 并且 J2 被标记为 AWSCURRENT。
    4. A 获取阶段 AWSCURRENT 的密钥 J2(由 ARN 和 V2 标识)。
    5. A 使用密钥 J2 将数据 D2 加密为 E2,并在消息 M2(ANR, V2, E2) 中将其发送给 B。
    6. B 接收到 M1 并通过指定 ARN、V1 获取密钥 (J1) 并解密 E1 以获得 D1。
    7. B 接收到 M2 并通过指定 ARN、V2 获取密钥 (J2) 并解密 E2 以获得 D2。

    请注意,A 和 B 都可以缓存密钥。如果要长期存储加密数据,则必须确保在加密数据不再存在或重新存储之前不会删除密钥- 使用当前密钥加密。您还可以通过传递不同的 ARN 来使用多个密钥(而不是版本)。

    另一种选择是使用KMS 进行加密。服务 A 将发送加密的 KMS 数据密钥而不是密钥标识符以及加密的有效负载。加密后的KMS数据密钥可以被B调用KMS解密,然后使用数据密钥解密payload。

    【讨论】:

    • 谢谢,这是很有价值的信息,让我意识到我没有足够详细地研究 AWS SM 文档轮换。让我烦恼的是,内置的旋转 lambda 机制将以某种同步的方式调用。我没有办法告诉服务使用新秘密,而是定期刷新秘密。并不是说这是一种理想的方法,但如果旋转步骤可以更加异步以便 SET_SECRET 步骤可以是例如“等待 5 分钟”。
    • 即使我有办法为所有服务实例提供新的秘密,也不一定能足够快地适应被称为内置轮换 lambda 机制的时间接二连三地。我可能需要求助于手动轮换方案。
    • 我用我目前正在进行的工作更新了我的问题。非常感谢您的指点。顺便说一句,是的,将密钥 ID 与加密的有效负载一起存储也是一个好主意。
    • 我更新了我的答案,提到旋转引擎是异步的,将重试。这可用于使最后一步失败并在大约一分钟后重试。或者,您可以在 Lambda 控制台中编辑 lambda 并增加最大执行时间并在最后一步中休眠。交换 AWSCURENT 和 AWSPENDING 阶段可能不起作用,因为轮换引擎会跟踪 AWSPENDING 阶段,等待它与 AWSCURRENT 收敛,因此它知道轮换已完成。要执行此类操作,您可能需要放弃 Secrets Manager 轮换引擎。
    • 我更新了我的答案。请记住,一旦您删除挂起阶段或将 AWSCURRENT 移动到挂起阶段,轮换引擎就会停止。我认为只要 B 接受当前和待处理(或当前、待处理和上一个),你之前所拥有的就可以在睡眠中使用。应该不需要添加额外的阶段或交换阶段。
    猜你喜欢
    • 1970-01-01
    • 2011-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多