亚马逊AWS官方博客

在中国区实现 SCP 功能的替代方案

2019 年 3 月,AWS 发布了服务控制策略(Service Control Policies,SCP)功能。这是一种组织策略,可用于管理 AWS Organizations 中的权限。

截止到 2021 年 4 月,AWS 位于中国大陆地区的两个区域(BJS、ZHY)暂未支持 SCP 功能。下面将讨论一种替代方案,以便在 BJS 和 ZHY 实现类似 SCP 的功能。本方案具有如下特点:

  1. 自动化。方案部署完成后,自动对被管理账号中创建的 IAM 实体进行策略关联。
  2. 无服务器化。本方案无需部署 EC2,无需考虑操作系统层的运维工作。
  3. 低成本。无服务器化方案通常机遇调用次数进行计费,本方案仅在用户创建 IAM 实体时产生调用,使用成本接近于 0。

 

方案架构说明

AWS IAM 服务的权限边界功能

对于 AWS Organizations 来说,SCP 限制成员账户中的 IAM 实体(用户和角色)的权限。在 AWS IAM 服务中,权限边界 功能具备相似的功能。

权限边界是一个高级功能,它使用托管策略设置可以为 IAM 实体授予的最大权限。实体的权限边界取决于实体上关联的基于 IAM 身份的策略和权限边界同时允许的操作。

基于 IAM 身份的策略是附加到用户、用户组或角色的内联或托管策略。基于身份的策略向实体授予权限,而权限边界限制这些权限。有效的权限是两种策略类型的交集。其中任一项策略中的显式拒绝将覆盖允许。其关系说明如下图:

图1:权限边界效果示意

如果可以对某个 AWS 账号内所有被创建出来的 IAM 实体自动设置权限边界,也相当于对这个账号设置了 SCP 功能。

方案架构设计

当用户在 AWS 内创建 IAM 实体(IAM User 或 IAM Role)时,会产生一个事件。本方案利用这个事件来触发一个 Lambda 函数,利用这个 Lambda 函数为创建出来的 IAM 实体关联一个权限边界,以此对新创建的 IAM 实体进行权限控制。整个过程如下图所示:

图2: 方案架构

权限边界策略

上述架构将创建/使用以下资源:

序号 资源类型 资源名称 说明
1 EventBridge Bus default 接受 API 调用事件
2 EventBridge Rule scp-rule 过滤出创建 IAM 实体的事件
3 Lambda Function scp-Permission 实现自动关联权限边界的功能
4 IAM Role scp-lambda Lambda 函数使用的角色
5 IAM Policy scpPolicy 被关联的权限边界策略
6 S3 Bucket 自定义 创建 CloudTrail Trail 时需要指定
7 CloudTrail Trail 自定义 捕获 API 调用事件

除 EventBridge Bus 和 IAM Policy 外,所涉及到的其它服务资源在创建时均设置如下标签:

Key Value
Owner SCP-Supervisor

为保证本方案的正常执行,创建出来的上述资源不能被删除或修改。然而在实际应用过程中,一个 IAM 实体很有可能被授予操作上述服务的权限。这就要求自动关联给 IAM 实体的权限边界策略中必须包含以下三类声明:

  1. 禁止针对附加了 Owner: SCP-Supervisro 标签的资源进行任何操作;
  2. 禁止针对 arn:aws-cn:iam::<ACCOUNT_ID>:policy/scpPolicy 策略进行任何修改操作,包括修改、删除、解除关联;
  3. 显性允许其他任何操作。

除上述三类声明外,用户再根据实际管理需要增加其它权限声明,以形成完整的权限边界策略。

 

方案部署说明

    1. 创建 CloudTrail Trail。如之前在 AWS 账号中已经创建 Trail,可跳过此步骤;如账号中不存在 Trail,可参考 官方文档 中的步骤。注意在创建时,为 Trail 添加 Owner:SCP-Supervisor 标签。
    2. 创建 IAM Role。信任实体为 Lambda 服务,并关联 IAMFullAccess 策略,添加 Owner:SCP-Supervisor 标签,可根据需要自定义 Role 的名称,如下图:

