diff --git a/README.md b/README.md index 09ea358..b167320 100644 --- a/README.md +++ b/README.md @@ -463,8 +463,9 @@ inline policy name. **HCP Terraform layout (org-level setup, console):** one org `seahaven` (free tier: 500 managed resources, 1 concurrent run); one HCP **project per AWS account** (`seahaven-prod`, `seahaven-dev`); one **workspace per stack** -(`-`, one state file = one blast radius). Dedicated -`iam-bootstrap` workspace in each prod/dev project (Manual apply only). +(`-`, one state file = one blast radius). Dedicated `iam-bootstrap-` workspace in each prod/dev project +(`iam-bootstrap-prod`, `iam-bootstrap-dev`; HCP workspace names are +org-unique). Manual apply only. Default execution mode Remote. Never use HCP's "Quick setup AWS dynamic credentials" button — it writes the single `TFC_AWS_RUN_ROLE_ARN`, which collapses the plan/apply role split. Never project-scoped variable sets. @@ -473,7 +474,7 @@ collapses the plan/apply role split. Never project-scoped variable sets. `StringEquals` on `iam:PermissionsBoundary` with a factory vs scoped split. - **General apply role `hcptf-bootstrap`:** trust pinned to - `organization:seahaven:project:seahaven-:workspace:iam-bootstrap:run_phase:apply` + `organization:seahaven:project:seahaven-:workspace:iam-bootstrap-:run_phase:apply` (exact `StringEquals` on `aud` and `sub`; never `StringLike` on `run_phase` or workspace). IAM writes on `role/tf-managed/*` and `policy/tf-managed/*`. `CreatePolicy` / `CreatePolicyVersion` only here. @@ -488,7 +489,7 @@ collapses the plan/apply role split. Never project-scoped variable sets. (OAA and CDK may still update trust). Manual apply. Not in external-dev. - **General plan role `hcptf-bootstrap-plan`:** `ViewOnlyAccess` plus a refresh sidecar. Never `ReadOnlyAccess`. Never IAM writes. Trust - `run_phase:plan` on the same `iam-bootstrap` workspace. + `run_phase:plan` on the matching `iam-bootstrap-` workspace. - **Scoped apply role `hcptf-`:** trust pinned to that stack's workspace and `run_phase:apply`. No `seahaven-hcptf-iam-management`. May manage `role/tf-managed/-*` with `iam:PermissionsBoundary` @@ -517,7 +518,7 @@ on the SAM guardrail. Do not add `ArnLike` to deploy-substrate. include it. Never a project-scoped variable set. 3. `scripts/create-hcptf-bootstrap-roles.sh --account prod|dev --allow-workspace -` (OAA). Trust stays exact `StringEquals` - on `iam-bootstrap` plus this one workspace. Never `StringLike`. + on `iam-bootstrap-` plus this one workspace. Never `StringLike`. 4. Point this workspace's `TFC_AWS_APPLY_ROLE_ARN` / `TFC_AWS_PLAN_ROLE_ARN` at `hcptf-bootstrap` / `hcptf-bootstrap-plan`. 5. App PR adds `aws_iam_role` plan/apply (trust exact `StringEquals`), a @@ -528,7 +529,7 @@ on the SAM guardrail. Do not add `ArnLike` to deploy-substrate. 6. One Manual apply. Roles and boundary exist (tolerate partial state on non-IAM resources). 7. Switch workspace vars to the new scoped ARNs. Re-run the create script - with no `--allow-workspace` so trust is `iam-bootstrap` only again. + with no `--allow-workspace` so trust is `iam-bootstrap-` only again. 8. Second Manual apply with the scoped role. Prove the stack. Then seal. 9. Live-path proof, cutover, docs as before. diff --git a/examples/hcptf-workspace-iam/hcp_iam.tf.example b/examples/hcptf-workspace-iam/hcp_iam.tf.example index 614ad42..ff4cb80 100644 --- a/examples/hcptf-workspace-iam/hcp_iam.tf.example +++ b/examples/hcptf-workspace-iam/hcp_iam.tf.example @@ -7,8 +7,8 @@ # Live `seahaven-hcptf-iam-management` DenySelfMutation blocks DetachRolePolicy # and PutRolePolicy on hcptf-* (including this role). The stack workspace # cannot apply this file while TFC_AWS_* still points at hcptf-STACK, and -# hcptf-bootstrap trust is exact StringEquals for workspace iam-bootstrap -# only. Import apply sequence: +# hcptf-bootstrap trust is exact StringEquals for workspace +# iam-bootstrap- only (HCP names are org-unique). Import apply sequence: # 1. scripts/create-hcptf-bootstrap-roles.sh --account prod|dev \ # --allow-workspace STACK-prod # 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap / @@ -17,7 +17,7 @@ # put scoped inline). # 4. Point TFC_AWS_* back at hcptf-STACK / hcptf-STACK-plan. # 5. Re-run the script without --allow-workspace to pin trust back to -# iam-bootstrap only. +# iam-bootstrap- only. # Later apply-role IAM edits use the same window. Do not add StringLike # on bootstrap trust. # diff --git a/lib/hcptf-bootstrap/trust-apply.json.tmpl b/lib/hcptf-bootstrap/trust-apply.json.tmpl index 6e82213..2335c89 100644 --- a/lib/hcptf-bootstrap/trust-apply.json.tmpl +++ b/lib/hcptf-bootstrap/trust-apply.json.tmpl @@ -10,7 +10,7 @@ "Condition": { "StringEquals": { "app.terraform.io:aud": "aws.workload.identity", - "app.terraform.io:sub": "organization:seahaven:project:__HCP_PROJECT__:workspace:iam-bootstrap:run_phase:apply" + "app.terraform.io:sub": "organization:seahaven:project:__HCP_PROJECT__:workspace:__BOOTSTRAP_WORKSPACE__:run_phase:apply" } } } diff --git a/lib/hcptf-bootstrap/trust-plan.json.tmpl b/lib/hcptf-bootstrap/trust-plan.json.tmpl index 468f89c..d880fed 100644 --- a/lib/hcptf-bootstrap/trust-plan.json.tmpl +++ b/lib/hcptf-bootstrap/trust-plan.json.tmpl @@ -10,7 +10,7 @@ "Condition": { "StringEquals": { "app.terraform.io:aud": "aws.workload.identity", - "app.terraform.io:sub": "organization:seahaven:project:__HCP_PROJECT__:workspace:iam-bootstrap:run_phase:plan" + "app.terraform.io:sub": "organization:seahaven:project:__HCP_PROJECT__:workspace:__BOOTSTRAP_WORKSPACE__:run_phase:plan" } } } diff --git a/scripts/create-hcptf-bootstrap-roles.sh b/scripts/create-hcptf-bootstrap-roles.sh index 09b4cbf..72ed30a 100755 --- a/scripts/create-hcptf-bootstrap-roles.sh +++ b/scripts/create-hcptf-bootstrap-roles.sh @@ -18,14 +18,16 @@ # --simulate runs iam:SimulatePrincipalPolicy against the apply role. Requires # the role to already exist. # -# Default trust is exact StringEquals for workspace iam-bootstrap only. -# --allow-workspace NAME adds one extra exact sub for that HCP workspace -# (first-apply / import window). Re-run with no --allow-workspace to pin -# trust back to iam-bootstrap only. Never StringLike. SCP blocks +# Default trust is exact StringEquals for workspace iam-bootstrap- only. +# HCP workspace names are org-unique, so prod and dev cannot both be +# "iam-bootstrap". --allow-workspace NAME adds one extra exact sub for that +# HCP workspace (first-apply / import window). Re-run with no --allow-workspace +# to pin trust back to iam-bootstrap- only. Never StringLike. SCP blocks # hcptf-bootstrap from updating its own trust; this script assumes OAA. # -# After create: HCP workspace iam-bootstrap in project seahaven-, Manual -# apply, workspace-level TFC_AWS_*_ROLE_ARN only (never a project variable set). +# After create: HCP workspace iam-bootstrap- in project seahaven-, +# Manual apply, workspace-level TFC_AWS_*_ROLE_ARN only (never a project +# variable set). # set -euo pipefail @@ -52,10 +54,12 @@ case "$ACCOUNT_KEY" in prod) ACCOUNT_ID="011934824531" HCP_PROJECT="seahaven-prod" + BOOTSTRAP_WORKSPACE="iam-bootstrap-prod" ;; dev) ACCOUNT_ID="710827005802" HCP_PROJECT="seahaven-dev" + BOOTSTRAP_WORKSPACE="iam-bootstrap-dev" ;; *) echo "usage: $0 --account prod|dev [--dry-run] [--simulate] [--allow-workspace NAME]" >&2 @@ -64,8 +68,8 @@ case "$ACCOUNT_KEY" in esac if [[ -n "$ALLOW_WORKSPACE" ]]; then - if [[ "$ALLOW_WORKSPACE" == "iam-bootstrap" ]]; then - echo "--allow-workspace iam-bootstrap is the default; omit the flag" >&2 + if [[ "$ALLOW_WORKSPACE" == "$BOOTSTRAP_WORKSPACE" ]]; then + echo "--allow-workspace ${BOOTSTRAP_WORKSPACE} is the default; omit the flag" >&2 exit 2 fi if [[ ! "$ALLOW_WORKSPACE" =~ ^[a-z0-9]([a-z0-9-]{0,88}[a-z0-9])?$ ]]; then @@ -77,7 +81,10 @@ fi render_to() { local src="$1" local dest="$2" - sed -e "s/__ACCOUNT_ID__/${ACCOUNT_ID}/g" -e "s/__HCP_PROJECT__/${HCP_PROJECT}/g" "$src" > "$dest" + sed -e "s/__ACCOUNT_ID__/${ACCOUNT_ID}/g" \ + -e "s/__HCP_PROJECT__/${HCP_PROJECT}/g" \ + -e "s/__BOOTSTRAP_WORKSPACE__/${BOOTSTRAP_WORKSPACE}/g" \ + "$src" > "$dest" } # Render a trust template. Optional extra workspace becomes a second exact @@ -85,10 +92,15 @@ render_to() { render_trust() { local src="$1" local dest="$2" - python3 - "$src" "$dest" "$ACCOUNT_ID" "$HCP_PROJECT" "$ALLOW_WORKSPACE" <<'PY' + python3 - "$src" "$dest" "$ACCOUNT_ID" "$HCP_PROJECT" "$BOOTSTRAP_WORKSPACE" "$ALLOW_WORKSPACE" <<'PY' import json, pathlib, sys -src, dest, account, project, extra = sys.argv[1:6] -text = pathlib.Path(src).read_text().replace("__ACCOUNT_ID__", account).replace("__HCP_PROJECT__", project) +src, dest, account, project, bootstrap_ws, extra = sys.argv[1:7] +text = ( + pathlib.Path(src).read_text() + .replace("__ACCOUNT_ID__", account) + .replace("__HCP_PROJECT__", project) + .replace("__BOOTSTRAP_WORKSPACE__", bootstrap_ws) +) data = json.loads(text) if extra: cond = data["Statement"][0]["Condition"]["StringEquals"] @@ -193,7 +205,7 @@ render_to "$TMPL/plan-refresh-policy.json.tmpl" "$WORKDIR/plan-refresh.json" if [[ -n "$ALLOW_WORKSPACE" ]]; then echo "trust extra workspace: ${ALLOW_WORKSPACE} (exact StringEquals; re-run without this flag to revoke)" else - echo "trust: iam-bootstrap only" + echo "trust: ${BOOTSTRAP_WORKSPACE} only" fi python3 - "$WORKDIR" <<'PY' @@ -265,7 +277,7 @@ aws iam attach-role-policy \ 2>/dev/null || true echo " hcptf-bootstrap-plan: attached ViewOnlyAccess" -echo "done. Next: HCP workspace iam-bootstrap in ${HCP_PROJECT}, Manual apply," +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" if [[ -n "$ALLOW_WORKSPACE" ]]; then