具有条件的 CDK 应用程序的 IAM 权限边界

IAM permission boundary for CDK apps with conditions

我正在为部署我们的 Cloud Development Kit (CDK) 应用程序的 CI/CD 用户编写 IAM 角色。 CDK 应用程序由 lambda 函数、Fargate 等组成。问题是,CDK 不允许我指定它需要的所有角色。相反,它自己创建了一些 then 。

几个例子:

  1. 每个带有日志保留的 lambda 函数都有另一个由 CDK 创建的 lambda,它将日志保留设置到日志组和日志流。
  2. 执行步骤函数的 CloudTrail 事件需要具有 states:StartExecution 权限的角色。

CDK 自动创建这些角色并为它们设置内联策略。这迫使我授予 CI/CD 角色创建角色和附加策略的权限。因此,如果有人可以访问 CI/CD 用户(例如,如果我们的 GitHub 凭据泄露),攻击者可以创建新角色并授予他们管理员权限。

我尝试在单独的堆栈中自己创建所有角色,然后在 CDK 应用程序中使用这些角色。但是正如我上面提到的(见上面的例子),它不可能无处不在...

我也为部署者角色尝试了 IAM 权限边界,但我不知道如何限制 iam:PutRolePolicy 的权限。 CDK 基本上执行以下操作:

  1. iam:CreateRole
  2. iam:PutRolePolicy

根据AWS documentation,条件是非常基本的字符串比较。我需要能够 select,在传递给 iam:PutRolePolicy.

的策略文档中允许哪些操作

这是我的权限边界示例,允许委托人创建角色并设置角色策略。请参阅条件注释。

permission_boundary = aws_iam.ManagedPolicy(
    scope=self,
    id='DeployerPermissionBoundary',
    managed_policy_name='DeployerPermissionBoundary',
    statements=[
        aws_iam.PolicyStatement(
            actions=['iam:CreateRole'],
            effect=aws_iam.Effect.ALLOW,
            resources=[f'arn:aws:iam::{core.Aws.ACCOUNT_ID}:role/my-project-lambda-role']
        ),
        aws_iam.PolicyStatement(
            actions=['iam:PutRolePolicy'],
            effect=aws_iam.Effect.ALLOW,
            resources=[f'arn:aws:iam::{core.Aws.ACCOUNT_ID}:role/my-project-lambda-role'],
            conditions=Conditions([
                StringLike('RoleName', 'Required-role-name'),
                StringLike('PolicyName', 'Required-policy-name'),
                StringEquals('PolicyDocument', '') # I want to allow only specified actions like logs:CreateLogStream and logs:PutLogEvents
            ])
        )
    ]
)

deployer_role = aws_iam.Role(
    scope=self,
    id='DeployerRole',
    assumed_by=aws_iam.AccountRootPrincipal(),
    permissions_boundary=permission_boundary,
    inline_policies={
        'Deployer': aws_iam.PolicyDocument(
            statements=[
                aws_iam.PolicyStatement(
                    actions=['iam:PutRolePolicy'],
                    effect=aws_iam.Effect.ALLOW,
                    resources=[f'arn:aws:iam::{core.Aws.ACCOUNT_ID}:role/my-project-lambda-role']
                ),
                ...
                ...
            ]
        )
    }
)

PutRolePolicy 限制为仅 selected 操作的正确方法是什么?我想允许 logs:CreateLogStreamlogs:PutLogEvents 而不是别的。

我已经为此奋斗了很长一段时间,我不想退回到给予不必要的更多权限。提前谢谢大家!

这是 Python 中针对 CDK 1.4.0 的解决方案,灵感来自 GitHub 上的 @matthewtapper 代码。这允许您为堆栈中的所有角色设置权限边界。

不用说它非常丑陋,因为python CDK 不提供方面的构造对象。我们必须深入挖掘 JSII 来解析对象。希望它能帮助别人。

from jsii._reference_map import _refs
from jsii._utils import Singleton
import jsii

@jsii.implements(core.IAspect)
class PermissionBoundaryAspect:

    def __init__(self, permission_boundary: Union[aws_iam.ManagedPolicy, str]) -> None:
        """
        :param permission_boundary: Either aws_iam.ManagedPolicy object or managed policy's ARN as string
        """
        self.permission_boundary = permission_boundary

    def visit(self, construct_ref: core.IConstruct) -> None:
        """
        construct_ref only contains a string reference to an object. To get the actual object, we need to resolve it using JSII mapping.
        :param construct_ref: ObjRef object with string reference to the actual object.
        :return: None
        """
        kernel = Singleton._instances[jsii._kernel.Kernel]
        resolve = _refs.resolve(kernel, construct_ref)

        def _walk(obj):
            if isinstance(obj, aws_iam.Role):
                cfn_role = obj.node.find_child('Resource')
                policy_arn = self.permission_boundary if isinstance(self.permission_boundary, str) else self.permission_boundary.managed_policy_arn
                cfn_role.add_property_override('PermissionsBoundary', policy_arn)
            else:
                if hasattr(obj, 'permissions_node'):
                    for c in obj.permissions_node.children:
                        _walk(c)
                if obj.node.children:
                    for c in obj.node.children:
                        _walk(c)

        _walk(resolve)