图3:为 Lambda 函数创建 IAM Role

  1. 创建作为权限边界的 IAM 策略,策略名 scpPolicy,包含策略内容如下:(以下策略内容仅包含保护本方案所需资源的策略声明,实际应用中还需根据需要添加其它权限声明):
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAllOtherAccess",
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        },
        {
            "Sid": "TaggedResourcesProtect",
            "Effect": "Deny",
            "Action": "*",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/Owner": "SCP-Supervisor"
                }
            }
        },
        {
            "Sid": "OtherResourcesProtect",
            "Effect": "Deny",
            "Action": "*",
            "Resource": [
                "arn:aws-cn:iam::<ACCOUNT_ID>:policy/scpPolicy",
                "arn:aws-cn:cloudtrail:cn-north-1:<ACCOUNT_ID>:trail/scp-trail-<ACCOUNT_ID>",
                "arn:aws-cn:s3:::scp-trail-do-not-delete-<ACCOUNT_ID>"
            ]
        },
        {
            "Sid": "DenyBoundaryRemove",
            "Effect": "Deny",
            "Action": [
                "iam:DeleteUserPermissionsBoundary",
                "iam:DeleteRolePermissionsBoundary"
            ],
            "Resource": [
                "arn:aws-cn:iam::<ACCOUNT_ID>:role/*",
                "arn:aws-cn:iam::<ACCOUNT_ID>:user/*"
            ],
            "Condition": {
                "ArnEquals": {
                    "iam:PermissionsBoundary": "arn:aws-cn:iam::<ACCOUNT_ID>:policy/scpPolicy"
                }
            }
        }
    ]
}
  1. 创建 Lambda 函数,选择6,使用之前创建的 scp-lambda 角色,可根据需要自定义函数名称,如下图:

图4:创建 Lambda 函数

为创建好的函数添加标签:

图5:函数标签

为创建好的函数设置环境变量:

图6:函数环境变量

进入到【代码】框,粘贴如下代码:

import json
import os
import boto3

scpBoundary = os.environ['SCP_BOUNDARY_POLICY']

def lambda_handler(event, context):
    
    iam_client = boto3.client('iam')

    accountID = event['account']   
    boundaryArn =  "arn:aws-cn:iam::" + accountID + ":policy/" + scpBoundary
    
    if event['detail']['eventName'] == "CreateUser":
        User_Name = event['detail']['responseElements']['user']['userName']
        identityArn = event['detail']['responseElements']['user']['arn']
        iam_client.put_user_permissions_boundary(
            UserName=User_Name,
            PermissionsBoundary=boundaryArn
        )

    elif event['detail']['eventName'] == "CreateRole":
        identityArn = event['detail']['responseElements']['role']['arn']
        Role_Name = identityArn.split('/')[-1]
        iam_client.put_role_permissions_boundary(
            RoleName=Role_Name,
            PermissionsBoundary=boundaryArn
        )
    else:
        print("Others")
    rspAction="Permissions boundary policy has been attached"
    output = {
        "Identity ARN": identityArn,
        "Respond Action": rspAction}
    
    return json.dumps(output)

选择【Deploy】,保存代码。

  1. 创建 EventBridge Rule。

可根据需要自定义 Rule 的名称。在【定义模式】环节,选择“自定义模式”,粘贴如下代码并保存:

{
  "source": ["aws.iam"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["iam.amazonaws.com"],
    "eventName": ["CreateUser", "CreateRole"],
    "errorCode": [{
      "exists": false
    }]
  }
}

 

界面显示如下:

图7:定义事件模式

保持使用 default 事件总线即可,选择 Lambda 函数作为目标,并从下来菜单中选择刚刚创建的 lambda. 函数:

图8:设定规则的目标

为规则添加标签:

图9:添加规则的标签

至此,整个方案部署完毕。可自行创建一个 IAM User 或 Role,创建完成后查看这个刚创建的 IAM 实体的权限,能够看到已被自动关联了权限边界策略:scpPolicy。Lambda 执行需要一个很短的时间。若 IAM 实体创建完成后立即查看其权限,可能出现未设置权限边界的情况。刷新一下页面即可看到结果,如下图:

图10:方案效果

 

延伸说明

本文介绍了一个方案原型,以便在 AWS 中国大陆地区的两个区域内实现类似 SCP 的功能效果。但本文所涉及到的所有管理资源均部署在被管账号内,实际应用中还需要考虑以下两个问题:

  1. 需要 SCP 功能的企业往往拥有多个账号,上述方案如何实现对多账号的统一管理?并确保可以针对不同账号设置权限边界?
  2. 当被管理账号越来越多时,如何追溯不同账号使用了那种权限边界策略?

为了解决上述两个关键问题,需要增加以下服务:

  • Amazon API Gateway
  • Amazon S3
  • Amazon EventBridge
  • Amazon DynamoDB
  • Amazon SNS

整个方案的架构升级为如下结构:

图11:多账号管理架构

如希望进一步了解该方案,可从 这里 获得方案源代码及架构详细说明。

 

本篇作者

刘伟平

AWS APN 合作伙伴解决方案架构师,主要负责 AWS (中国)合作伙伴的技术支持工作,同时致力于 AWS 云服务在国内的应用及推广。加入 AWS 前,在 HP(HPE)服务超过7年,历任存储售前工程师、电信行业售前工程师、NFV 解决方案架构师,熟悉传统企业 IT 架构、私有云及混合云部署。