亚马逊AWS官方博客

使用 Amazon EventBridge 和 AWS Lambda 在 AWS Secrets Manager 自动创建和备份密钥历史版本

一. 前言

目前 AWS Secrets Manager 默认情况下只会自动保留一个历史版本。如果需要保留多个历史版本则需要通过 CLI 或者 API 手动打上并管理 staging label 暂存标签。AWS Secrets Manager 目前支持添加 20 个暂存标签。由于不能通过 Management Console UI 控制面板来操作,对于客户来说使用这种方法保留和回溯历史版本变得比较困难。

本文将介绍通过使用 Amazon EventBridge 和 AWS Lambda 在 AWS  Secrets Manager 自动创建保留密钥历史版本的方案。这个方案能够为客户的每一次密钥更改自动生成 staging label 并保留最新的 20 个历史版本,不需要人为干预和手动操作。方案支持将密钥以及历史版本保存在当前区域,也可以跨区域跨账号保存和备份。

二. 原地创建版本架构设计

原理

通过 Amazon EventBridge 捕捉 Secrets Manager 密钥更新事件的形式,触发 Lambda 来处理事件并在密钥自动打上 staging label 来保留多个版本。同时因为 Secrets Manager 限制在一个密钥下 staging label 必须唯一且数量不能超过 20 个, Lambda 会自动为备份的密钥生成唯一的标签且在达到标签上限时自动删除最老的版本上的标签以保留最新的版本。

详细配置

  • 创建一个 AWS Lambda Python 函数命名为 SecretsVersionFunction,代码如下:
    import json
    import boto3
    import logging
    from datetime import datetime
    from traceback import print_exc
    
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    secretsmanager_client = boto3.client('secretsmanager')
    
    def describe_secret(secret_name):
        """Retrieve the source secret metadata."""
        logger.info(f"Retrieving source secret: {secret_name} metadata.")
    
        return secretsmanager_client.describe_secret(
            SecretId=secret_name
        )
    
    def get_previous_version(secret_data):
        for version_id, stages in secret_data["VersionIdsToStages"].items():
            if "AWSPREVIOUS" in stages:
                return version_id
        return None
    
    def lambda_handler(event, context):
        try:
            event_detail = event["detail"]
            event_name = event_detail["eventName"]
    
            if event_name == "PutSecretValue":
                secret_name = event_detail["requestParameters"]["secretId"]
                logger.info(f'Update staging label for secret: {secret_name}')
                secret_data = describe_secret(secret_name)
                prev_version_id = get_previous_version(secret_data)
                if(prev_version_id != None):
                    update_secret_version_stage(secret_name, secret_data, prev_version_id)
            return {
                'statusCode': 200
            }
        except Exception:
            print_exc()
            raise
    
    def update_secret_version_stage(secret_name, secret_data, version_id):
        #Update Staging Label for Secret Version
        max_labels = 22
        staging_labels = []
        version_stages=secret_data["VersionIdsToStages"]
        print(version_stages)
        for labels in version_stages.values():
            staging_labels.extend(labels)
        if len(staging_labels) == max_labels:
            logger.info(f"\"Maximum number of staging label reached. Remove the oldest label\"")
            oldest_label=None
            oldest_ver=None
            for key, labels in version_stages.items():
                for label in labels:
                    if oldest_label is None or label < oldest_label:
                        oldest_label = label
                        oldest_ver = key
            
            print(f"Oldest label: {oldest_label}")
            print(f"Corresponding version_id: {oldest_ver}")
            
            logger.info(f"\"Remove staging label: {oldest_label} for version_id {oldest_ver}\"")
            secretsmanager_client.update_secret_version_stage(
                SecretId=secret_name,
                VersionStage=oldest_label,
                RemoveFromVersionId=oldest_ver
            )
            
        logger.info(f"\"Update staging label for version_id {version_id}\"")
        secretsmanager_client.update_secret_version_stage(
            SecretId=secret_name,
            VersionStage=datetime.now().strftime("%Y%m%d%H%M%S"),
            MoveToVersionId=version_id
        )
    
  • 在 Lambda 函数的默认执行角色上添加以下策略:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Resource": "arn:aws:secretsmanager:<REGION>: <ACCOUNT_ID>:secret:*",
                "Action": [
                    "secretsmanager:DescribeSecret",
                    "secretsmanager:UpdateSecretVersionStage"
                ]
            }
        ]
    }
    
  • 打开 Amazon EventBridge,在事件总线,默认事件总线(default)中创建一个新的规则 SMCreateUpdateSecretsEvent。将事件模式设定为:
    {
      "detail-type": ["AWS API Call via CloudTrail"],
      "detail": {
        "eventSource": ["secretsmanager.amazonaws.com"],
        "eventName": ["CreateSecret", "PutSecretValue", "UpdateSecret"]
      },
      "source": ["aws.secretsmanager"]
    }
    
  • 事件目标类型设置为 Lambda 函数并选定之前创建的 SecretsVersionFunction 并保存。
  • 在 AWS Secrets Manager 中创建一个测试密钥并更新,可以观察到暂存标签被自动添加到了版本上:
  • 通过 AWS CloudWatch Lambda 命名空间 Errors 指标并设置告警通知可以监控 Lambda 函数的执行情况,确保自动备份过程正常运行。

