如何实现零停机键旋转

时间:2019-01-22 16:29:53

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

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

要实现我的服务,我需要一些秘密(RSA密钥对来签名/验证令牌,对称密钥,API密钥等)。我正在为此使用AWS SecretsManager,并且工作正常,但是我正在为密钥旋转实施适当的支持,并且我有几点想法。

  • 我正在使用AWS SecretsManager,定期(约5分钟)获取秘密并将其本地缓存。
  • 根据需要,我正在使用AWS SecretsManager的版本阶段功能来引用AWSCURRENT和AWSPREVIOUS版本。

比方说,服务A需要为服务B提供密钥K:

  • 比方说,K具有当前值K1和先前的值K0。
  • 服务A在与B进行通信时将始终使用(并在本地缓存)K的AWSCURRENT版本,因此在这种情况下为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,并接受它们(如果存在)->所有服务都接受[{{ 1}} = K1]
  • 所有客户端都使用AWSCURRENT = K1

按键旋转

  1. 在PENDING阶段输入新值K2
  2. 等待T秒->现在所有服务都接受[AWSCURRENT = K1,AWSCURRENT = K2]
  3. AWSPENDING添加到K1版本中,然后将ROTATING移到K2版本中,然后从K2中删除AWSCURRENT标签(似乎没有原子交换标签)。直到T秒过去,有些客户端将使用K2和某些K1,但是所有服务都接受这两者
  4. 等待T秒->所有服务仍接受[AWSPENDING = K2,AWSCURRENT = K1],所有客户端都使用AWSPENDING = K2
  5. 从K1移除AWSCURRENT阶段。请注意,K1仍将处于ROTATING阶段。
  6. 在T秒之后,所有服务将只接受[AWSPREVIOUS = K2],而K1实际上已失效。

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

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

1 个答案:

答案 0 :(得分:3)

对于您的凭证问题,只要服务B支持两个活动凭证,就不必在应用程序中同时保留当前凭证和先前凭证。为此,您必须确保在准备好凭据之前,不要将其标记为AWSCURRENT。然后,应用程序始终总是获取并使用AWSCURRENT凭证。为此,请在轮播Lambda中执行以下步骤:

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

这些是机密管理器在创建多用户RDS旋转lambda时采取的相同步骤。确保使用AWSPENDING标签,因为密钥管理器会对此加以特殊对待。如果服务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发送加密的数据,它将按以下方式工作:

  1. A获取阶段AWSCURRENT(由ARN和版本V1标识)的密钥J1。
  2. A使用密钥J1将数据D1加密为E1,并将其在消息M1(ANR,V1,E1)中发送给B。
  3. 稍后将J1旋转到J2,并将J2标记为AWSCURRENT。
  4. A获取阶段AWSCURRENT(由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数据密钥而不是密钥标识符以及加密的有效负载。 B可以通过调用KMS来解密已加密的KMS数据密钥,然后使用该数据密钥来解密有效载荷。