diff --git a/CHANGELOG.md b/CHANGELOG.md index 0180fa82..6fe9cb53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - stronger password in `scripts/setup-secrets-example` - use capabilities instead of passwordless sudo in `dcv-image` - use https in `demo-only/rosbag-webviz` +- scope down over-privileged roles in `emrstudio-on-eks` ### **Removed** diff --git a/modules/analysis/rosbag-image-pipeline-sfn/stack.py b/modules/analysis/rosbag-image-pipeline-sfn/stack.py index 84d9c5bc..274207cf 100644 --- a/modules/analysis/rosbag-image-pipeline-sfn/stack.py +++ b/modules/analysis/rosbag-image-pipeline-sfn/stack.py @@ -210,7 +210,10 @@ def __init__( "Object Detection", service="sagemaker", action="createProcessingJob", - iam_resources=["*"], + iam_resources=[ + f"arn:aws:sagemaker:{self.region}:{self.account}:processing-job/step-*-yolo*", + f"arn:aws:sagemaker:{self.region}:{self.account}:processing-job/{dep_mod}*" + ], # integration_pattern=sfn.IntegrationPattern.RUN_JOB, # not supported in CDK (as of 2024-02-08) additional_iam_statements=[ iam.PolicyStatement( @@ -287,7 +290,10 @@ def __init__( "Lane Detection", service="sagemaker", action="createProcessingJob", - iam_resources=["*"], + iam_resources=[ + f"arn:aws:sagemaker:{self.region}:{self.account}:processing-job/step-*-lane*", + f"arn:aws:sagemaker:{self.region}:{self.account}:processing-job/{dep_mod}*" + ], # integration_pattern=sfn.IntegrationPattern.RUN_JOB, # not supported in CDK (as of 2024-02-08) additional_iam_statements=[ iam.PolicyStatement( @@ -462,7 +468,10 @@ def processing_job_add_wait( f"Get {id} Status", service="sagemaker", action="describeProcessingJob", - iam_resources=["*"], + iam_resources=[ + f"arn:aws:sagemaker:{self.region}:{self.account}:processing-job/step-*", + f"arn:aws:sagemaker:{self.region}:{self.account}:processing-job/{dep_mod}*" + ], parameters={ "ProcessingJobName": sfn.JsonPath.array_get_item( sfn.JsonPath.string_split(sfn.JsonPath.string_at("$.ProcessingJobArn"), "/"), diff --git a/modules/analysis/rosbag-image-pipeline/stack.py b/modules/analysis/rosbag-image-pipeline/stack.py index dc788aa7..dccf5970 100755 --- a/modules/analysis/rosbag-image-pipeline/stack.py +++ b/modules/analysis/rosbag-image-pipeline/stack.py @@ -75,15 +75,33 @@ def __init__( # Create Dag IAM Role and policy policy_statements = [ iam.PolicyStatement( - actions=["dynamodb:*"], + actions=[ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchGetItem", + "dynamodb:BatchWriteItem" + ], effect=iam.Effect.ALLOW, resources=[f"arn:{self.partition}:dynamodb:{self.region}:{self.account}:table/{dep_mod}*"], ), iam.PolicyStatement( - actions=["ecr:*"], + actions=[ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], effect=iam.Effect.ALLOW, resources=[f"arn:{self.partition}:ecr:{self.region}:{self.account}:repository/{dep_mod}*"], ), + iam.PolicyStatement( + actions=["ecr:GetAuthorizationToken"], + effect=iam.Effect.ALLOW, + resources=["*"], # GetAuthorizationToken doesn't support resource-level permissions + ), iam.PolicyStatement( actions=[ "batch:UntagResource", @@ -142,13 +160,26 @@ def __init__( iam.ManagedPolicy.from_managed_policy_arn( self, id="fullaccess", managed_policy_arn=self.bucket_access_policy ), - iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSageMakerFullAccess"), ], role_name=dag_role_name, max_session_duration=Duration.hours(12), path="/", ) + # Add minimal SageMaker permissions + self.dag_role.add_to_policy( + iam.PolicyStatement( + actions=[ + "sagemaker:CreateProcessingJob", + "sagemaker:DescribeProcessingJob", + "sagemaker:StopProcessingJob", + "sagemaker:ListProcessingJobs" + ], + effect=iam.Effect.ALLOW, + resources=[f"arn:{self.partition}:sagemaker:{self.region}:{self.account}:processing-job/{dep_mod}*"], + ) + ) + # Sagemaker Security Group self.vpc = ec2.Vpc.from_lookup( self, diff --git a/modules/beta/emrstudio-on-eks/app.py b/modules/beta/emrstudio-on-eks/app.py index 590a1678..a52b1c42 100644 --- a/modules/beta/emrstudio-on-eks/app.py +++ b/modules/beta/emrstudio-on-eks/app.py @@ -53,6 +53,7 @@ def _param(name: str) -> str: eks_oidc_arn=eks_oidc_arn, eks_openid_issuer=eks_openid_issuer, emr_namespace=emr_eks_namespace, + artifact_bucket_name=artifact_bucket_name, ) emr_studio = StudioLiveStack( diff --git a/modules/beta/emrstudio-on-eks/modulestack.yaml b/modules/beta/emrstudio-on-eks/modulestack.yaml index 32c2adc7..81d9452a 100644 --- a/modules/beta/emrstudio-on-eks/modulestack.yaml +++ b/modules/beta/emrstudio-on-eks/modulestack.yaml @@ -28,18 +28,23 @@ Resources: - !Ref EksClusterAdminRoleArn - Effect: Allow Action: - # Permissions to cleanup EMR Virtual Cluster resources - # These permissions doesnt support Resource level + # List/Describe permissions require wildcard - no resource-level support - "emr-containers:List*" - "emr-containers:Describe*" - - "emr-containers:DeleteVirtualCluster" - - "emr-containers:DeleteManagedEndpoint" - "elasticmapreduce:Describe*" - "elasticmapreduce:List*" - - "elasticmapreduce:DeleteStudio" - "sso:DeleteManagedApplicationInstance" Resource: - "*" + - Effect: Allow + Action: + # Delete permissions scoped to specific resources + - "emr-containers:DeleteVirtualCluster" + - "emr-containers:DeleteManagedEndpoint" + - "elasticmapreduce:DeleteStudio" + Resource: + - !Sub "arn:aws:emr-containers:${AWS::Region}:${AWS::AccountId}:virtualcluster/*" + - !Sub "arn:aws:elasticmapreduce:${AWS::Region}:${AWS::AccountId}:studio/*" - Effect: Allow Action: - "ec2:RevokeSecurityGroupEgress" diff --git a/modules/beta/emrstudio-on-eks/rbac_stack.py b/modules/beta/emrstudio-on-eks/rbac_stack.py index d5b9f836..d66eec5f 100755 --- a/modules/beta/emrstudio-on-eks/rbac_stack.py +++ b/modules/beta/emrstudio-on-eks/rbac_stack.py @@ -35,6 +35,7 @@ def __init__( eks_oidc_arn: str, eks_openid_issuer: str, emr_namespace: str, + artifact_bucket_name: str, **kwargs: Any, ) -> None: super().__init__( @@ -215,7 +216,11 @@ def __init__( self.job_role.add_to_policy( iam.PolicyStatement( - resources=["*"], + resources=[ + f"arn:{self.partition}:s3:::{artifact_bucket_name}", + f"arn:{self.partition}:s3:::{artifact_bucket_name}/*", + f"arn:{self.partition}:s3:::aws-logs-{self.account}-{self.region}/elasticmapreduce/*", + ], actions=["s3:PutObject", "s3:GetObject", "s3:ListBucket"], effect=iam.Effect.ALLOW, ) @@ -223,7 +228,10 @@ def __init__( self.job_role.add_to_policy( iam.PolicyStatement( - resources=[f"arn:{self.partition}:logs:*:*:*"], + resources=[ + f"arn:{self.partition}:logs:{self.region}:{self.account}:log-group:/aws/emr-containers/*", + f"arn:{self.partition}:logs:{self.region}:{self.account}:log-group:/aws/emr-serverless/*", + ], actions=[ "logs:PutLogEvents", "logs:CreateLogStream", diff --git a/modules/beta/emrstudio-on-eks/studio_stack.py b/modules/beta/emrstudio-on-eks/studio_stack.py index b47e6c18..00a57b54 100644 --- a/modules/beta/emrstudio-on-eks/studio_stack.py +++ b/modules/beta/emrstudio-on-eks/studio_stack.py @@ -61,34 +61,12 @@ def __init__( name=f"{dep_mod}-EMRCluster", ) - # policy to let Lambda invoke the api - custom_policy_document = iam.PolicyDocument( - statements=[ - iam.PolicyStatement( - effect=iam.Effect.ALLOW, - actions=[ - "ec2:CreateSecurityGroup", - "ec2:RevokeSecurityGroupEgress", - "ec2:CreateSecurityGroup", - "ec2:DeleteSecurityGroup", - "ec2:AuthorizeSecurityGroupEgress", - "ec2:AuthorizeSecurityGroupIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:DeleteSecurityGroup", - ], - resources=["*"], - ) - ] - ) - managed_policy = iam.ManagedPolicy(self, f"{id}-ManagedPolicy", document=custom_policy_document) - self.role = iam.Role( scope=self, id=f"{id}-LambdaRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole"), - managed_policy, ], ) @@ -176,12 +154,32 @@ def __init__( self, "StudioServiceRole", assumed_by=iam.ServicePrincipal("elasticmapreduce.amazonaws.com"), - managed_policies=[iam.ManagedPolicy.from_aws_managed_policy_name("AmazonS3FullAccess")], ) Tags.of(role).add("for-use-with-amazon-emr-managed-policies", "true") role.add_to_policy( iam.PolicyStatement( - resources=["*"], + resources=[ + f"arn:aws:s3:::{artifact_bucket_name}", + f"arn:aws:s3:::{artifact_bucket_name}/*", + ], + actions=[ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket", + "s3:GetBucketLocation", + ], + effect=iam.Effect.ALLOW, + ) + ) + role.add_to_policy( + iam.PolicyStatement( + resources=[ + f"arn:aws:ec2:{self.region}:{self.account}:vpc/{vpc_id}", + f"arn:aws:ec2:{self.region}:{self.account}:subnet/*", + f"arn:aws:ec2:{self.region}:{self.account}:security-group/*", + f"arn:aws:ec2:{self.region}:{self.account}:network-interface/*", + ], actions=[ "ec2:AuthorizeSecurityGroupEgress", "ec2:AuthorizeSecurityGroupIngress", @@ -200,6 +198,17 @@ def __init__( "ec2:DescribeInstances", "ec2:DescribeSubnets", "ec2:DescribeVpcs", + ], + effect=iam.Effect.ALLOW, + ) + ) + role.add_to_policy( + iam.PolicyStatement( + resources=[ + f"arn:aws:elasticmapreduce:{self.region}:{self.account}:cluster/*", + f"arn:aws:emr-containers:{self.region}:{self.account}:/virtualclusters/*", + ], + actions=[ "elasticmapreduce:ListInstances", "elasticmapreduce:DescribeCluster", "elasticmapreduce:ListSteps", diff --git a/modules/demo-only/jupyter-hub/addons-iam-policies/jupyterhub-iam.json b/modules/demo-only/jupyter-hub/addons-iam-policies/jupyterhub-iam.json deleted file mode 100644 index 2f6c647c..00000000 --- a/modules/demo-only/jupyter-hub/addons-iam-policies/jupyterhub-iam.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "secretsmanager:GetSecretValue", - "secretsmanager:DescribeSecret", - "secretsmanager:GetResourcePolicy", - "secretsmanager:ListSecretVersionIds" - ], - "Resource": "*" - }, - { - "Action": [ - "dynamodb:List*", - "dynamodb:Describe*" - ], - "Resource": "*", - "Effect": "Allow" - }, - { - "Action": [ - "dynamodb:BatchGet*", - "dynamodb:Get*", - "dynamodb:Query", - "dynamodb:Scan", - "dynamodb:BatchWrite*", - "dynamodb:Update*", - "dynamodb:PutItem" - ], - "Resource": [ - "arn:aws:dynamodb:::table/" - ], - "Effect": "Allow" - }, - { - "Effect": "Allow", - "Action": "secretsmanager:ListSecrets", - "Resource": "*" - } - ] -} diff --git a/modules/demo-only/jupyter-hub/app.py b/modules/demo-only/jupyter-hub/app.py index d8105118..c6a98f3d 100644 --- a/modules/demo-only/jupyter-hub/app.py +++ b/modules/demo-only/jupyter-hub/app.py @@ -52,6 +52,7 @@ def _param(name: str) -> str: eks_cluster_name=eks_cluster_name, eks_admin_role_arn=eks_admin_role_arn, eks_oidc_arn=eks_oidc_arn, + secrets_manager_name=secrets_manager_name, jh_username=jh_username, jh_password=jh_password, jh_image_name=jh_image_name, # type: ignore diff --git a/modules/demo-only/jupyter-hub/stack.py b/modules/demo-only/jupyter-hub/stack.py index 6d8a4c46..e240b527 100755 --- a/modules/demo-only/jupyter-hub/stack.py +++ b/modules/demo-only/jupyter-hub/stack.py @@ -1,7 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import json import logging import os from typing import Any, cast @@ -30,6 +29,7 @@ def __init__( eks_cluster_name: str, eks_admin_role_arn: str, eks_oidc_arn: str, + secrets_manager_name: str, jh_username: str, jh_password: str, jh_image_name: str, @@ -72,15 +72,22 @@ def __init__( "jupyterhub", name="jupyterhub", namespace="jupyter-hub" ) - jupyterhub_policy_statement_json_path = os.path.join(project_dir, "addons-iam-policies", "jupyterhub-iam.json") - with open(jupyterhub_policy_statement_json_path) as json_file: - jupyterhub_policy_statement_json = json.load(json_file) + secret_arn = f"arn:{self.partition}:secretsmanager:{self.region}:{self.account}:secret:{secrets_manager_name}*" + jupyterhub_policy_document = iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=["secretsmanager:GetSecretValue"], + resources=[secret_arn], + ) + ] + ) # Attach the necessary permissions jupyterhub_policy = iam.Policy( self, "jupyterhubpolicy", - document=iam.PolicyDocument.from_json(jupyterhub_policy_statement_json), + document=jupyterhub_policy_document, ) jupyterhub_service_account.role.attach_inline_policy(jupyterhub_policy)