seahaven-org-baseline/scripts/delete-terraform-substrate-prod-dev.sh
Adam Moussa b02f52b805
feat(iam): lock app-owned HCP IAM and add bootstrap SCP (PLAT-143) (#137)
* feat(iam): lock app-owned HCP IAM and add bootstrap SCP (PLAT-143)

* fix(iam): pin HCP boundary ARNs and bootstrap trust window (PLAT-143)

Null on iam:PermissionsBoundary accepted any ceiling, including AdministratorAccess. Import apply cannot self-mutate hcptf-* while bootstrap trust is iam-bootstrap only; add a time-boxed exact StringEquals workspace grant instead of StringLike.
2026-09-02 15:22:48 +00:00

111 lines
4.3 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# delete-terraform-substrate-prod-dev.sh — PLAT-147 live delete of
# seahaven-terraform-substrate in prod (011934824531) and/or dev (710827005802).
# Does not touch terraform-substrate-external-dev (396287094661).
#
# Gate: every prod/dev hcptf-* apply role MUST already have detached
# seahaven-hcptf-iam-management (consumer import PRs). OIDC is Retain and
# survives. The guardrail policy is deleted with the stack.
#
# Do not run until:
# 1. Each of the eight prod apply roles is imported in app Terraform.
# 2. A substrate update with DeletionPolicy: Retain has removed those roles
# from the template (CFN forgets them without deleting).
# 3. terraform-substrate-prod and terraform-substrate-dev are removed from
# bin/app.ts and .github/workflows/deploy.yaml so CD cannot recreate them.
#
# Usage:
# scripts/delete-terraform-substrate-prod-dev.sh --account prod|dev --inventory
# scripts/delete-terraform-substrate-prod-dev.sh --account prod|dev --yes
#
set -euo pipefail
ACCOUNT_KEY=""
INVENTORY=0
ASSUME_YES=0
while [[ $# -gt 0 ]]; do
case "$1" in
--account) ACCOUNT_KEY="$2"; shift 2 ;;
--inventory) INVENTORY=1; shift ;;
--yes|-y) ASSUME_YES=1; shift ;;
-h|--help) sed -n '2,24p' "$0"; exit 0 ;;
-*) echo "unknown flag: $1" >&2; exit 2 ;;
*) echo "unexpected argument: $1" >&2; exit 2 ;;
esac
done
case "$ACCOUNT_KEY" in
prod) ACCOUNT_ID="011934824531" ;;
dev) ACCOUNT_ID="710827005802" ;;
*)
echo "usage: $0 --account prod|dev [--inventory|--yes]" >&2
exit 2
;;
esac
if [[ "$ACCOUNT_ID" == "396287094661" ]]; then
echo "refusing: external-dev is out of scope (PLAT-148)" >&2
exit 2
fi
ORIG_AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID-}"
ORIG_AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY-}"
ORIG_AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN-}"
restore_creds() {
if [[ -n "${ORIG_AWS_ACCESS_KEY_ID}" ]]; then
export AWS_ACCESS_KEY_ID="$ORIG_AWS_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$ORIG_AWS_SECRET_ACCESS_KEY"
export AWS_SESSION_TOKEN="$ORIG_AWS_SESSION_TOKEN"
else
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
fi
}
trap restore_creds EXIT
CREDS="$(aws sts assume-role \
--role-arn "arn:aws:iam::${ACCOUNT_ID}:role/OrganizationAccountAccessRole" \
--role-session-name plat-147-tf-substrate \
--query Credentials --output json)"
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
AWS_ACCESS_KEY_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["AccessKeyId"])' <<<"$CREDS")"
AWS_SECRET_ACCESS_KEY="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["SecretAccessKey"])' <<<"$CREDS")"
AWS_SESSION_TOKEN="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["SessionToken"])' <<<"$CREDS")"
echo "account: ${ACCOUNT_ID} (${ACCOUNT_KEY})"
echo "caller: $(aws sts get-caller-identity --query Arn --output text)"
POLICY_ARN="arn:aws:iam::${ACCOUNT_ID}:policy/seahaven-hcptf-iam-management"
ATTACHED="[]"
if aws iam get-policy --policy-arn "$POLICY_ARN" >/dev/null 2>&1; then
ATTACHED="$(aws iam list-entities-for-policy --policy-arn "$POLICY_ARN" --query 'PolicyRoles[].RoleName' --output json)"
fi
echo "seahaven-hcptf-iam-management attachments: $ATTACHED"
HCPS="$(aws iam list-roles --query 'Roles[?starts_with(RoleName, `hcptf-`)].RoleName' --output json)"
echo "hcptf-* roles still present: $HCPS"
OIDC="$(aws iam list-open-id-connect-providers --query 'OpenIDConnectProviderList[?contains(Arn, `app.terraform.io`)].Arn' --output json)"
echo "app.terraform.io OIDC: $OIDC"
if [[ "$INVENTORY" -eq 1 ]]; then
exit 0
fi
python3 - "$ATTACHED" <<'PY'
import json, sys
roles = json.loads(sys.argv[1])
if roles:
raise SystemExit(f"refusing delete: seahaven-hcptf-iam-management still attached to {roles}")
PY
if [[ "$ASSUME_YES" -eq 0 ]]; then
read -r -p "delete CloudFormation stack seahaven-terraform-substrate in ${ACCOUNT_ID}? [y/N] " ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo "skipped"; exit 0; }
fi
echo "deleting seahaven-terraform-substrate (OIDC DeletionPolicy=Retain)"
aws cloudformation delete-stack --stack-name seahaven-terraform-substrate
aws cloudformation wait stack-delete-complete --stack-name seahaven-terraform-substrate
echo "stack gone. confirm OIDC still exists:"
aws iam list-open-id-connect-providers --query 'OpenIDConnectProviderList[?contains(Arn, `app.terraform.io`)].Arn' --output text