fix(scripts): account-id assertion in cfn-stack-decommission; complete the aws-cdk-lib 2.262.0 bump (patched brace-expansion); document CMK cutover trap

- cfn-stack-decommission.sh: --account-id is now REQUIRED and asserted
  against sts get-caller-identity before anything runs. Stack names are no
  longer org-unique (seahaven-dynamodb-cmk now exists in mgmt AND prod), so
  a name-only lookup under the wrong ambient profile could report or delete
  the wrong account's stack (security-review LOGIC-001).
- package.json/lock: PR #56's bump-for-patched-brace-expansion landed the
  commit title but not the pin; package.json still said 2.261.0 and the
  lockfile still resolved brace-expansion 5.0.6 (GHSA-3jxr-9vmj-r5cp HIGH,
  blocking the pre-commit scanner). Pin 2.262.0 and regenerate; npm audit
  now clean.
- bin/app.ts comments: slack-bot cutover MUST grant the PROD key ARN, never
  the account-local mgmt SSM param (LOGIC-005); failed-first-create orphan
  CMK recovery note (LOGIC-004).
This commit is contained in:
Adam Moussa 2026-07-23 14:46:17 -04:00
parent 5dab14ce68
commit c1347fcdb5
No known key found for this signature in database
4 changed files with 63 additions and 26 deletions

View file