用法:

stack.node.apply_aspect(PermissionBoundaryAspect(managed_policy_arn))

这是 CDK 版本 1.9.0 的解决方案 + 添加和额外的 try_find_child() 以防止节点上的嵌套子错误,而且 stack.node.apply_aspect() 方法已被 AWS 弃用,因此有一个新的用法实现。

from aws_cdk import (
    aws_iam as iam,
    core,
)
import jsii
from jsii._reference_map import _refs
from jsii._utils import Singleton

from typing import Union


@jsii.implements(core.IAspect)
class PermissionBoundaryAspect:
    """
    This aspect finds all aws_iam.Role objects in a node (ie. CDK stack) and sets
    permission boundary to the given ARN.
    """

    def __init__(self, permission_boundary: Union[iam.ManagedPolicy, str]) -> None:
        """
        This initialization method sets the permission boundary attribute.

        :param permission_boundary: The provided permission boundary
        :type permission_boundary: iam.ManagedPolicy|str
        """
        self.permission_boundary = permission_boundary
        print(self.permission_boundary)

    def visit(self, construct_ref: core.IConstruct) -> None:
        """
        construct_ref only contains a string reference to an object.
        To get the actual object, we need to resolve it using JSII mapping.
        :param construct_ref: ObjRef object with string reference to the actual object.
        :return: None
        """
        if isinstance(construct_ref, jsii._kernel.ObjRef) and hasattr(
            construct_ref, "ref"
        ):
            kernel = Singleton._instances[
                jsii._kernel.Kernel
            ]  # The same object is available as: jsii.kernel
            resolve = _refs.resolve(kernel, construct_ref)
        else:
            resolve = construct_ref

        def _walk(obj):
            if obj.node.try_find_child("Resource") is not None:
                if isinstance(obj, iam.Role):
                    cfn_role = obj.node.find_child("Resource")
                    policy_arn = (
                        self.permission_boundary
                        if isinstance(self.permission_boundary, str)
                        else self.permission_boundary.managed_policy_arn
                    )
                    cfn_role.add_property_override("PermissionsBoundary", policy_arn)
            else:
                if hasattr(obj, "permissions_node"):
                    for c in obj.permissions_node.children:
                        _walk(c)
                if hasattr(obj, "node") and obj.node.children:
                    for c in obj.node.children:
                        _walk(c)

        _walk(resolve)

堆栈的新实现 API 是:

core.Aspects.of(stack).add(
    PermissionBoundaryAspect(
        f"arn:aws:iam::{target_environment.account}:policy/my-permissions-boundary"
    )
)

在某些情况下仍在挣扎或想要 Java 示例的任何人:

    @Slf4j
    public class PermissionBoundaryRoleAspect implements IAspect {

    private static final String BOUNDED_PATH = "/bounded/";

    @Override
    public void visit(final @NotNull IConstruct node) {

        node.getNode().findAll().stream().filter(iConstruct -> CfnResource.isCfnResource(iConstruct) && iConstruct.toString().contains("AWS::IAM::Role")).forEach(iConstruct -> {
            var resource = (CfnResource) iConstruct;
            resource.addPropertyOverride("PermissionsBoundary", "arn:aws:iam::xxx:policy/BoundedPermissionsPolicy");
            resource.addPropertyOverride("Path", BOUNDED_PATH);
        });

        if (node instanceof CfnInstanceProfile) {
            var instanceProfile = (CfnInstanceProfile) node;
            instanceProfile.setPath(BOUNDED_PATH);
        }
      }
    }

为什么我这样做是因为我遇到了这样一种情况,即并非所有创建的角色都是 CfnRole 类型

在我的例子中,我必须创建一个 CfnCloudFormationProvisionedProduct

这个构造函数创建角色的方式很奇怪。此构造函数中的角色是 CfnResource 类型,不能转换为“CfnRole”

因此我正在使用 iConstruct.toString().contains("AWS::IAM::Role") 它适用于每个资源,如果它的类型 AWS::IAM::Role 和任何 CfnRole