代码之家  ›  专栏  ›  技术社区  ›  JHH

如何实现零停机键轮换

  •  0
  • JHH  · 技术社区  · 5 年前

    我有几个微服务在AWS中运行,其中一些微服务彼此通信,其中一些微服务有外部客户或是外部服务的客户。

    要实现我的服务,我需要一些秘密(RSA密钥对用于签名/验证令牌、对称密钥、API密钥等)。我使用的是AWS SecretsManager,它工作得很好,但是我现在正在为键旋转实现适当的支持,我有一些想法。

    • 我正在使用AWS SecretsManager,定期(大约5分钟)获取秘密并在本地缓存它们。
    • 我正在使用AWS SecretsManager的版本阶段功能,根据需要引用当前版本和先前版本。

    假设服务A需要服务B的密钥K:

    • 假设在开始时,k的当前值为k1,前一个值为k0。
    • 服务A在与B通信时始终使用(和本地缓存)当前版本的k,因此在本例中是k1
    • 服务B将在本地缓存中保持版本awscurrent和awsprevious,并接受这两个版本[k1,k0]
    • 当旋转k时,我首先确保服务b使用的秘密被旋转,这样在刷新间隔结束后,服务b的所有实例都接受[k2,k1]而不是[k1,k0]。在刷新间隔结束之前,的所有实例仍使用k1。
    • 当刷新间隔已过,意味着B的所有实例都必须已获取k2时,我旋转键进行服务,以便A将使用k1或k2,直到刷新间隔已过,然后仅使用k2。
    • 这就完成了键的旋转(但是如果k1被认为受到了影响,我们可以再次旋转b的秘密来推出k1并得到[k3,k2])。

    这是最好的方法还是有其他方法可以考虑?

    然后,在某些情况下,我有一个对称的密钥j,它在同一个服务中使用,例如一个密钥来加密某些会话。因此,在对服务C的一个请求中,一个会话是用密钥j1加密的,随后需要用j1解密。我有多个C服务实例。

    这里的问题是,如果加密和解密同时使用相同的秘密,则旋转它会变得更混乱-如果将密钥旋转为具有值j2,并且一个实例已刷新,以便使用j2进行加密,而另一个实例仍看不到j2,则解密将失败。

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

    1. 通过单独的旋转方案分为两个秘密,一次旋转一个,与上面类似。这就增加了处理额外机密的开销,这些机密具有相同的值(除了它们之间的旋转时间)。

    2. 如果解密失败,将强制刷新机密:

      • 加密始终使用awsccurrent(j1或j2,取决于是否刷新)
      • 解密将先尝试awscurrent,然后尝试awsprevious,如果两者都失败(因为存储了另一个使用j2和j1的实例的加密),则会请求手动刷新该机密([j2,j1]现在已存储),然后再次尝试awscurrent和awsprevious。
    3. 在“密钥”窗口中使用三个密钥,并始终使用中间的密钥加密,因为它应始终位于所有其他实例的窗口中(除非它旋转了几次,速度比刷新间隔快)。这增加了复杂性。

    还有其他的选择吗?这看起来像是一个标准的用例,但是我仍然很难找到最好的方法。

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

    根据Joeb的答案,到目前为止我提出的算法是: 假设秘密最初的值为k1,而挂起的值为空。

    正常运行

    • 所有服务定期(每t秒)查询SecretsManager AWSCURRENT , AWSPENDING 和自定义标签 ROTATING 接受所有服务(如果存在)->所有服务都接受[ AWS电流 = K1]
    • 所有客户使用 AWS电流 = K1

    密钥旋转

    1. 为挂起阶段添加新值k2
    2. 等待t秒->所有服务现在接受[ AWS电流 = K1, 消费支出 = K2]
    3. 添加 旋转的 到K1版本+移动 AWS电流 至k2版本+删除 消费支出 来自k2的标签(标签似乎没有原子交换)。在T秒过去之前,一些客户机将使用k2和一些k1,但所有服务都接受这两者
    4. 等待t秒->所有服务仍接受[ AWS电流 = K2, 消费支出 =k1]和所有客户端使用 AWS电流 = K2
    5. 移除 旋转的 从K1阶段开始。请注意,k1仍然具有 AWSPREVIOUS 阶段。
    6. T秒后,所有服务将只接受[ AWS电流 =k2],而k1实际上是死的。

    这既适用于单独的机密,也适用于用于加密和解密的对称机密。

    不幸的是,我不知道如何使用内置的旋转机制来实现这一点,因为它需要几个步骤,其间会有延迟。一个想法是发明一些定制的步骤 setSecret 步骤创建一个cloudwatch cron事件,该事件将在t秒后再次调用该函数,并用步骤调用它 swapPending removePending . 如果SecretsManager能够自动支持这一点,比如通过支持函数返回一个值来指示下一步应该在t秒后调用,那就太棒了。

    1 回复  |  直到 5 年前
        1
  •  3
  •   JoeB    5 年前

    对于凭证问题,只要服务B支持两个活动凭证,就不必在应用程序中同时保留当前和以前的凭证。要做到这一点,您必须确保凭证在准备就绪之前没有标记为“当前”。然后应用程序总是获取并使用当前凭证。要在旋转lambda中执行此操作,请执行以下步骤:

    1. 将新的凭证存储在Secrets Manager中,并带有阶段标签awExpending(如果在创建阶段时通过了阶段,则不会将该秘密标记为awscurrent)。另外,在创建秘密时使用提供给lambda的等幂标记,这样在重试时就不会创建重复项。
    2. 在awspending阶段将存储在Secrets Manager中的秘密作为凭证添加到服务B中。
    3. 验证您是否可以使用awspending凭证登录到服务B。
    4. 将支出凭证的阶段更改为“当前”。

    这与Secrets Manager在创建多用户RDS旋转lambda时所采取的步骤相同。一定要使用awspending标签,因为Secrets Manager会专门处理这个问题。如果服务B不支持两个活动凭据或多个用户共享数据,则可能无法实现这一点。见 secrets manager rotation docs 对此。

    此外,Secrets Manager旋转引擎是异步的,将在失败后重试(这就是为什么每个lambda步骤必须是等幂的)。有一组初始的重试(大约5次),之后每天都有一些重试。您可以利用这一点,通过异常使第三步(测试秘密)失败,直到满足传播条件为止。或者,您可以将lambda执行时间增加到 15 minutes 并休眠适当的时间,等待传播完成。不过,睡眠法的缺点是不必要地占用资源。

    请记住,一旦拆下挂起的阶段或移动到挂起的阶段,旋转引擎将停止。如果应用程序B接受当前和挂起(或当前、挂起和上一个,如果您想更加安全),那么如果您添加所描述的延迟,上述四个步骤将起作用。你也可以看看 AWS Secrets Manager Sample Lambdas 有关如何操作阶段以进行数据库旋转的示例。

    对于您的加密问题,我看到的最好的方法是用加密数据存储加密密钥的标识符。因此,当您使用键j1加密数据d1时,您要么存储数据,要么以其他方式传递给下游应用程序,比如将secret arn和version(比如v)传递给应用程序。如果服务A在消息M(…)中向服务B发送加密数据,它的工作方式如下:

    1. A获取阶段A当前(由ARN和版本V1标识)的键J1。
    2. a使用键j1将数据d1加密为e1,并以消息m1(anr,v1,e1)的形式发送给b。
    3. 随后,j1旋转到j2,j2标记为“A当前”。
    4. A获取阶段A当前(由ARN和V2标识)的键J2。
    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来解密,然后使用数据密钥来解密有效负载。

    推荐文章