@ -185,6 +185,17 @@ const prodDynamoCmk = new DynamoDbCmkStack(app, "dynamodb-cmk-prod", {
// PrincipalArn is wildcarded by stack prefix because CFN role suffixes rotate;
// each bot role must ALSO carry an identity-policy grant on this key ARN
// (added in the slack-bot repo's cutover PR).
//
// ⚠️ CUTOVER TRAP: the slack-bot's existing pattern resolves the CMK via SSM
// /seahaven/dynamodb/cmk-arn — that parameter is ACCOUNT-LOCAL and in mgmt
// resolves the MGMT key forever. The cutover PR's identity grant must use the
// PROD key ARN (this stack's DynamoDbCmkArn output), never the mgmt param;
// reusing the SSM pattern grants the wrong key and fails only at runtime.
//
// Recovery note (failed FIRST create): the key is RETAIN, its alias/SSM param
// are not — a CREATE_FAILED rollback orphans an unaliased rotation-enabled
// key. Before re-running the deploy, list unaliased CMKs in prod and schedule
// deletion of the orphan.
prodDynamoCmk.key.addToResourcePolicy(
new iam.PolicyStatement({
sid: "AllowMgmtSlackBotReadViaDynamoDb",

53
package-lock.json generated
View file

@ -1,14 +1,14 @@
{
"name": "seahaven-account-baseline",
"name": "seahaven-org-baseline",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "seahaven-account-baseline",
"name": "seahaven-org-baseline",
"version": "1.0.0",
"dependencies": {
"aws-cdk-lib": "2.261.0",
"aws-cdk-lib": "2.262.0",
"constructs": "^10.0.0"
},
"bin": {
@ -36,9 +36,9 @@
"license": "Apache-2.0"
},
"node_modules/@aws-cdk/cloud-assembly-schema": {
"version": "54.2.0",
"resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.2.0.tgz",
"integrity": "sha512-u3lFXmiXSBozxGBmKTCVD/2mTDsaXzLZH3KYiIQKcB+zPldXOeE5TnooBgKV9ih2jVTo8ML0HpkhfAq2eiv0eQ==",
"version": "54.13.0",
"resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.13.0.tgz",
"integrity": "sha512-C6LS1YxugR7j6BPVjhVsWRu/VNN8eoGDOf7dHafPviz6Y5Nv/FjVIGIPoC6jJmgJcea7VOM9TPHbBEwapCUySg==",
"bundleDependencies": [
"jsonschema",
"semver"
@ -46,7 +46,7 @@
"license": "Apache-2.0",
"dependencies": {
"jsonschema": "^1.5.0",
"semver": "^7.8.1"
"semver": "^7.8.5"
},
"engines": {
"node": ">= 18.0.0"
@ -61,7 +61,7 @@
}
},
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": {
"version": "7.8.1",
"version": "7.8.5",
"inBundle": true,
"license": "ISC",
"bin": {
@ -887,10 +887,11 @@
}
},
"node_modules/aws-cdk-lib": {
"version": "2.261.0",
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.261.0.tgz",
"integrity": "sha512-e52e3Abjg0HkuRWlWwtSv5+ZiMW1rhCDdL9ff7lzWXInU8xdfLJpuoimfa0IJwjiNGyphppgg52Azx9M80OA0g==",
"version": "2.262.0",
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.262.0.tgz",
"integrity": "sha512-6zRVoWRd8kQs9ZZ9xhERSm36W8uWS2vW3/g9Zx0xlhvRRiUwTc80bzfJkohKGdT/5AOW+wy6fSDvohavG94pcA==",
"bundleDependencies": [
"@aws/cloudformation-validate",
"@balena/dockerignore",
"@aws-cdk/cloud-assembly-api",
"case",
@ -907,17 +908,18 @@
"dependencies": {
"@aws-cdk/asset-awscli-v1": "2.2.282",
"@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2",
"@aws-cdk/cloud-assembly-api": "^2.2.5",
"@aws-cdk/cloud-assembly-schema": "^54.0.0",
"@aws-cdk/cloud-assembly-api": "^2.2.6",
"@aws-cdk/cloud-assembly-schema": "^54.11.0",
"@aws/cloudformation-validate": "1.5.0-beta",
"@balena/dockerignore": "^1.0.2",
"case": "1.6.3",
"fs-extra": "^11.3.5",
"fs-extra": "^11.3.6",
"ignore": "^5.3.2",
"jsonschema": "^1.5.0",
"mime-types": "^2.1.35",
"minimatch": "^10.2.5",
"punycode": "^2.3.1",
"semver": "^7.8.1",
"semver": "^7.8.5",
"yaml": "1.10.3"
},
"engines": {
@ -928,18 +930,27 @@
}
},
"node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": {
"version": "2.2.5",
"version": "2.2.6",
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
"jsonschema": "^1.5.0",
"semver": "^7.8.0"
"semver": "^7.8.4"
},
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"@aws-cdk/cloud-assembly-schema": ">=53.28.0"
"@aws-cdk/cloud-assembly-schema": ">=54.5.0"
}
},
"node_modules/aws-cdk-lib/node_modules/@aws/cloudformation-validate": {
"version": "1.5.0-beta",
"inBundle": true,
"license": "Apache-2.0",
"engines": {
"node": "^22.15.0",
"npm": ">=10.5.0"
}
},
"node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": {
@ -956,7 +967,7 @@
}
},
"node_modules/aws-cdk-lib/node_modules/brace-expansion": {
"version": "5.0.6",
"version": "5.0.7",
"inBundle": true,
"license": "MIT",
"dependencies": {
@ -975,7 +986,7 @@
}
},
"node_modules/aws-cdk-lib/node_modules/fs-extra": {
"version": "11.3.5",
"version": "11.3.6",
"inBundle": true,
"license": "MIT",
"dependencies": {
@ -1061,7 +1072,7 @@
}
},
"node_modules/aws-cdk-lib/node_modules/semver": {
"version": "7.8.1",
"version": "7.8.5",
"inBundle": true,
"license": "ISC",
"bin": {

View file

@ -20,7 +20,7 @@
"typescript": "~7.0.2"
},
"dependencies": {
"aws-cdk-lib": "2.261.0",
"aws-cdk-lib": "2.262.0",
"constructs": "^10.0.0"
}
}

View file

@ -12,28 +12,43 @@
# `feedback_cfn_decommission_and_remediation`.
#
# Usage:
# scripts/cfn-stack-decommission.sh [--profile NAME] [--execute] STACK
# scripts/cfn-stack-decommission.sh --account-id ID [--profile NAME] [--execute] STACK
#
# --account-id REQUIRED. Asserted against the credentials' actual account
# before anything runs. Stack names are NOT org-unique
# (seahaven-dynamodb-cmk exists in both mgmt and prod), so a
# name-only lookup with the wrong ambient profile would
# report — or with --execute, DELETE — the wrong account's
# stack and then purge its Retain orphans.
# (no --execute) REPORT only: termination protection, consumed exports,
# Retain resources (orphans-to-be), in-stack S3 buckets.
# --execute Disable termination protection, empty Delete-policy buckets,
# delete the stack, wait, then delete the Retain orphans.
#
set -euo pipefail
PROFILE_ARG=(); EXECUTE=0; STACK=""
PROFILE_ARG=(); EXECUTE=0; STACK=""; EXPECTED_ACCOUNT=""
while [[ $# -gt 0 ]]; do
case "$1" in
--profile) PROFILE_ARG=(--profile "$2"); shift 2 ;;
--account-id) EXPECTED_ACCOUNT="$2"; shift 2 ;;
--execute) EXECUTE=1; shift ;;
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
-h|--help) sed -n '2,28p' "$0"; exit 0 ;;
-*) echo "unknown flag: $1" >&2; exit 2 ;;
*) STACK="$1"; shift ;;
esac
done
[[ -z "$STACK" ]] && { echo "usage: $0 [--profile NAME] [--execute] STACK" >&2; exit 2; }
[[ -z "$STACK" ]] && { echo "usage: $0 --account-id ID [--profile NAME] [--execute] STACK" >&2; exit 2; }
[[ -z "$EXPECTED_ACCOUNT" ]] && { echo "ERROR: --account-id is required (stack names are not org-unique)." >&2; exit 2; }
aws_() { aws "${PROFILE_ARG[@]}" "$@"; }
R="us-east-1"
CALLER_ACCOUNT="$(aws_ sts get-caller-identity --query Account --output text)"
if [[ "$CALLER_ACCOUNT" != "$EXPECTED_ACCOUNT" ]]; then
echo "ABORT: credentials resolve to account $CALLER_ACCOUNT, expected $EXPECTED_ACCOUNT." >&2
exit 1
fi
echo "== account: $CALLER_ACCOUNT (verified) =="
echo "== stack: $STACK =="
aws_ cloudformation describe-stacks --stack-name "$STACK" --region "$R" \
--query 'Stacks[0].{Status:StackStatus,TermProt:EnableTerminationProtection}' --output table