三. 跨区域跨账号多版本备份架构设计

原理

通过 Amazon EventBridge 转发源区域和账号的事件到目标区域和账号,触发 AWS Lambda 函数来进行密钥备份和保存历史版本。

同时,这个方案利用 AWS Service Catalog 来创建和管理 AWS CloudFormation 的堆栈集以方便在多个账户和区域同时进行部署。

最后,方案会自动在备份账号中生成新的 KMS key 来加密原有密钥并在多区域场景下自动生成副本密钥。

详细配置

以需要将 AWS Secrets Manager 源账号东京区(ap-northeast-1)的 Secrets 备份到目标账号孟买(ap-south-1)为例:

  • 在 AWS Management Console 中打开 Service Catalog。如果需要跨账号备份,请登陆管理员账号 Administrator Account 部署此方案。
  • 如果已有产品组合,可跳过这个步骤 – 在 Service Catalog 服务中创建一个产品组合,完成后在产品组合访问权限中添加当前用户和需要访问权限的 IAM 主体(组、角色或用户), 记录创建的产品组合 ID。
  • 在目标账户的目标区域创建一个 S3 桶。克隆 aws-secrets-manager-p 并将 scripts/ 和 templates/ 文件夹中的文件上传至 S3 桶,注意所有文件需要同一个前缀。确保此存储桶具有一个策略,该策略允许密钥来源的 AWS 帐户以及备份目标帐户进行读取访问。Bucket policy 范例如下:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::<ACCOUNT_ID>:root"
                },
                "Action": "s3:ListBucket",
                "Resource": "arn:aws:s3:::<BUCKET_NAME>"
            },
            {
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::<ACCOUNT_ID>:root"
                },
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::<BUCKET_NAME>/*"
            }
        ]
    }
    
  • 选择 secrets_manager_backup.baseline.template.yml,复制 URL:
  • 打开 CloudFormation 创建新的堆栈,指定模版为 Amazon S3 URL 并提供 secrets_manager_backup.baseline.template.yml URL:
  • 创建完成后,打开 Service Catalog,在产品中选择“Secrets Manager Backup”,并选择“启动产品”:
  • 填写以下参数:
  • 点击“启动产品”后 Service Catalog 会自动将产品部署到目标区域。可以看到资源已经部署成功。
  • 选择 Lambda 服务,找到 SecretsManagerBackupBackupFunction 函数,在“配置”→“环境变量”中添加“OVERRIDE_ON_UPDATE”,值为“False”。
  • 部署完成,可以在源区域中创建/更新密钥,并观察密钥是否备份到目标区域和账号。

范例效果

本篇作者

罗建霖

亚马逊云科技解决方案架构师,负责基于亚马逊云科技云计算方案架构的咨询和设计。

殷雨濛

亚马逊云科技解决方案架构师,负责基于亚马逊云科技的云计算方案的架构设计。曾在 IBM、HPE 和花旗银行担任数据科学家和研发负责人,在 IT 咨询、数据科学和软件研发领域有丰富的实践经验。专注于推动技术创新,通过高效的解决方案设计,帮助企业实现业务转型和增长。