mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 04:33:15 +00:00
feat(scp): deny iam changes on the platform path (PLAT-233) (#159)
* feat(scp): deny iam changes on role/platform unless the platform principal (PLAT-233) Adds ProtectPlatformPath beside the existing name denies in the prod/nonprod SCP and the security OU copy. New hcptf-bootstrap creates use /platform/. Existing roles are not recreated. Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * fix(scp): use live bootstrap ARNs and close the platform path gaps (PLAT-233) Resolve simulate and printed role ARNs from iam:GetRole so a /platform/ create is not reported as an unpathed role. Deny boundary changes on role/platform/*, and match both Identity Center SSO role ARN shapes. Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> * fix(scp): deny platform-path changes in bootstrap simulate (PLAT-233) Add a simulate case for role/platform/hcptf-example and fail when CreateRole, PutRolePolicy, or DeleteRole is allowed. The unpathed hcptf-* import check stays. Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
This commit is contained in:
parent
8ff8ba1a87
commit
8cbc98d927
3 changed files with 123 additions and 8 deletions
|
|
@ -260,7 +260,7 @@ export class OrgGovernanceStack extends cdk.Stack {
|
|||
name: "protect-privileged-roles",
|
||||
type: "SERVICE_CONTROL_POLICY",
|
||||
description:
|
||||
"prod/nonprod: protect break-glass, CDK exec, githubdeploy, and hcptf-bootstrap roles",
|
||||
"prod/nonprod: protect break-glass, CDK exec, githubdeploy, hcptf-bootstrap, and /platform/ roles",
|
||||
targetIds: [prodOu.attrId, nonprodOu.attrId],
|
||||
content: scpContent("protect-privileged-roles"),
|
||||
});
|
||||
|
|
@ -338,6 +338,33 @@ export class OrgGovernanceStack extends cdk.Stack {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Sid: "ProtectPlatformPath",
|
||||
Effect: "Deny",
|
||||
Action: [
|
||||
"iam:CreateRole",
|
||||
"iam:UpdateAssumeRolePolicy",
|
||||
"iam:AttachRolePolicy",
|
||||
"iam:DetachRolePolicy",
|
||||
"iam:PutRolePolicy",
|
||||
"iam:DeleteRolePolicy",
|
||||
"iam:DeleteRole",
|
||||
"iam:UpdateRole",
|
||||
"iam:PutRolePermissionsBoundary",
|
||||
"iam:DeleteRolePermissionsBoundary",
|
||||
"iam:TagRole",
|
||||
"iam:UntagRole",
|
||||
],
|
||||
Resource: "arn:aws:iam::*:role/platform/*",
|
||||
Condition: {
|
||||
ArnNotLike: {
|
||||
"aws:PrincipalArn": [
|
||||
"arn:aws:iam::*:role/OrganizationAccountAccessRole",
|
||||
"arn:aws:iam::*:role/aws-reserved/sso.amazonaws.com/*AWSReservedSSO_Platform_*",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Sid: "ProtectDelegatedAdminMembership",
|
||||
Effect: "Deny",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,33 @@
|
|||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sid": "ProtectPlatformPath",
|
||||
"Effect": "Deny",
|
||||
"Action": [
|
||||
"iam:CreateRole",
|
||||
"iam:UpdateAssumeRolePolicy",
|
||||
"iam:AttachRolePolicy",
|
||||
"iam:DetachRolePolicy",
|
||||
"iam:PutRolePolicy",
|
||||
"iam:DeleteRolePolicy",
|
||||
"iam:DeleteRole",
|
||||
"iam:UpdateRole",
|
||||
"iam:PutRolePermissionsBoundary",
|
||||
"iam:DeleteRolePermissionsBoundary",
|
||||
"iam:TagRole",
|
||||
"iam:UntagRole"
|
||||
],
|
||||
"Resource": "arn:aws:iam::*:role/platform/*",
|
||||
"Condition": {
|
||||
"ArnNotLike": {
|
||||
"aws:PrincipalArn": [
|
||||
"arn:aws:iam::*:role/OrganizationAccountAccessRole",
|
||||
"arn:aws:iam::*:role/aws-reserved/sso.amazonaws.com/*AWSReservedSSO_Platform_*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@
|
|||
# scripts/create-hcptf-bootstrap-roles.sh --account prod --allow-workspace STACK-prod
|
||||
#
|
||||
# --simulate runs iam:SimulatePrincipalPolicy against the apply role. Requires
|
||||
# the role to already exist.
|
||||
# the role to already exist. The policy source is the live Role.Arn from
|
||||
# iam:GetRole, so an existing unpathed role and a /platform/ create both match.
|
||||
# The platform-path case must come back denied. role/hcptf-* does not cover
|
||||
# role/platform/*, and ProtectPlatformPath denies that resource for every
|
||||
# principal except OrganizationAccountAccessRole and the Platform SSO role.
|
||||
#
|
||||
# Default trust is exact StringEquals for workspace iam-bootstrap-<env> only.
|
||||
# HCP workspace names are org-unique, so prod and dev cannot both be
|
||||
|
|
@ -44,7 +48,7 @@ while [[ $# -gt 0 ]]; do
|
|||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--simulate) SIMULATE=1; shift ;;
|
||||
--allow-workspace) ALLOW_WORKSPACE="$2"; shift 2 ;;
|
||||
-h|--help) sed -n '2,36p' "$0"; exit 0 ;;
|
||||
-h|--help) sed -n '2,35p' "$0"; exit 0 ;;
|
||||
-*) echo "unknown flag: $1" >&2; exit 2 ;;
|
||||
*) echo "unexpected argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
|
|
@ -145,8 +149,41 @@ AWS_SESSION_TOKEN="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["Se
|
|||
echo "account: ${ACCOUNT_ID} (${ACCOUNT_KEY})"
|
||||
echo "caller: $(aws sts get-caller-identity --query Arn --output text)"
|
||||
|
||||
# Live ARN, checked against the role path. IAM role names are unique per
|
||||
# account, so GetRole finds either /hcptf-bootstrap or /platform/hcptf-bootstrap.
|
||||
role_arn() {
|
||||
local name="$1"
|
||||
local line arn path
|
||||
# Command substitution so set -e stops when GetRole fails. A missing role
|
||||
# must not fall through to a printed ARN.
|
||||
line="$(aws iam get-role --role-name "$name" \
|
||||
--query 'Role.[Arn,Path]' --output text)"
|
||||
arn="${line%%$'\t'*}"
|
||||
path="${line#*$'\t'}"
|
||||
path="${path%$'\r'}"
|
||||
case "$path" in
|
||||
/)
|
||||
[[ "$arn" == "arn:aws:iam::${ACCOUNT_ID}:role/${name}" ]] || {
|
||||
echo "${name}: ARN ${arn} does not match path ${path}" >&2
|
||||
exit 1
|
||||
}
|
||||
;;
|
||||
/platform/)
|
||||
[[ "$arn" == "arn:aws:iam::${ACCOUNT_ID}:role/platform/${name}" ]] || {
|
||||
echo "${name}: ARN ${arn} does not match path ${path}" >&2
|
||||
exit 1
|
||||
}
|
||||
;;
|
||||
*)
|
||||
echo "${name}: unexpected path ${path} (ARN ${arn})" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' "$arn"
|
||||
}
|
||||
|
||||
if [[ "$SIMULATE" -eq 1 ]]; then
|
||||
APPLY_ARN="arn:aws:iam::${ACCOUNT_ID}:role/hcptf-bootstrap"
|
||||
APPLY_ARN="$(role_arn hcptf-bootstrap)"
|
||||
echo "== simulate ${APPLY_ARN} =="
|
||||
echo "-- CreateRole with tf-managed boundary (expect allowed) --"
|
||||
aws iam simulate-principal-policy \
|
||||
|
|
@ -171,7 +208,7 @@ if [[ "$SIMULATE" -eq 1 ]]; then
|
|||
--resource-arns "arn:aws:iam::${ACCOUNT_ID}:policy/tf-managed/example" \
|
||||
--query 'EvaluationResults[].{Action:EvalActionName,Decision:EvalDecision}' \
|
||||
--output table
|
||||
echo "-- PutRolePolicy on hcptf-* (expect allowed; import/first-apply path) --"
|
||||
echo "-- PutRolePolicy on unpathed role/hcptf-example (expect allowed; import/first-apply path) --"
|
||||
aws iam simulate-principal-policy \
|
||||
--policy-source-arn "$APPLY_ARN" \
|
||||
--action-names iam:PutRolePolicy iam:DetachRolePolicy \
|
||||
|
|
@ -187,6 +224,24 @@ if [[ "$SIMULATE" -eq 1 ]]; then
|
|||
"arn:aws:iam::${ACCOUNT_ID}:role/OrganizationAccountAccessRole" \
|
||||
--query 'EvaluationResults[].{Action:EvalActionName,Resource:EvalResourceName,Decision:EvalDecision}' \
|
||||
--output table
|
||||
echo "-- IAM changes on role/platform/hcptf-example (expect denied) --"
|
||||
platform_sim="$(aws iam simulate-principal-policy \
|
||||
--policy-source-arn "$APPLY_ARN" \
|
||||
--action-names iam:CreateRole iam:PutRolePolicy iam:DeleteRole \
|
||||
--resource-arns "arn:aws:iam::${ACCOUNT_ID}:role/platform/hcptf-example" \
|
||||
--query 'EvaluationResults[].{Action:EvalActionName,Decision:EvalDecision}' \
|
||||
--output table)"
|
||||
printf '%s\n' "$platform_sim"
|
||||
if grep -q 'allowed' <<<"$platform_sim"; then
|
||||
echo "role/platform/* simulation allowed an IAM change" >&2
|
||||
exit 1
|
||||
fi
|
||||
for action in iam:CreateRole iam:PutRolePolicy iam:DeleteRole; do
|
||||
grep -q "$action" <<<"$platform_sim" || {
|
||||
echo "role/platform/* simulation missing ${action}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
|
@ -239,10 +294,14 @@ create_or_update_role() {
|
|||
aws iam update-assume-role-policy --role-name "$name" --policy-document "file://${trust_file}"
|
||||
fi
|
||||
else
|
||||
echo " $name: create"
|
||||
echo " $name: create on /platform/"
|
||||
# Path is create-only. IAM cannot move an existing role onto /platform/.
|
||||
# Prod and dev already have hcptf-bootstrap and hcptf-bootstrap-plan, so
|
||||
# this branch does not run for them. Do not delete and recreate to set a path.
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
aws iam create-role \
|
||||
--role-name "$name" \
|
||||
--path /platform/ \
|
||||
--assume-role-policy-document "file://${trust_file}" \
|
||||
--description "HCP Terraform ${name} (PLAT-145). Console/CLI owned. Manual apply only." \
|
||||
--tags Key=Project,Value=hcp-bootstrap Key=Owner,Value=adam@seahavenind.com Key=ManagedBy,Value=cli
|
||||
|
|
@ -277,9 +336,11 @@ aws iam attach-role-policy \
|
|||
2>/dev/null || true
|
||||
echo " hcptf-bootstrap-plan: attached ViewOnlyAccess"
|
||||
|
||||
APPLY_ARN="$(role_arn hcptf-bootstrap)"
|
||||
PLAN_ARN="$(role_arn hcptf-bootstrap-plan)"
|
||||
echo "done. Next: HCP workspace ${BOOTSTRAP_WORKSPACE} in ${HCP_PROJECT}, Manual apply,"
|
||||
echo " TFC_AWS_APPLY_ROLE_ARN=arn:aws:iam::${ACCOUNT_ID}:role/hcptf-bootstrap"
|
||||
echo " TFC_AWS_PLAN_ROLE_ARN=arn:aws:iam::${ACCOUNT_ID}:role/hcptf-bootstrap-plan"
|
||||
echo " TFC_AWS_APPLY_ROLE_ARN=${APPLY_ARN}"
|
||||
echo " TFC_AWS_PLAN_ROLE_ARN=${PLAN_ARN}"
|
||||
if [[ -n "$ALLOW_WORKSPACE" ]]; then
|
||||
echo "First-apply/import window: point workspace ${ALLOW_WORKSPACE} TFC_AWS_* at the pair above,"
|
||||
echo " apply, retarget scoped ARNs, then re-run this script with no --allow-workspace."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue