mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-07 13:48:56 +00:00
docs(iam): codify hcp terraform migration checklist from PLAT-56 (#79)
Expand the README playbook to steps 0–10 and document the required plan-refresh sidecar plus prefix-scoped apply-role wildcards so the next workload copies afi patterns instead of relearning first-apply misses.
This commit is contained in:
parent
2c5c644a5e
commit
9ee4d4a3d7
2 changed files with 103 additions and 36 deletions
107
README.md
107
README.md
|
|
@ -253,49 +253,102 @@ 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
|
writes the single `TFC_AWS_RUN_ROLE_ARN`, which collapses the plan/apply role
|
||||||
split this substrate exists to enforce.
|
split this substrate exists to enforce.
|
||||||
|
|
||||||
|
**Reference implementation:** first workload was `afi-backup-monitor` in
|
||||||
|
seahaven-prod (PLAT-56). Copy
|
||||||
|
`Sea-Haven-Industries/afi-backup-monitor` `terraform/` and the live
|
||||||
|
`hcptf-afi-backup-monitor*` / `hcptf-afi-backup-monitor-plan` statements in
|
||||||
|
this template rather than inventing new IAM shapes.
|
||||||
|
|
||||||
**Migration checklist (per stack, in order):**
|
**Migration checklist (per stack, in order):**
|
||||||
|
|
||||||
1. Create the workspace in the target account's HCP project
|
0. **Freeze the app's SAM/CDK CD** (remove or disable the deploy workflow) so
|
||||||
(`<stack>-<env>`). Apply method **Manual**; automatic speculative plans on
|
HCP Terraform becomes the sole deploy path before the first apply. Leave
|
||||||
if VCS-connected (CLI `terraform plan` runs are inherently speculative).
|
the source-account stack frozen until cutover.
|
||||||
2. PR to this repo appending `hcptf-<stack>-plan` (read-only —
|
1. **Secrets first.** Create exact secret shells in the target account; strip
|
||||||
`arn:aws:iam::aws:policy/job-function/ViewOnlyAccess`, never
|
trailing newlines/whitespace before `put-secret-value` (a trailing `\n`
|
||||||
`ReadOnlyAccess`, which grants `secretsmanager:GetSecretValue`,
|
breaks HTTP headers at runtime). Capture ARNs. Never put secret *values*
|
||||||
`s3:GetObject` and `kms:Decrypt` and would let any PR-triggered speculative
|
in Terraform state (ARN references only).
|
||||||
plan render secret values into HCP run output; **no** IAM writes, **no**
|
2. **HCP workspace** in the target account's project (`<stack>-<env>`). Apply
|
||||||
guardrail-policy attach)
|
method **Manual**; automatic speculative plans on if VCS-connected;
|
||||||
and `hcptf-<stack>` (attaches `seahaven-hcptf-iam-management` + stack-scoped
|
working directory `terraform/`. (CLI `terraform plan` runs are inherently
|
||||||
service statements) to the substrate template. Trust: this account's
|
speculative.)
|
||||||
`app.terraform.io` provider; `StringEquals` on
|
3. **Substrate PR** to this repo appending `hcptf-<stack>-plan` and
|
||||||
`app.terraform.io:aud` = `aws.workload.identity` and on
|
`hcptf-<stack>` (see 3a/3b). Trust: this account's `app.terraform.io`
|
||||||
`app.terraform.io:sub` =
|
provider; `StringEquals` on `app.terraform.io:aud` =
|
||||||
|
`aws.workload.identity` and on `app.terraform.io:sub` =
|
||||||
`organization:seahaven:project:seahaven-<env>:workspace:<workspace>:run_phase:plan`
|
`organization:seahaven:project:seahaven-<env>:workspace:<workspace>:run_phase:plan`
|
||||||
(or `:apply`). Exact `StringEquals` only — never `StringLike`, never a
|
(or `:apply`). Exact `StringEquals` only — never `StringLike`, never a
|
||||||
wildcarded `run_phase` (a speculative PR plan must never hold write
|
wildcarded `run_phase` (a speculative PR plan must never hold write
|
||||||
credentials). **If the stack creates Lambda execution roles, this same PR
|
credentials). **If the stack creates Lambda execution roles, this same PR
|
||||||
must also widen `seahaven-lambda-execution-boundary`** per the WIDENING
|
must also widen `seahaven-lambda-execution-boundary`** per the WIDENING
|
||||||
PATH in `lib/deploy-substrate/deploy-substrate.template.yaml`: the
|
PATH in `lib/deploy-substrate/deploy-substrate.template.yaml` with the
|
||||||
|
**exact** secret ARNs from step 1 (no `secret:afi-*` patterns): the
|
||||||
guardrail forces every Terraform-created role to carry that boundary, and
|
guardrail forces every Terraform-created role to carry that boundary, and
|
||||||
it is a fleet-wide floor with zero data-plane permissions until widened —
|
it is a fleet-wide floor with zero data-plane permissions until widened —
|
||||||
an unwidened migration deploys green, then every data-plane call is denied
|
an unwidened migration deploys green, then every data-plane call is denied
|
||||||
at first invoke and async/DLQ writes are discarded silently. IAM roles and
|
at first invoke and async/DLQ writes are discarded silently. IAM roles and
|
||||||
boundary widenings = mandatory cross-family review +
|
boundary widenings = mandatory cross-family review +
|
||||||
`/sh-security-review` on the diff.
|
`/sh-security-review` on the diff.
|
||||||
3. After deploy, verify: both roles exist; `hcptf-<stack>` lists
|
|
||||||
`seahaven-hcptf-iam-management` in `list-attached-role-policies`; trust
|
3a. **Plan role (required for every stack):** attach
|
||||||
subs match the live org/project/workspace names byte-for-byte; simulate
|
`arn:aws:iam::aws:policy/job-function/ViewOnlyAccess` (never
|
||||||
the apply role against a `hcptf-*` ARN (expect `explicitDeny` from
|
`ReadOnlyAccess`, which grants `secretsmanager:GetSecretValue`,
|
||||||
`DenySelfMutation`) and against a normal stack role name (expect
|
`s3:GetObject` and `kms:Decrypt` and would let any PR-triggered speculative
|
||||||
`allowed`); and if step 2 widened the boundary, confirm the deployed
|
plan render secret values into HCP run output) **plus** a scoped
|
||||||
default version carries the stack's data-plane statements
|
plan-refresh sidecar inline policy. ViewOnly alone is insufficient for
|
||||||
(`aws iam get-policy-version`) — role verification alone never checks
|
Terraform refresh after partial apply — it lacks `iam:GetRole`,
|
||||||
boundary content.
|
`events:DescribeRule`, and several Lambda/S3 reads. Sidecar minimum:
|
||||||
4. Set **workspace-level** variables `TFC_AWS_PLAN_ROLE_ARN` +
|
`iam:GetRole` / related reads on `role/tf-managed/<prefix>-*`;
|
||||||
|
`events:DescribeRule` (and list-targets/tags as needed) on
|
||||||
|
`rule/<prefix>-*`; `lambda:*` (or at least the Get*/List* the provider
|
||||||
|
uses) on `function:<prefix>-*` / `layer:<prefix>-*`; `s3:Get*` /
|
||||||
|
`s3:ListBucket` on the stack artifact bucket. **No** IAM writes, **no**
|
||||||
|
guardrail-policy attach on the plan role. Copy
|
||||||
|
`afi-backup-monitor-plan-refresh` on `hcptf-afi-backup-monitor-plan`.
|
||||||
|
|
||||||
|
3b. **Apply role (Lambda/EventBridge stacks):** attach
|
||||||
|
`seahaven-hcptf-iam-management` plus stack-scoped service statements.
|
||||||
|
Prefer prefix-scoped `lambda:*` on `function:<prefix>-*` /
|
||||||
|
`layer:<prefix>-*`, `events:*` on `rule/<prefix>-*`, and bucket-scoped
|
||||||
|
`s3:*` on the artifact bucket — do **not** enumerate individual provider
|
||||||
|
Get* APIs (`GetFunctionCodeSigningConfig`, `GetBucketAcl`, …); that list
|
||||||
|
lags and fails first apply. Keep list/describe-on-`*` only where the
|
||||||
|
service requires it (e.g. `lambda:ListFunctions`). Copy
|
||||||
|
`afi-backup-monitor-services` on `hcptf-afi-backup-monitor`.
|
||||||
|
4. **Deploy substrate** to `UPDATE_COMPLETE`. Verify: both roles exist;
|
||||||
|
`hcptf-<stack>` lists `seahaven-hcptf-iam-management` in
|
||||||
|
`list-attached-role-policies`; trust subs match the live
|
||||||
|
org/project/workspace names byte-for-byte; simulate the apply role against
|
||||||
|
a `hcptf-*` ARN (expect `explicitDeny` from `DenySelfMutation`) and against
|
||||||
|
a normal stack role name (expect `allowed`); and if step 3 widened the
|
||||||
|
boundary, confirm the deployed default version carries the stack's
|
||||||
|
data-plane statements (`aws iam get-policy-version`) — role verification
|
||||||
|
alone never checks boundary content. Mechanical template↔deployed policy
|
||||||
|
reconcile as for other substrate policies.
|
||||||
|
5. Set **workspace-level** variables `TFC_AWS_PLAN_ROLE_ARN` +
|
||||||
`TFC_AWS_APPLY_ROLE_ARN` (category env) to the verified role ARNs, plus
|
`TFC_AWS_APPLY_ROLE_ARN` (category env) to the verified role ARNs, plus
|
||||||
`TFC_AWS_PROVIDER_AUTH=true`. Never project-scoped variable sets — the
|
`TFC_AWS_PROVIDER_AUTH=true`. Never project-scoped variable sets — the
|
||||||
trust is pinned per workspace, so a shared set breaks every other
|
trust is pinned per workspace, so a shared set breaks every other
|
||||||
workspace.
|
workspace. Auto-apply stays OFF until the stack is sealed.
|
||||||
5. Auto-apply stays OFF until the stack is sealed.
|
6. **App Terraform PR:** every `aws_iam_role` sets `path = "/tf-managed/"` and
|
||||||
|
the boundary; package Lambda/layer zips via an account artifact S3 bucket
|
||||||
|
and `aws_s3_object` `content_base64` (HCP plan and apply run on separate
|
||||||
|
workers and do not share local `archive_file` paths — see
|
||||||
|
`afi-backup-monitor/terraform/artifacts.tf`); functions `depends_on` their
|
||||||
|
IAM policies before create; commit `.terraform.lock.hcl` with
|
||||||
|
multi-platform hashes.
|
||||||
|
7. **First Manual apply** from the HCP workspace (not local apply against
|
||||||
|
prod). Tolerate partial state on permission misses; widen the apply/plan
|
||||||
|
roles and retry. Confirm all expected resources exist in the target
|
||||||
|
account.
|
||||||
|
8. **Live-path proof:** real invoke of every critical function must hit real
|
||||||
|
external APIs / Slack (not synth or simulate alone) before cutover.
|
||||||
|
9. **Cutover + decommission:** disable source-account schedules (e.g.
|
||||||
|
EventBridge rules); observe a clean prod path; delete the source
|
||||||
|
CloudFormation/CDK stack per the decommission playbook; sweep or retain
|
||||||
|
log groups deliberately; delete source secrets last.
|
||||||
|
10. **Docs:** update Confluence AWS Architecture Map and the stack ops page;
|
||||||
|
promote durable gotchas to the convention ledger when they are general.
|
||||||
|
|
||||||
**HCP-side authority is AWS authority.** AWS exposes only `aud`, `sub` and
|
**HCP-side authority is AWS authority.** AWS exposes only `aud`, `sub` and
|
||||||
`amr` as trust-policy condition keys for a generic OIDC provider — HCP's
|
`amr` as trust-policy condition keys for a generic OIDC provider — HCP's
|
||||||
|
|
|
||||||
|
|
@ -373,16 +373,30 @@ Resources:
|
||||||
"iam:PassedToService": "lambda.amazonaws.com"
|
"iam:PassedToService": "lambda.amazonaws.com"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Per-workspace roles: afi-backup-monitor-prod (PLAT-56)
|
# Per-workspace role pattern (required for every future hcptf-* append)
|
||||||
|
# and first workload: afi-backup-monitor-prod (PLAT-56).
|
||||||
#
|
#
|
||||||
# First HCP Terraform workload. Trust subs are exact StringEquals on
|
# Copy this shape — do not invent enumerated Get* allow-lists.
|
||||||
# organization/project/workspace/run_phase — never StringLike, never a
|
#
|
||||||
# wildcarded run_phase. Plan role: ViewOnlyAccess (never ReadOnlyAccess,
|
# Plan role (every stack):
|
||||||
# which grants secretsmanager:GetSecretValue) PLUS a stack-scoped refresh
|
# - Managed: ViewOnlyAccess (never ReadOnlyAccess — it grants
|
||||||
# inline policy — ViewOnlyAccess omits iam:GetRole and events:DescribeRule,
|
# secretsmanager:GetSecretValue / s3:GetObject / kms:Decrypt to
|
||||||
# which Terraform needs to refresh state after the first apply. Apply role:
|
# speculative PR plans).
|
||||||
# attaches the shared guardrail plus stack-scoped Lambda / layer /
|
# - PLUS a stack-scoped plan-refresh sidecar. ViewOnly alone omits
|
||||||
# EventBridge / Logs / artifact-bucket. Prod-only (IsProdAccount).
|
# iam:GetRole, events:DescribeRule, and provider Lambda/S3 reads
|
||||||
|
# needed after a partial first apply.
|
||||||
|
#
|
||||||
|
# Apply role (Lambda / EventBridge stacks):
|
||||||
|
# - Attach seahaven-hcptf-iam-management.
|
||||||
|
# - Service grants: prefix-scoped lambda:* on function:<prefix>-* and
|
||||||
|
# layer:<prefix>-*, events:* on rule/<prefix>-*, and bucket-scoped
|
||||||
|
# s3:* on the stack artifact bucket. Enumerating provider Get*
|
||||||
|
# (GetFunctionCodeSigningConfig, GetBucketAcl, …) lags and fails
|
||||||
|
# first apply (PLAT-56).
|
||||||
|
#
|
||||||
|
# Trust: exact StringEquals on organization/project/workspace/run_phase —
|
||||||
|
# never StringLike, never a wildcarded run_phase. Prod-only for this
|
||||||
|
# pair (IsProdAccount). See README "Migration checklist".
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
HcptfAfiBackupMonitorPlanRole:
|
HcptfAfiBackupMonitorPlanRole:
|
||||||
Type: AWS::IAM::Role
|
Type: AWS::IAM::Role
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue