fix(iam): use unique HCP bootstrap workspace names (PLAT-143) (#138)
Some checks are pending
Deploy / deploy-management (push) Waiting to run
Deploy / deploy-external-dev (push) Waiting to run
Deploy / deploy-security (push) Waiting to run
Deploy / deploy-dev (push) Waiting to run
Deploy / deploy-prod (push) Waiting to run

HCP workspace names are org-unique, so prod and dev cannot both be iam-bootstrap. Pin trust to iam-bootstrap-prod and iam-bootstrap-dev.
This commit is contained in:
Adam Moussa 2026-09-02 15:51:39 +00:00 • committed by GitHub
parent b02f52b805
commit 4f0d84cddb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 38 additions and 25 deletions

View file

@ -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**
(`<stack>-<env>`, one state file = one blast radius). Dedicated
`iam-bootstrap` workspace in each prod/dev project (Manual apply only).
(`<stack>-<env>`, one state file = one blast radius). Dedicated `iam-bootstrap-<env>` 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-<env>:workspace:iam-bootstrap:run_phase:apply`
`organization:seahaven:project:seahaven-<env>:workspace:iam-bootstrap-<env>: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-<env>` workspace.
- **Scoped apply role `hcptf-<stack>`:** trust pinned to that stack's
workspace and `run_phase:apply`. No `seahaven-hcptf-iam-management`. May
manage `role/tf-managed/<prefix>-*` 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 <stack>-<env>` (OAA). Trust stays exact `StringEquals`
on `iam-bootstrap` plus this one workspace. Never `StringLike`.
on `iam-bootstrap-<env>` 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-<env>` only again.
8. Second Manual apply with the scoped role. Prove the stack. Then seal.
9. Live-path proof, cutover, docs as before.

View file

@ -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-<env> 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-<env> only.
# Later apply-role IAM edits use the same window. Do not add StringLike
# on bootstrap trust.
#

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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-<env> 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-<env> 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-<env>, Manual
# apply, workspace-level TFC_AWS_*_ROLE_ARN only (never a project variable set).
# After create: HCP workspace iam-bootstrap-<env> in project seahaven-<env>,
# 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