From a7729b7db7e488bd2b474dd96b1e87025cd95b64 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Mon, 18 May 2026 16:57:15 +0700 Subject: [PATCH] feat(security): scoped IAM inline policy + rollback script for github-deploy-miti99bot (F1) Replaces the 10x *FullAccess managed policies with a single stack-scoped inline policy (miti99bot-deploy) on the role. Policy boundaries: - All resource ARNs scoped to miti99bot* (covers future miti99bot-dev) - iam:PassRole conditioned on iam:PassedToService = lambda + scheduler - iam:UpdateAssumeRolePolicy excluded (no trust-rewrite escalation) - iam:AttachRolePolicy excluded (SAM uses inline PutRolePolicy) - Wildcards limited to actions with no resource-level support (sts:GetCallerIdentity, s3:ListAllMyBuckets, cloudformation:ListStacks, cloudformation:ValidateTemplate) Rollback: aws/iam-rollback-fullaccess.sh re-attaches all 10 FullAccess policies with retry-on-throttle + final verification. Apply via Phase 4 two-stage cutover (dual-attach trial then detach). Policy is committed but NOT yet attached -- Phase 4 applies it. Plan: plans/260518-1019-iam-least-privilege/phase-03-draft-custom-policy.md Audit: plans/reports/code-reviewer-260518-1019-security-aws-infra.md --- aws/iam-github-deploy-policy.json | 236 ++++++++++++++++++++++++++++++ aws/iam-rollback-fullaccess.sh | 51 +++++++ 2 files changed, 287 insertions(+) create mode 100644 aws/iam-github-deploy-policy.json create mode 100755 aws/iam-rollback-fullaccess.sh diff --git a/aws/iam-github-deploy-policy.json b/aws/iam-github-deploy-policy.json new file mode 100644 index 0000000..8e7fa1e --- /dev/null +++ b/aws/iam-github-deploy-policy.json @@ -0,0 +1,236 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Budgets", + "Effect": "Allow", + "Action": [ + "budgets:CreateBudget", + "budgets:CreateNotification", + "budgets:CreateSubscriber", + "budgets:DeleteBudget", + "budgets:DeleteNotification", + "budgets:DeleteSubscriber", + "budgets:DescribeBudget", + "budgets:DescribeNotificationsForBudget", + "budgets:ModifyBudget" + ], + "Resource": "arn:aws:budgets::225603493174:budget/miti99bot*" + }, + { + "Sid": "CloudFormation", + "Effect": "Allow", + "Action": [ + "cloudformation:CancelUpdateStack", + "cloudformation:ContinueUpdateRollback", + "cloudformation:CreateChangeSet", + "cloudformation:CreateStack", + "cloudformation:DeleteChangeSet", + "cloudformation:DeleteStack", + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStackEvents", + "cloudformation:DescribeStackResources", + "cloudformation:DescribeStacks", + "cloudformation:ExecuteChangeSet", + "cloudformation:GetTemplate", + "cloudformation:GetTemplateSummary", + "cloudformation:ListChangeSets", + "cloudformation:ListStackResources", + "cloudformation:RollbackStack", + "cloudformation:TagResource", + "cloudformation:UntagResource", + "cloudformation:UpdateStack" + ], + "Resource": [ + "arn:aws:cloudformation:ap-southeast-1:225603493174:stack/miti99bot*/*", + "arn:aws:cloudformation:ap-southeast-1:225603493174:changeSet/*/miti99bot*/*" + ] + }, + { + "Sid": "CloudFormationGlobalRead", + "Effect": "Allow", + "Action": [ + "cloudformation:ListStacks", + "cloudformation:ValidateTemplate" + ], + "Resource": "*" + }, + { + "Sid": "DynamoDB", + "Effect": "Allow", + "Action": [ + "dynamodb:CreateTable", + "dynamodb:DeleteTable", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeTable", + "dynamodb:ListTables", + "dynamodb:ListTagsOfResource", + "dynamodb:TagResource", + "dynamodb:UntagResource", + "dynamodb:UpdateTable" + ], + "Resource": "arn:aws:dynamodb:ap-southeast-1:225603493174:table/miti99bot*" + }, + { + "Sid": "EventBridgeScheduler", + "Effect": "Allow", + "Action": [ + "scheduler:CreateSchedule", + "scheduler:DeleteSchedule", + "scheduler:GetSchedule", + "scheduler:ListSchedules", + "scheduler:ListTagsForResource", + "scheduler:TagResource", + "scheduler:UntagResource", + "scheduler:UpdateSchedule" + ], + "Resource": "arn:aws:scheduler:ap-southeast-1:225603493174:schedule/*/miti99bot*" + }, + { + "Sid": "IAMRolesScoped", + "Effect": "Allow", + "Action": [ + "iam:CreateRole", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:ListRolePolicies", + "iam:ListRoleTags", + "iam:ListRoles", + "iam:PutRolePolicy", + "iam:TagRole", + "iam:UntagRole" + ], + "Resource": "arn:aws:iam::225603493174:role/miti99bot*" + }, + { + "Sid": "IAMPassRole", + "Effect": "Allow", + "Action": "iam:PassRole", + "Resource": "arn:aws:iam::225603493174:role/miti99bot*", + "Condition": { + "ForAllValues:StringEquals": { + "iam:PassedToService": [ + "lambda.amazonaws.com", + "scheduler.amazonaws.com" + ] + } + } + }, + { + "Sid": "Lambda", + "Effect": "Allow", + "Action": [ + "lambda:AddPermission", + "lambda:CreateFunction", + "lambda:CreateFunctionUrlConfig", + "lambda:DeleteFunction", + "lambda:DeleteFunctionUrlConfig", + "lambda:GetFunction", + "lambda:GetFunctionCodeSigningConfig", + "lambda:GetFunctionConfiguration", + "lambda:GetFunctionUrlConfig", + "lambda:GetPolicy", + "lambda:ListTags", + "lambda:ListVersionsByFunction", + "lambda:PublishVersion", + "lambda:RemovePermission", + "lambda:TagResource", + "lambda:UntagResource", + "lambda:UpdateFunctionCode", + "lambda:UpdateFunctionConfiguration", + "lambda:UpdateFunctionUrlConfig" + ], + "Resource": "arn:aws:lambda:ap-southeast-1:225603493174:function:miti99bot*" + }, + { + "Sid": "LambdaLayerRead", + "Effect": "Allow", + "Action": "lambda:GetLayerVersion", + "Resource": "arn:aws:lambda:ap-southeast-1:753240598075:layer:LambdaAdapterLayerArm64:*" + }, + { + "Sid": "Logs", + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:DeleteMetricFilter", + "logs:DeleteRetentionPolicy", + "logs:DescribeLogGroups", + "logs:DescribeMetricFilters", + "logs:ListTagsForResource", + "logs:PutMetricFilter", + "logs:PutRetentionPolicy", + "logs:TagResource", + "logs:UntagResource" + ], + "Resource": [ + "arn:aws:logs:ap-southeast-1:225603493174:log-group:/aws/lambda/miti99bot*", + "arn:aws:logs:ap-southeast-1:225603493174:log-group:/aws/lambda/miti99bot*:*" + ] + }, + { + "Sid": "S3SamArtifacts", + "Effect": "Allow", + "Action": [ + "s3:CreateBucket", + "s3:DeleteObject", + "s3:GetBucketLocation", + "s3:GetBucketPolicy", + "s3:GetBucketVersioning", + "s3:GetEncryptionConfiguration", + "s3:GetObject", + "s3:ListBucket", + "s3:PutBucketPolicy", + "s3:PutBucketVersioning", + "s3:PutEncryptionConfiguration", + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::aws-sam-cli-managed-default-*", + "arn:aws:s3:::aws-sam-cli-managed-default-*/*" + ] + }, + { + "Sid": "S3GlobalList", + "Effect": "Allow", + "Action": "s3:ListAllMyBuckets", + "Resource": "*" + }, + { + "Sid": "SQS", + "Effect": "Allow", + "Action": [ + "sqs:CreateQueue", + "sqs:DeleteQueue", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + "sqs:ListQueueTags", + "sqs:ListQueues", + "sqs:SetQueueAttributes", + "sqs:TagQueue", + "sqs:UntagQueue" + ], + "Resource": "arn:aws:sqs:ap-southeast-1:225603493174:miti99bot*" + }, + { + "Sid": "SSMRead", + "Effect": "Allow", + "Action": [ + "ssm:GetParameter", + "ssm:GetParameters" + ], + "Resource": "arn:aws:ssm:ap-southeast-1:225603493174:parameter/miti99bot/*/*" + }, + { + "Sid": "STS", + "Effect": "Allow", + "Action": "sts:GetCallerIdentity", + "Resource": "*" + } + ] +} diff --git a/aws/iam-rollback-fullaccess.sh b/aws/iam-rollback-fullaccess.sh new file mode 100755 index 0000000..263c98c --- /dev/null +++ b/aws/iam-rollback-fullaccess.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Re-attaches the 10 FullAccess managed policies to github-deploy-miti99bot. +# Idempotent: attach-role-policy succeeds even if policy already attached. +# Use as emergency rollback during Phase 4 cutover (plans/260518-1019-iam-least-privilege). +# +# Usage: +# bash aws/iam-rollback-fullaccess.sh +# AWS_PROFILE=admin bash aws/iam-rollback-fullaccess.sh +ROLE=github-deploy-miti99bot +POLICIES=" + arn:aws:iam::aws:policy/AWSCloudFormationFullAccess + arn:aws:iam::aws:policy/AWSLambda_FullAccess + arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess + arn:aws:iam::aws:policy/AmazonEventBridgeFullAccess + arn:aws:iam::aws:policy/AmazonSQSFullAccess + arn:aws:iam::aws:policy/AmazonSSMFullAccess + arn:aws:iam::aws:policy/CloudWatchLogsFullAccess + arn:aws:iam::aws:policy/AWSBudgetsActionsWithAWSResourceControlAccess + arn:aws:iam::aws:policy/IAMFullAccess + arn:aws:iam::aws:policy/AmazonS3FullAccess +" + +PROFILE_FLAG="" +if [ -n "$AWS_PROFILE" ]; then + PROFILE_FLAG="--profile $AWS_PROFILE" +fi + +for arn in $POLICIES; do + for try in 1 2 3 4 5; do + if aws iam attach-role-policy --role-name "$ROLE" --policy-arn "$arn" $PROFILE_FLAG 2>&1; then + break + fi + echo "retry $try for $arn after throttle..." + sleep $((try * 2)) + done +done + +# Verify final state -- exit non-zero if anything is missing. +ATTACHED=$(aws iam list-attached-role-policies --role-name "$ROLE" $PROFILE_FLAG \ + --query 'AttachedPolicies[].PolicyArn' --output text) +MISSING=0 +for arn in $POLICIES; do + echo "$ATTACHED" | grep -q "$arn" || { echo "MISSING: $arn"; MISSING=1; } +done + +if [ "$MISSING" = 0 ]; then + echo "Rollback complete -- all 10 FullAccess policies attached." +else + echo "Rollback INCOMPLETE -- see MISSING lines above. Re-run or attach via console." + exit 1 +fi