mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-07 16:08:55 +00:00
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.
This commit is contained in:
parent
26f6ab879f
commit
5ed57abfd7
5 changed files with 191 additions and 44 deletions
67
README.md
67
README.md
|
|
@ -475,48 +475,62 @@ collapses the plan/apply role split. Never project-scoped variable sets.
|
|||
- **General apply role `hcptf-bootstrap`:** trust pinned to
|
||||
`organization:seahaven:project:seahaven-<env>:workspace:iam-bootstrap:run_phase:apply`
|
||||
(exact `StringEquals` on `aud` and `sub`; never `StringLike` on
|
||||
`run_phase`). IAM writes on `role/tf-managed/*` and `policy/tf-managed/*`.
|
||||
`CreatePolicy` / `CreatePolicyVersion` only here. Must set a permissions
|
||||
boundary on every `tf-managed` role it creates (`Null` false). Must not
|
||||
mutate `githubdeploy-*`, `github-cfn-execution-role`, `cdk-hnb659fds-*`,
|
||||
`run_phase` or workspace). IAM writes on `role/tf-managed/*` and
|
||||
`policy/tf-managed/*`. `CreatePolicy` / `CreatePolicyVersion` only here.
|
||||
`CreateRole` / `PutRolePolicy` / `AttachRolePolicy` /
|
||||
`PutRolePermissionsBoundary` on `tf-managed` roles require
|
||||
`iam:PermissionsBoundary` `StringLike` `policy/tf-managed/*` or
|
||||
`policy/seahaven-lambda-execution-boundary*`. `Null` false is not enough:
|
||||
it would accept `AdministratorAccess` as the ceiling. Must not mutate
|
||||
`githubdeploy-*`, `github-cfn-execution-role`, `cdk-hnb659fds-*`,
|
||||
`OrganizationAccountAccessRole`, `seahaven-*`. SCP
|
||||
`protect-privileged-roles` covers `hcptf-bootstrap*` in prod and nonprod.
|
||||
Manual apply. Not in external-dev.
|
||||
`protect-privileged-roles` covers `hcptf-bootstrap*` in prod and nonprod
|
||||
(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.
|
||||
- **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 a boundary required. May not
|
||||
`CreatePolicy` / `CreatePolicyVersion`. May not `UpdateAssumeRolePolicy`,
|
||||
`DeleteRole`, or `PutRolePolicy` on `hcptf-*` (including itself). Need more
|
||||
apply-role permission later: switch the workspace back to `hcptf-bootstrap`,
|
||||
apply, switch back.
|
||||
manage `role/tf-managed/<prefix>-*` with `iam:PermissionsBoundary`
|
||||
`StringLike` this stack's `policy/tf-managed/<prefix>*`, the shared
|
||||
`seahaven-lambda-execution-boundary`, or
|
||||
`seahaven-lambda-execution-boundary-<stack>`. May not `CreatePolicy` /
|
||||
`CreatePolicyVersion`. May not `UpdateAssumeRolePolicy`, `DeleteRole`, or
|
||||
`PutRolePolicy` on `hcptf-*` (including itself). Need more apply-role
|
||||
permission later: `--allow-workspace <stack>-<env>` on the create script,
|
||||
point that workspace's `TFC_AWS_*` at `hcptf-bootstrap`, apply, retarget
|
||||
vars, re-run the script with no extra workspace. Do not leave a stack
|
||||
workspace on bootstrap trust.
|
||||
- **Scoped plan role:** `ViewOnlyAccess` plus a scoped refresh sidecar.
|
||||
|
||||
This does not re-open PLAT-52 AC1. On the HCP path, `CreatePolicy` lives only
|
||||
on `hcptf-bootstrap`, which is not `githubdeploy-seahaven-org-baseline`. Do
|
||||
not add `ArnLike` to the SAM guardrail in deploy-substrate.
|
||||
on `hcptf-bootstrap`, which is not `githubdeploy-seahaven-org-baseline`.
|
||||
Prefix matching is `StringLike` on the bootstrap/scoped HCP documents, not
|
||||
on the SAM guardrail. Do not add `ArnLike` to deploy-substrate.
|
||||
|
||||
**First-apply runbook (new prod/dev HCP stack):**
|
||||
|
||||
1. Freeze the app's SAM/CDK CD. Secrets first (ARN references only in state).
|
||||
2. HCP workspace `<stack>-<env>` exists. Auto-apply off. Vars still empty.
|
||||
3. Point `TFC_AWS_APPLY_ROLE_ARN` / `TFC_AWS_PLAN_ROLE_ARN` at the general
|
||||
pair, or run IAM from workspace `iam-bootstrap`. Never a project-scoped
|
||||
variable set.
|
||||
4. App PR adds `aws_iam_role` plan/apply (trust exact `StringEquals`), a
|
||||
IAM lives in this workspace's state, so this workspace must assume
|
||||
`hcptf-bootstrap` for the first apply. Default bootstrap trust does not
|
||||
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`.
|
||||
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
|
||||
boundary at `path = "/tf-managed/"`, and exec roles with that boundary.
|
||||
Copy `examples/hcptf-workspace-iam/hcp_iam.tf.example`. Cross-family
|
||||
review + `/sh-security-review` still apply to IAM in the app PR. There is
|
||||
no org-baseline IAM PR for the new stack.
|
||||
5. One Manual apply. Roles and boundary exist (tolerate partial state on
|
||||
6. One Manual apply. Roles and boundary exist (tolerate partial state on
|
||||
non-IAM resources).
|
||||
6. Switch workspace vars to the new scoped ARNs. Remove this workspace from
|
||||
general-role trust if it was ever listed (dedicated `iam-bootstrap`
|
||||
workspace is the pick, so this step is usually a no-op).
|
||||
7. Second Manual apply with the scoped role. Prove the stack. Then seal.
|
||||
8. Live-path proof, cutover, docs as before.
|
||||
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.
|
||||
8. Second Manual apply with the scoped role. Prove the stack. Then seal.
|
||||
9. Live-path proof, cutover, docs as before.
|
||||
|
||||
Plan role: `ViewOnlyAccess` (never `ReadOnlyAccess`) plus a scoped
|
||||
plan-refresh sidecar. Apply role: scoped IAM statements from
|
||||
|
|
@ -537,7 +551,12 @@ remaining SAM / unmigrated stacks.
|
|||
Each PR adds `aws_iam_role` / inline policy resources matching live names,
|
||||
`terraform import` (see `examples/hcptf-workspace-iam/hcp_iam.tf.example`),
|
||||
and drops the attached `seahaven-hcptf-iam-management` once the scoped
|
||||
statements live on the role. Lambda `permissions_boundary` may keep pointing
|
||||
statements live on the role. Role names stay the same. The import apply
|
||||
cannot run as `hcptf-<stack>`: live DenySelfMutation denies
|
||||
`DetachRolePolicy` / `PutRolePolicy` on `hcptf-*`. Use the same
|
||||
`--allow-workspace` window as first-apply, point `TFC_AWS_*` at bootstrap
|
||||
for that one Manual apply, then retarget the original scoped ARNs and
|
||||
revoke the extra trust. Lambda `permissions_boundary` may keep pointing
|
||||
at `seahaven-lambda-execution-boundary-<stack>` in deploy-substrate for this
|
||||
pass.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,25 @@
|
|||
# Example: import an existing prod/dev hcptf-<stack> pair into app Terraform
|
||||
# (PLAT-146). Copy into the consumer repo's terraform/ directory. Replace
|
||||
# locals, then `terraform import` (or keep the import blocks) on a Manual
|
||||
# apply. Do not recreate the role. Do not rotate TFC_AWS_*_ROLE_ARN.
|
||||
# apply. Do not recreate the role. Role names stay `hcptf-STACK` /
|
||||
# `hcptf-STACK-plan`.
|
||||
#
|
||||
# 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:
|
||||
# 1. scripts/create-hcptf-bootstrap-roles.sh --account prod|dev \
|
||||
# --allow-workspace STACK-prod
|
||||
# 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap /
|
||||
# hcptf-bootstrap-plan (workspace vars, never a project set).
|
||||
# 3. One Manual apply (import + detach seahaven-hcptf-iam-management +
|
||||
# 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.
|
||||
# Later apply-role IAM edits use the same window. Do not add StringLike
|
||||
# on bootstrap trust.
|
||||
#
|
||||
# SAM-only repos and SHOC/external-dev do not use this file.
|
||||
|
||||
|
|
@ -11,6 +29,7 @@ locals {
|
|||
hcp_workspace = "STACK-prod"
|
||||
apply_role = "hcptf-STACK"
|
||||
plan_role = "hcptf-STACK-plan"
|
||||
stack_name = "STACK"
|
||||
stack_prefix = "STACK-"
|
||||
}
|
||||
|
||||
|
|
@ -61,8 +80,9 @@ data "aws_iam_policy_document" "hcptf_plan_trust" {
|
|||
}
|
||||
|
||||
# Rendered from lib/hcptf-bootstrap/scoped-apply-iam.json.tmpl. CreatePolicy
|
||||
# stays on hcptf-bootstrap only. Exec-role writes are prefix-scoped. The role
|
||||
# cannot PutRolePolicy on hcptf-* (including itself).
|
||||
# stays on hcptf-bootstrap only. Exec-role writes are prefix-scoped. Boundary
|
||||
# ARNs are StringLike-pinned (not Null); AdministratorAccess is not accepted.
|
||||
# The role cannot PutRolePolicy on hcptf-* (including itself).
|
||||
data "aws_iam_policy_document" "hcptf_scoped_iam" {
|
||||
statement {
|
||||
sid = "DenyCreatePolicy"
|
||||
|
|
@ -76,9 +96,13 @@ data "aws_iam_policy_document" "hcptf_scoped_iam" {
|
|||
actions = ["iam:CreateRole"]
|
||||
resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/${local.stack_prefix}*"]
|
||||
condition {
|
||||
test = "Null"
|
||||
test = "StringLike"
|
||||
variable = "iam:PermissionsBoundary"
|
||||
values = ["false"]
|
||||
values = [
|
||||
"arn:aws:iam::${local.account_id}:policy/tf-managed/${local.stack_prefix}*",
|
||||
"arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary",
|
||||
"arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-${local.stack_name}",
|
||||
]
|
||||
}
|
||||
}
|
||||
statement {
|
||||
|
|
@ -91,9 +115,13 @@ data "aws_iam_policy_document" "hcptf_scoped_iam" {
|
|||
]
|
||||
resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/${local.stack_prefix}*"]
|
||||
condition {
|
||||
test = "Null"
|
||||
test = "StringLike"
|
||||
variable = "iam:PermissionsBoundary"
|
||||
values = ["false"]
|
||||
values = [
|
||||
"arn:aws:iam::${local.account_id}:policy/tf-managed/${local.stack_prefix}*",
|
||||
"arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary",
|
||||
"arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-${local.stack_name}",
|
||||
]
|
||||
}
|
||||
}
|
||||
statement {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@
|
|||
"Action": "iam:CreateRole",
|
||||
"Resource": "arn:aws:iam::__ACCOUNT_ID__:role/tf-managed/*",
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"iam:PermissionsBoundary": "false"
|
||||
"StringLike": {
|
||||
"iam:PermissionsBoundary": [
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/tf-managed/*",
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/seahaven-lambda-execution-boundary*"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -42,8 +45,11 @@
|
|||
],
|
||||
"Resource": "arn:aws:iam::__ACCOUNT_ID__:role/tf-managed/*",
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"iam:PermissionsBoundary": "false"
|
||||
"StringLike": {
|
||||
"iam:PermissionsBoundary": [
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/tf-managed/*",
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/seahaven-lambda-execution-boundary*"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,8 +16,12 @@
|
|||
"Action": "iam:CreateRole",
|
||||
"Resource": "arn:aws:iam::__ACCOUNT_ID__:role/tf-managed/__STACK_PREFIX__*",
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"iam:PermissionsBoundary": "false"
|
||||
"StringLike": {
|
||||
"iam:PermissionsBoundary": [
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/tf-managed/__STACK_PREFIX__*",
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/seahaven-lambda-execution-boundary",
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/seahaven-lambda-execution-boundary-__STACK_NAME__"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -31,8 +35,12 @@
|
|||
],
|
||||
"Resource": "arn:aws:iam::__ACCOUNT_ID__:role/tf-managed/__STACK_PREFIX__*",
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"iam:PermissionsBoundary": "false"
|
||||
"StringLike": {
|
||||
"iam:PermissionsBoundary": [
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/tf-managed/__STACK_PREFIX__*",
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/seahaven-lambda-execution-boundary",
|
||||
"arn:aws:iam::__ACCOUNT_ID__:policy/seahaven-lambda-execution-boundary-__STACK_NAME__"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,10 +13,17 @@
|
|||
# Usage:
|
||||
# scripts/create-hcptf-bootstrap-roles.sh --account prod|dev [--dry-run]
|
||||
# scripts/create-hcptf-bootstrap-roles.sh --account prod --simulate
|
||||
# scripts/create-hcptf-bootstrap-roles.sh --account prod --allow-workspace STACK-prod
|
||||
#
|
||||
# --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
|
||||
# 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).
|
||||
#
|
||||
|
|
@ -28,12 +35,14 @@ TMPL="$ROOT/lib/hcptf-bootstrap"
|
|||
ACCOUNT_KEY=""
|
||||
DRY_RUN=0
|
||||
SIMULATE=0
|
||||
ALLOW_WORKSPACE=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--account) ACCOUNT_KEY="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--simulate) SIMULATE=1; shift ;;
|
||||
-h|--help) sed -n '2,28p' "$0"; exit 0 ;;
|
||||
--allow-workspace) ALLOW_WORKSPACE="$2"; shift 2 ;;
|
||||
-h|--help) sed -n '2,36p' "$0"; exit 0 ;;
|
||||
-*) echo "unknown flag: $1" >&2; exit 2 ;;
|
||||
*) echo "unexpected argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
|
|
@ -49,17 +58,52 @@ case "$ACCOUNT_KEY" in
|
|||
HCP_PROJECT="seahaven-dev"
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 --account prod|dev [--dry-run] [--simulate]" >&2
|
||||
echo "usage: $0 --account prod|dev [--dry-run] [--simulate] [--allow-workspace NAME]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -n "$ALLOW_WORKSPACE" ]]; then
|
||||
if [[ "$ALLOW_WORKSPACE" == "iam-bootstrap" ]]; then
|
||||
echo "--allow-workspace iam-bootstrap 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
|
||||
echo "invalid --allow-workspace '$ALLOW_WORKSPACE' (lowercase kebab, no wildcards)" >&2
|
||||
exit 2
|
||||
fi
|
||||
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"
|
||||
}
|
||||
|
||||
# Render a trust template. Optional extra workspace becomes a second exact
|
||||
# StringEquals sub (OR). Does not use StringLike.
|
||||
render_trust() {
|
||||
local src="$1"
|
||||
local dest="$2"
|
||||
python3 - "$src" "$dest" "$ACCOUNT_ID" "$HCP_PROJECT" "$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)
|
||||
data = json.loads(text)
|
||||
if extra:
|
||||
cond = data["Statement"][0]["Condition"]["StringEquals"]
|
||||
sub = cond["app.terraform.io:sub"]
|
||||
if isinstance(sub, str):
|
||||
sub = [sub]
|
||||
phase = "apply" if ":run_phase:apply" in sub[0] else "plan"
|
||||
added = f"organization:seahaven:project:{project}:workspace:{extra}:run_phase:{phase}"
|
||||
if added not in sub:
|
||||
sub.append(added)
|
||||
cond["app.terraform.io:sub"] = sub
|
||||
pathlib.Path(dest).write_text(json.dumps(data, indent=2) + "\n")
|
||||
PY
|
||||
}
|
||||
|
||||
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-}"
|
||||
|
|
@ -92,6 +136,7 @@ echo "caller: $(aws sts get-caller-identity --query Arn --output text)"
|
|||
if [[ "$SIMULATE" -eq 1 ]]; then
|
||||
APPLY_ARN="arn:aws:iam::${ACCOUNT_ID}:role/hcptf-bootstrap"
|
||||
echo "== simulate ${APPLY_ARN} =="
|
||||
echo "-- CreateRole with tf-managed boundary (expect allowed) --"
|
||||
aws iam simulate-principal-policy \
|
||||
--policy-source-arn "$APPLY_ARN" \
|
||||
--action-names iam:CreateRole \
|
||||
|
|
@ -99,12 +144,28 @@ if [[ "$SIMULATE" -eq 1 ]]; then
|
|||
--context-entries 'ContextKeyName=iam:PermissionsBoundary,ContextKeyValues=arn:aws:iam::'"${ACCOUNT_ID}"':policy/tf-managed/example,ContextKeyType=string' \
|
||||
--query 'EvaluationResults[].{Action:EvalActionName,Decision:EvalDecision}' \
|
||||
--output table
|
||||
echo "-- CreateRole with AdministratorAccess boundary (expect implicitDeny) --"
|
||||
aws iam simulate-principal-policy \
|
||||
--policy-source-arn "$APPLY_ARN" \
|
||||
--action-names iam:CreateRole \
|
||||
--resource-arns "arn:aws:iam::${ACCOUNT_ID}:role/tf-managed/example" \
|
||||
--context-entries 'ContextKeyName=iam:PermissionsBoundary,ContextKeyValues=arn:aws:iam::aws:policy/AdministratorAccess,ContextKeyType=string' \
|
||||
--query 'EvaluationResults[].{Action:EvalActionName,Decision:EvalDecision}' \
|
||||
--output table
|
||||
echo "-- CreatePolicy on tf-managed (expect allowed) --"
|
||||
aws iam simulate-principal-policy \
|
||||
--policy-source-arn "$APPLY_ARN" \
|
||||
--action-names iam:CreatePolicy \
|
||||
--resource-arns "arn:aws:iam::${ACCOUNT_ID}:policy/tf-managed/example" \
|
||||
--query 'EvaluationResults[].{Action:EvalActionName,Decision:EvalDecision}' \
|
||||
--output table
|
||||
echo "-- PutRolePolicy on hcptf-* (expect allowed; import/first-apply path) --"
|
||||
aws iam simulate-principal-policy \
|
||||
--policy-source-arn "$APPLY_ARN" \
|
||||
--action-names iam:PutRolePolicy iam:DetachRolePolicy \
|
||||
--resource-arns "arn:aws:iam::${ACCOUNT_ID}:role/hcptf-example" \
|
||||
--query 'EvaluationResults[].{Action:EvalActionName,Decision:EvalDecision}' \
|
||||
--output table
|
||||
aws iam simulate-principal-policy \
|
||||
--policy-source-arn "$APPLY_ARN" \
|
||||
--action-names iam:CreateRole iam:PutRolePolicy iam:DeleteRole \
|
||||
|
|
@ -124,16 +185,37 @@ cleanup() {
|
|||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
render_to "$TMPL/trust-apply.json.tmpl" "$WORKDIR/trust-apply.json"
|
||||
render_to "$TMPL/trust-plan.json.tmpl" "$WORKDIR/trust-plan.json"
|
||||
render_trust "$TMPL/trust-apply.json.tmpl" "$WORKDIR/trust-apply.json"
|
||||
render_trust "$TMPL/trust-plan.json.tmpl" "$WORKDIR/trust-plan.json"
|
||||
render_to "$TMPL/apply-policy.json.tmpl" "$WORKDIR/apply-policy.json"
|
||||
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"
|
||||
fi
|
||||
|
||||
python3 - "$WORKDIR" <<'PY'
|
||||
import json, pathlib, sys
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
for p in root.glob("*.json"):
|
||||
json.loads(p.read_text())
|
||||
data = json.loads(p.read_text())
|
||||
dump = json.dumps(data)
|
||||
if p.name.startswith("trust-"):
|
||||
if "StringLike" in dump:
|
||||
raise SystemExit(f"{p.name}: trust must stay StringEquals")
|
||||
subs = data["Statement"][0]["Condition"]["StringEquals"]["app.terraform.io:sub"]
|
||||
if isinstance(subs, str):
|
||||
subs = [subs]
|
||||
for s in subs:
|
||||
if "*" in s or "?" in s:
|
||||
raise SystemExit(f"{p.name}: wildcard in sub {s}")
|
||||
if p.name == "apply-policy.json":
|
||||
if '"Null"' in dump:
|
||||
raise SystemExit("apply-policy must not use Null on PermissionsBoundary")
|
||||
if "AdministratorAccess" in dump:
|
||||
raise SystemExit("apply-policy must not name AdministratorAccess")
|
||||
PY
|
||||
|
||||
create_or_update_role() {
|
||||
|
|
@ -186,4 +268,8 @@ echo " hcptf-bootstrap-plan: attached ViewOnlyAccess"
|
|||
echo "done. Next: HCP workspace iam-bootstrap 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
|
||||
echo "First-apply/import window: point workspace ${ALLOW_WORKSPACE} TFC_AWS_* at the pair above,"
|
||||
echo " apply, retarget scoped ARNs, then re-run this script with no --allow-workspace."
|
||||
fi
|
||||
echo "Then: $0 --account ${ACCOUNT_KEY} --simulate"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue