diff --git a/README.md b/README.md index 9275598..99480cf 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ The deployer verifies account/role, live service configuration, source label, EC ECR may omit its optional severity-count map for a clean image. That response is accepted only after the same completion/freshness checks, with an exact registry/repository/digest match, explicitly empty findings lists, and no remaining page. An absent summary without that evidence, nonempty findings without a summary, or malformed counts fails closed. This requires no additional IAM permissions or changes to registry scanning. See the [ECR response schema](https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_ImageScanFindings.html). +Enhanced scanning also requires `inspector2:ListFindings` and `inspector2:ListCoverage` in the deployment role, its permissions boundary, and the STS session policy. The generated role policy limits these read-only actions to the deployment region. AWS does not support repository-level resource restrictions for these Inspector list actions, so they can read account-wide scan metadata in that region. No Inspector configuration/write actions or parameter/secret access are granted. Existing roles and boundaries must be updated through the private infrastructure repository before adopting this change. See [AWS enhanced-scanning permissions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning-enhanced-iam.html). + Use a unique CircleCI build number when retrying publication to immutable tags. A failed rollout remains a failure even if ECS rolls back. The script never blindly reverts over another deployment. ## Tests diff --git a/tcdeploy.py b/tcdeploy.py index f1da01b..bd9e95f 100644 --- a/tcdeploy.py +++ b/tcdeploy.py @@ -95,6 +95,10 @@ def role_policy(m): 'Resource': arn(m, 'ecs', 'task-definition/' + m['taskFamily'] + ':*')}, {'Effect': 'Allow', 'Action': ['ecs:ListTasks', 'ecs:DescribeTasks'], 'Resource': '*', 'Condition': {'ArnEquals': {'ecs:cluster': arn(m, 'ecs', 'cluster/' + m['cluster'])}}}, + # ECR enhanced scan reads depend on these Inspector APIs. They do not + # support resource-level permissions; limit them to the deployment region. + {'Effect': 'Allow', 'Action': ['inspector2:ListFindings', 'inspector2:ListCoverage'], 'Resource': '*', + 'Condition': {'StringEquals': {'aws:RequestedRegion': m['region']}}}, {'Effect': 'Allow', 'Action': 'iam:PassRole', 'Resource': [r for r in (m['taskRoleArn'], m['executionRoleArn']) if r], 'Condition': {'StringEquals': {'iam:PassedToService': 'ecs-tasks.amazonaws.com'}}} ]} @@ -122,7 +126,8 @@ def trust_policy(m): 'ecr:BatchCheckLayerAvailability', 'ecr:InitiateLayerUpload', 'ecr:UploadLayerPart', 'ecr:CompleteLayerUpload', 'ecr:PutImage', 'ecr:DescribeImageScanFindings', 'ecs:DescribeServices', 'ecs:DescribeTaskDefinition', 'ecs:RegisterTaskDefinition', - 'ecs:TagResource', 'ecs:ListTagsForResource', 'ecs:UpdateService', 'ecs:ListTasks', 'ecs:DescribeTasks', 'iam:PassRole'], 'Resource': '*'}]} + 'ecs:TagResource', 'ecs:ListTagsForResource', 'ecs:UpdateService', 'ecs:ListTasks', 'ecs:DescribeTasks', 'iam:PassRole', + 'inspector2:ListFindings', 'inspector2:ListCoverage'], 'Resource': '*'}]} # Reviewed service adapters may retain migration/health/CloudFormation ownership # gates. These capabilities are not exposed as deployment CLI flags. Their IAM @@ -139,7 +144,7 @@ def trust_policy(m): def session_policy(additional_actions=()): require(set(additional_actions) <= ADAPTER_ACTIONS, 'Unsupported custom deployment capability') result = deepcopy(SESSION_POLICY) - result['Statement'][1]['Action'] += sorted(set(additional_actions)) + result['Statement'][1]['Action'] += sorted(set(additional_actions) - set(result['Statement'][1]['Action'])) require(len(compact(result)) <= 2048, 'Session policy exceeds the STS limit') return result diff --git a/tests/test_security.py b/tests/test_security.py index 44f3db0..c11661c 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -187,6 +187,20 @@ def test_policies_bind_identity_and_forbid_parameters(self): passrole = d.role_policy(m)['Statement'][-1] self.assertEqual(passrole['Resource'], [m['taskRoleArn'], m['executionRoleArn']]) + def test_enhanced_scan_permissions_are_read_only_and_region_limited(self): + m = manifest() + inspector = [s for s in d.role_policy(m)['Statement'] + if any(a.startswith('inspector2:') for a in s['Action'])] + self.assertEqual(inspector, [{'Effect': 'Allow', + 'Action': ['inspector2:ListFindings', 'inspector2:ListCoverage'], 'Resource': '*', + 'Condition': {'StringEquals': {'aws:RequestedRegion': m['region']}}}]) + for policy in (d.session_policy(), d.session_policy(d.ADAPTER_ACTIONS)): + actions = policy['Statement'][1]['Action'] + self.assertEqual({a for a in actions if a.startswith('inspector2:')}, + {'inspector2:ListFindings', 'inspector2:ListCoverage'}) + self.assertEqual(len(actions), len(set(actions))) + self.assertLessEqual(len(d.compact(policy)), 2048) + class CredentialTests(unittest.TestCase): def test_ecs_service_parameter_reaches_the_cli_without_name_collision(self):