diff --git a/oidc-deploy-roles.yaml b/oidc-deploy-roles.yaml index 27d01b5..7ab639f 100644 --- a/oidc-deploy-roles.yaml +++ b/oidc-deploy-roles.yaml @@ -242,7 +242,36 @@ Resources: - !Sub "arn:aws:kms:us-east-1:${AWS::AccountId}:key/*" # --------------------------------------------------------------------------- - # Shared CloudFormation execution role (SAM stacks) + # Shared CloudFormation execution role (SAM stacks) — INFRA-97 scoped + # + # Replaces the previous blanket managed-policy set (IAMFullAccess + + # *FullAccess) with per-service inline statements that cover exactly + # what the five SAM stacks need during a CloudFormation deploy/update. + # + # PRIMARY ESCALATION CONTROL + # iam:CreateRole and iam:AttachRolePolicy / iam:PutRolePolicy are + # conditioned on iam:PermissionsBoundary StringEquals the boundary ARN + # (seahaven-lambda-execution-boundary, created in INFRA-103). That + # condition is what prevents the CFN execution role from minting an + # unconstrained admin role. + # + # SAM RolePath deviation note + # The original cross-review suggestion mentioned scoping IAM role + # creation to a specific path (/cfn-managed/). AWS::Serverless::Function + # does NOT support a custom RolePath on auto-generated execution roles — + # the PermissionsBoundary property is supported, but the role always lands + # at path /. Relying on a path condition (iam:ResourceTag or path-prefix) + # would therefore exclude the SAM auto-roles and break every deploy. + # The iam:PermissionsBoundary condition achieves the same security goal + # without requiring a path. For any explicit AWS::IAM::Role resources + # in SAM templates (e.g. AdminAuthorizerInvokeRole in meal-order-manager) + # where we can control the path, path scoping can be added in a follow-up. + # + # DEPLOY ORDER DEPENDENCY + # This role references the boundary ARN by literal value. The boundary + # managed policy (seahaven-lambda-execution-boundary, INFRA-103) MUST + # exist before this stack is deployed. See PR description for the + # mandatory three-step deploy sequence. # --------------------------------------------------------------------------- SamCfnExecutionRole: Type: AWS::IAM::Role @@ -255,41 +284,418 @@ Resources: Principal: Service: cloudformation.amazonaws.com Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/AWSLambda_FullAccess - - arn:aws:iam::aws:policy/AmazonAPIGatewayAdministrator - - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess - - arn:aws:iam::aws:policy/AmazonS3FullAccess - - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - - arn:aws:iam::aws:policy/AmazonEventBridgeFullAccess - - arn:aws:iam::aws:policy/AmazonSESFullAccess - - arn:aws:iam::aws:policy/IAMFullAccess Policies: - - PolicyName: additional-service-permissions + + # ── CloudFormation transforms (SAM macro) ───────────────────────── + - PolicyName: cloudformation-transforms PolicyDocument: Version: "2012-10-17" Statement: - - Effect: Allow + - Sid: AllowSAMTransform + Effect: Allow Action: - cloudformation:CreateChangeSet Resource: - arn:aws:cloudformation:us-east-1:aws:transform/* - - Effect: Allow + + # ── Lambda management ───────────────────────────────────────────── + # Covers function create/update/delete, aliases, event source + # mappings, and Lambda layers — all needed for SAM deploys. + - PolicyName: lambda-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: LambdaFunctions + Effect: Allow Action: - - sqs:* - - sns:* - - ec2:* - # cloudfront:* and ssm:* reconciled from out-of-band drift - # (audit H-16) — needed by SAM deploys that manage CloudFront - # distributions (meal-order-manager) and SSM parameters - # (afterhours / payments / meal-order). Codified 2026-05-29. - - cloudfront:* - - ssm:* + - lambda:AddPermission + - lambda:CreateFunction + - lambda:DeleteFunction + - lambda:GetFunction + - lambda:GetFunctionConfiguration + - lambda:ListFunctions + - lambda:RemovePermission + - lambda:UpdateFunctionCode + - lambda:UpdateFunctionConfiguration + - lambda:UpdateFunctionEventInvokeConfig + - lambda:PutFunctionEventInvokeConfig + - lambda:DeleteFunctionEventInvokeConfig + - lambda:GetFunctionEventInvokeConfig + - lambda:ListTags + - lambda:TagResource + - lambda:UntagResource + - lambda:GetPolicy + - lambda:ListVersionsByFunction + - lambda:PublishVersion + - lambda:CreateAlias + - lambda:DeleteAlias + - lambda:UpdateAlias + - lambda:GetAlias + Resource: + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:*" + - Sid: LambdaLayers + Effect: Allow + Action: + - lambda:PublishLayerVersion + - lambda:DeleteLayerVersion + - lambda:GetLayerVersion + - lambda:ListLayerVersions + - lambda:ListLayers + - lambda:AddLayerVersionPermission + - lambda:RemoveLayerVersionPermission + Resource: + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:layer:*" + - Sid: LambdaEventSourceMappings + Effect: Allow + Action: + - lambda:CreateEventSourceMapping + - lambda:DeleteEventSourceMapping + - lambda:GetEventSourceMapping + - lambda:ListEventSourceMappings + - lambda:UpdateEventSourceMapping Resource: "*" - # WAF (audit M-17) — needed for SAM/CFN-managed WebACL associations - # on CloudFront distributions (meal-order-manager orders). Read + - # (dis)associate only, not wafv2:*. Added 2026-06-02. - - Effect: Allow + + # ── API Gateway (HTTP APIs + REST APIs) ─────────────────────────── + - PolicyName: apigateway-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: ApiGateway + Effect: Allow + Action: + - apigateway:GET + - apigateway:POST + - apigateway:PUT + - apigateway:PATCH + - apigateway:DELETE + Resource: + - "arn:aws:apigateway:us-east-1::*" + + # ── DynamoDB ────────────────────────────────────────────────────── + - PolicyName: dynamodb-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: DynamoDBTables + Effect: Allow + Action: + - dynamodb:CreateTable + - dynamodb:DeleteTable + - dynamodb:DescribeTable + - dynamodb:UpdateTable + - dynamodb:ListTables + - dynamodb:TagResource + - dynamodb:UntagResource + - dynamodb:DescribeTimeToLive + - dynamodb:UpdateTimeToLive + - dynamodb:DescribeContinuousBackups + - dynamodb:UpdateContinuousBackups + Resource: + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/*" + + # ── S3 ──────────────────────────────────────────────────────────── + # Covers bucket create/configure + object operations for SAM + # artifact buckets and application buckets. + - PolicyName: s3-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: S3BucketOps + Effect: Allow + Action: + - s3:CreateBucket + - s3:DeleteBucket + - s3:GetBucketLocation + - s3:GetBucketPolicy + - s3:PutBucketPolicy + - s3:DeleteBucketPolicy + - s3:GetBucketTagging + - s3:PutBucketTagging + - s3:GetBucketVersioning + - s3:PutBucketVersioning + - s3:GetLifecycleConfiguration + - s3:PutLifecycleConfiguration + - s3:GetBucketPublicAccessBlock + - s3:PutBucketPublicAccessBlock + - s3:GetBucketNotification + - s3:PutBucketNotification + - s3:GetBucketWebsite + - s3:PutBucketWebsite + - s3:DeleteBucketWebsite + - s3:GetBucketAcl + - s3:PutBucketAcl + Resource: + - "arn:aws:s3:::*" + - Sid: S3ObjectOps + Effect: Allow + Action: + - s3:GetObject + - s3:PutObject + - s3:DeleteObject + - s3:ListBucket + - s3:ListBucketVersions + - s3:GetObjectVersion + Resource: + - "arn:aws:s3:::*" + - "arn:aws:s3:::*/*" + + # ── CloudWatch Logs ─────────────────────────────────────────────── + - PolicyName: cloudwatch-logs-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CWLogs + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:DeleteLogGroup + - logs:DescribeLogGroups + - logs:PutRetentionPolicy + - logs:DeleteRetentionPolicy + - logs:ListTagsLogGroup + - logs:TagLogGroup + - logs:UntagLogGroup + - logs:ListTagsForResource + - logs:TagResource + - logs:UntagResource + - logs:CreateLogDelivery + - logs:GetLogDelivery + - logs:UpdateLogDelivery + - logs:DeleteLogDelivery + - logs:ListLogDeliveries + - logs:PutResourcePolicy + - logs:DescribeResourcePolicies + - logs:PutDestination + - logs:DeleteDestination + - logs:DescribeDestinations + - logs:AssociateKmsKey + - logs:DisassociateKmsKey + Resource: "*" + + # ── EventBridge / CloudWatch Events (scheduled Lambdas) ─────────── + - PolicyName: eventbridge-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: EventBridge + Effect: Allow + Action: + - events:DeleteRule + - events:DescribeRule + - events:EnableRule + - events:DisableRule + - events:ListRules + - events:ListTargetsByRule + - events:PutRule + - events:PutTargets + - events:RemoveTargets + - events:TagResource + - events:UntagResource + - events:ListTagsForResource + - events:PutPermission + - events:RemovePermission + Resource: "*" + + # ── SES (afterhours weekly-post, meal-order email-report) ───────── + - PolicyName: ses-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: SESRules + Effect: Allow + Action: + - ses:CreateReceiptRule + - ses:DeleteReceiptRule + - ses:DescribeReceiptRule + - ses:UpdateReceiptRule + - ses:CreateReceiptRuleSet + - ses:DescribeActiveReceiptRuleSet + - ses:DescribeReceiptRuleSet + - ses:SetActiveReceiptRuleSet + - ses:ReorderReceiptRuleSet + - ses:GetIdentityVerificationAttributes + - ses:ListIdentities + Resource: "*" + + # ── SQS (payments-dashboard queues + DLQs) ──────────────────────── + - PolicyName: sqs-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: SQSQueues + Effect: Allow + Action: + - sqs:CreateQueue + - sqs:DeleteQueue + - sqs:GetQueueAttributes + - sqs:SetQueueAttributes + - sqs:GetQueueUrl + - sqs:ListQueues + - sqs:TagQueue + - sqs:UntagQueue + - sqs:ListQueueTags + - sqs:AddPermission + - sqs:RemovePermission + Resource: + - !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:*" + + # ── SNS (validation / alarm notifications) ──────────────────────── + - PolicyName: sns-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: SNS + Effect: Allow + Action: + - sns:CreateTopic + - sns:DeleteTopic + - sns:GetTopicAttributes + - sns:SetTopicAttributes + - sns:Subscribe + - sns:Unsubscribe + - sns:ListSubscriptionsByTopic + - sns:ListTopics + - sns:TagResource + - sns:UntagResource + Resource: + - !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:*" + + # ── CloudWatch Alarms ───────────────────────────────────────────── + - PolicyName: cloudwatch-alarms-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CWAlarms + Effect: Allow + Action: + - cloudwatch:PutMetricAlarm + - cloudwatch:DeleteAlarms + - cloudwatch:DescribeAlarms + - cloudwatch:EnableAlarmActions + - cloudwatch:DisableAlarmActions + - cloudwatch:ListTagsForResource + - cloudwatch:TagResource + - cloudwatch:UntagResource + Resource: "*" + + # ── EC2 / VPC / NAT / EIP / Security Groups ─────────────────────── + # payments-dashboard deploys a VPC, NAT gateway, EIP, route tables, + # subnets, security groups, and gateway VPC endpoints. + - PolicyName: ec2-vpc-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: EC2VPC + Effect: Allow + Action: + - ec2:AllocateAddress + - ec2:AssociateRouteTable + - ec2:AttachInternetGateway + - ec2:AuthorizeSecurityGroupEgress + - ec2:AuthorizeSecurityGroupIngress + - ec2:CreateInternetGateway + - ec2:CreateNatGateway + - ec2:CreateRoute + - ec2:CreateRouteTable + - ec2:CreateSecurityGroup + - ec2:CreateSubnet + - ec2:CreateVpc + - ec2:CreateVpcEndpoint + - ec2:CreateTags + - ec2:DeleteInternetGateway + - ec2:DeleteNatGateway + - ec2:DeleteRoute + - ec2:DeleteRouteTable + - ec2:DeleteSecurityGroup + - ec2:DeleteSubnet + - ec2:DeleteVpc + - ec2:DeleteVpcEndpoints + - ec2:DescribeAddresses + - ec2:DescribeAvailabilityZones + - ec2:DescribeInternetGateways + - ec2:DescribeNatGateways + - ec2:DescribeRouteTables + - ec2:DescribeSecurityGroups + - ec2:DescribeSubnets + - ec2:DescribeVpcEndpoints + - ec2:DescribeVpcs + - ec2:DescribePrefixLists + - ec2:DetachInternetGateway + - ec2:DisassociateAddress + - ec2:DisassociateRouteTable + - ec2:ModifySubnetAttribute + - ec2:ModifyVpcAttribute + - ec2:ModifyVpcEndpoint + - ec2:ReleaseAddress + - ec2:RevokeSecurityGroupEgress + - ec2:RevokeSecurityGroupIngress + - ec2:UpdateSecurityGroupRuleDescriptionsEgress + - ec2:UpdateSecurityGroupRuleDescriptionsIngress + Resource: "*" + + # ── CloudFront + OAC (meal-order-manager form distribution) ─────── + - PolicyName: cloudfront-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: CloudFront + Effect: Allow + Action: + - cloudfront:CreateDistribution + - cloudfront:DeleteDistribution + - cloudfront:GetDistribution + - cloudfront:GetDistributionConfig + - cloudfront:UpdateDistribution + - cloudfront:TagResource + - cloudfront:UntagResource + - cloudfront:ListTagsForResource + - cloudfront:CreateOriginAccessControl + - cloudfront:DeleteOriginAccessControl + - cloudfront:GetOriginAccessControl + - cloudfront:GetOriginAccessControlConfig + - cloudfront:UpdateOriginAccessControl + - cloudfront:ListOriginAccessControls + - cloudfront:CreateInvalidation + - cloudfront:GetInvalidation + Resource: "*" + + # ── SSM Parameter Store (meal-order-manager, afterhours) ────────── + # Write is needed because meal-order-manager creates + # /meal-order-manager/slack-channel-id via AWS::SSM::Parameter. + - PolicyName: ssm-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: SSMParameters + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + - ssm:GetParametersByPath + - ssm:PutParameter + - ssm:DeleteParameter + - ssm:DeleteParameters + - ssm:DescribeParameters + - ssm:AddTagsToResource + - ssm:RemoveTagsFromResource + - ssm:ListTagsForResource + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/*" + # WAF association needs SSM parameter read at deploy time + # (/seahaven/waf/app-web-acl-arn value lookup) + - Sid: SSMParameterDescribe + Effect: Allow + Action: + - ssm:DescribeParameters + Resource: "*" + + # ── WAF (meal-order-manager CloudFront WebACL association) ──────── + - PolicyName: waf-management + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: WAF + Effect: Allow Action: - wafv2:GetWebACL - wafv2:GetWebACLForResource @@ -299,6 +705,110 @@ Resources: - wafv2:ListResourcesForWebACL Resource: "*" + # ── IAM role lifecycle — BOUNDARY-GATED ────────────────────────── + # This is the PRIMARY escalation control for INFRA-97. + # + # iam:CreateRole / iam:AttachRolePolicy / iam:PutRolePolicy are + # conditioned on iam:PermissionsBoundary StringEquals the + # seahaven-lambda-execution-boundary ARN. That condition means + # any role this execution role creates must have the boundary + # applied, so it can never exceed what the boundary allows + # (which is scoped to the services the five stacks actually use). + # + # iam:PassRole is also included here so CloudFormation can pass + # the auto-generated Lambda execution role to the Lambda service. + # + # Why not path-scoped (e.g. iam:ResourceTag / path /cfn-managed/)? + # SAM's AWS::Serverless::Function auto-generates execution roles at + # path / — there is no supported way to set a custom RolePath on + # SAM auto-roles. A path condition would therefore exclude the + # SAM auto-roles and break every deploy. The PermissionsBoundary + # condition achieves the same security goal without a path requirement. + - PolicyName: iam-role-management-boundary-gated + PolicyDocument: + Version: "2012-10-17" + Statement: + # Create role — MUST attach boundary + - Sid: IAMCreateRoleWithBoundary + Effect: Allow + Action: + - iam:CreateRole + Resource: + - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" + Condition: + StringEquals: + "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + + # Attach managed policies — MUST have boundary already on role + - Sid: IAMAttachPolicyWithBoundary + Effect: Allow + Action: + - iam:AttachRolePolicy + Resource: + - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" + Condition: + StringEquals: + "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + + # Put inline policy — MUST have boundary already on role + - Sid: IAMPutRolePolicyWithBoundary + Effect: Allow + Action: + - iam:PutRolePolicy + Resource: + - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" + Condition: + StringEquals: + "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + + # Boundary management — can only put/delete the boundary itself + # (so SAM can set PermissionsBoundary on the roles it creates) + - Sid: IAMPutPermissionsBoundary + Effect: Allow + Action: + - iam:PutRolePermissionsBoundary + - iam:DeleteRolePermissionsBoundary + Resource: + - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" + Condition: + StringEquals: + "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + + # Read / tag / delete role and policy — no boundary condition needed + - Sid: IAMRoleReadAndDelete + Effect: Allow + Action: + - iam:DeleteRole + - iam:DeleteRolePolicy + - iam:DetachRolePolicy + - iam:GetRole + - iam:GetRolePolicy + - iam:ListAttachedRolePolicies + - iam:ListRolePolicies + - iam:ListRoles + - iam:TagRole + - iam:UntagRole + - iam:UpdateRole + - iam:UpdateRoleDescription + - iam:UpdateAssumeRolePolicy + - iam:GetPolicy + - iam:GetPolicyVersion + - iam:ListPolicies + - iam:ListPolicyVersions + Resource: "*" + + # PassRole — CloudFormation passes the Lambda execution role + # to the Lambda service. Scoped to SAM-generated role pattern. + - Sid: IAMPassRole + Effect: Allow + Action: + - iam:PassRole + Resource: + - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" + Condition: + StringEquals: + "iam:PassedToService": "lambda.amazonaws.com" + # --------------------------------------------------------------------------- # SAM deploy roles (4 repos) # ---------------------------------------------------------------------------