你们把 Lambda 函数的凭据保存在哪里?
如果您的 Lambda 函数需要访问数据库(或任何其他需要凭据的服务),您将该配置存储在哪里以及如何存储?
最近我们一直在迭代我们的 MVP,应用程序的需求和规模有所增长,我们一直在讨论如何安全地处理不同环境/阶段的数据库配置以及相关的用户/密码。
有很多种可能性,我们来看其中一些:
只需将主机名、用户名和密码硬编码到文件中即可。
请不要这样。我真的需要告诉你为什么吗?
使用 .env 文件——该文件已提交到仓库中。
尽管这种方案可能提供更大的灵活性,但仍然非常糟糕。任何能够访问你代码仓库的人都能立即看到你的凭据。
使用 .secrets 文件(基本上就是上面的 .env 文件,但通过serverless secrets 插件加密)。
这是我们最初采取的快速方法,但效果并不理想,因为:
- Lambda 函数部署完成后,凭证会在 AWS UI 控制台中清晰可见(环境变量在部署时已嵌入到代码中)。
- 有人误将解密文件上传的风险很高。
- 我们不得不在多个使用类似凭据的存储库中复制这些文件。
- 最重要的是,出现了一个问题——我们应该把解密这些秘密的密码存储在哪里?
plugins:
- serverless-secrets-plugin
custom:
secrets: ${file(secrets.${self:provider.stage}.yml)}
在 serverless.yml 文件中使用 SSM 加密的环境变量
这比 secrets-plugin 更进一步,AWS Systems Manager Parameter Store允许您摆脱文件,只需一个配置即可供多个 Lambda 函数/存储库共享,并且可以通过 AWS UI 控制台或 AWS CLI 快速更新,但它也有同样的缺点:
- 配置值以明文形式存储在 Lambda 环境变量中——您可以在 AWS Lambda 控制台中看到它们——如果该函数被攻击者攻破(攻击者将可以访问 process.env),那么他们也能够轻松找到解密后的值——(此视频解释了具体方法)。
- 由于您将代码与环境变量一起部署,因此如果您需要更改配置,则需要重新部署每个 Lambda 函数以传播所有更改。
custom:
supersecret: ${ssm:/aws/reference/secretsmanager/secret_ID_in_Secrets_Manager~true}
在运行时访问 SSM 或 SecretsManager(并使用缓存)
将您的凭据安全地加密存储在 Systems Manager Parameter Store 或Secrets Manager中(后者还支持自动轮换),并在运行时访问它们。
然后配置您的无服务器 YAML 文件,通过 IAMRole 策略授予 Lambda 函数访问权限:
iamRoleStatements:
- Effect: Allow
Action:
- ssm:GetParameter
Resource:"arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/YOUR_PARAMETER"
您可以设置越来越细粒度的权限级别。
"arn:aws:ssm:*:*:parameter/*"
"arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/*"
"arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/YOUR_PARAMETER-*"
"arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:parameter/YOUR_PARAMETER-SOME_MORE_SPECIFIC"
上面的代码直接指定了您的 ARN/区域/帐户 - 如果您想要更灵活的方式,您可以设置权限以自动获取这些值:
iamRoleStatements:
- Effect: Allow
Action:
- ssm:GetParameter
Resource:
- Fn::Join:
- ':'
- - arn:aws:ssm
- Ref: AWS::Region
- Ref: AWS::AccountId
- parameter/YOUR_PARAMETER-*
由于 SecretsManager 与 ParameterStore 集成,您可以通过 SSM 访问您的密钥,只需在您的密钥前加上 `<key>` 即可。aws/reference/secretsmanager/
如果您开始修改这些权限(尤其是在 UI 控制台中编辑策略而不是重新部署 Lambda 函数时,可能需要一些时间。通常只需几秒钟,但也可能需要 2-5 分钟)。
一旦您授予 Lambda 函数访问密钥的权限,您就可以指定一个环境变量,以便根据环境/阶段,简单地告诉 Lambda 函数在运行时加载哪些凭据:
custom:
credentialsKey:
production: YOUR-PRODUCTION-CREDENTIALS-KEY
development: YOUR-DEV-CREDENTIALS-KEY
other: YOUR-OTHER-CREDENTIALS-KEY
functions:
environment:
SECRETS_KEY:${self:custom.credentialsKey}
这是一个巧妙的小技巧,可以为无服务器部署应用条件判断。简单来说,就是告诉无服务器架构你有三个密钥:一个用于生产环境,一个用于开发环境,一个用于所有其他阶段。
然后在 Lambda 函数的环境节点中,根据当前部署阶段设置密钥。如果当前阶段与列表中的某个变量名匹配,则使用该密钥;否则,将回退到另一个密钥。
然后,在你的 Lambda 函数中,你只需要从 SSM 或 SecretsManager 加载凭据并连接到你的数据库即可。
const ssm = new AWS.SSM();
const params = {
Name: process.env.SECRETS_KEY,
WithDecryption: true
};
ssm.getParameter(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data.Parameter.Value); // here you have your values!
});
请记住要实施某种缓存机制,这样在重用 Lambda 容器时,就可以避免从 AWS 加载密钥(从而避免产生额外费用)。
我想指出的一点是,SSM 要求在实例化时定义 aws-region。如您所见,我并没有传递该值。这是因为该值process.env.AWS_REGION会自动从 AWS SDK 读取,并且该环境变量由 serverless offline 设置。
在您编写集成测试尝试加载密钥之前,您无需执行任何操作——我们添加了一些测试,以确保每次部署后,该环境阶段的密钥都可在 SecretsManager 上获取。在这种情况下,您必须将该变量传递给集成测试(请记住手动将其传递给集成测试)。
这是我们的 npm 脚本(我们使用AVA进行测试,使用Instanbul/nyc进行代码覆盖率测试):
"test:integration": "AWS_REGION=eu-west-1 SECRETS_KEY=MY_KEY_DEVSTAGE nyc ava tests-integration/**/*.*"
对于这种常见的(或者说基本/根本性的)特性,您还有其他处理方法吗?
更多相关资源:
https://docs.aws.amazon.com/en_us/systems-manager/latest/userguide/integration-ps-secretsmanager.html
https://serverless.com/framework/docs/providers/aws/guide/variables/#reference-variables-using-aws-secrets-manager