From 5ed57abfd7f5b13a1779351ae968b684213f98e0 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 2 Sep 2026 11:19:04 -0400 Subject: [PATCH] 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. --- README.md | 67 ++++++++----- .../hcptf-workspace-iam/hcp_iam.tf.example | 42 ++++++-- lib/hcptf-bootstrap/apply-policy.json.tmpl | 14 ++- .../scoped-apply-iam.json.tmpl | 16 +++- scripts/create-hcptf-bootstrap-roles.sh | 96 ++++++++++++++++++- 5 files changed, 191 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b0426a0..09ea358 100644 --- a/README.md +++ b/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-: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-`:** trust pinned to that stack's workspace and `run_phase:apply`. No `seahaven-hcptf-iam-management`. May - manage `role/tf-managed/-*` 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/-*` with `iam:PermissionsBoundary` + `StringLike` this stack's `policy/tf-managed/*`, the shared + `seahaven-lambda-execution-boundary`, or + `seahaven-lambda-execution-boundary-`. May not `CreatePolicy` / + `CreatePolicyVersion`. May not `UpdateAssumeRolePolicy`, `DeleteRole`, or + `PutRolePolicy` on `hcptf-*` (including itself). Need more apply-role + permission later: `--allow-workspace -` 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 `-` 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 -` (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-`: 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-` in deploy-substrate for this pass. diff --git a/examples/hcptf-workspace-iam/hcp_iam.tf.example b/examples/hcptf-workspace-iam/hcp_iam.tf.example index 61a943f..614ad42 100644 --- a/examples/hcptf-workspace-iam/hcp_iam.tf.example +++ b/examples/hcptf-workspace-iam/hcp_iam.tf.example @@ -1,7 +1,25 @@ # Example: import an existing prod/dev hcptf- 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 { diff --git a/lib/hcptf-bootstrap/apply-policy.json.tmpl b/lib/hcptf-bootstrap/apply-policy.json.tmpl index 121baa7..ce64504 100644 --- a/lib/hcptf-bootstrap/apply-policy.json.tmpl +++ b/lib/hcptf-bootstrap/apply-policy.json.tmpl @@ -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*" + ] } } }, diff --git a/lib/hcptf-bootstrap/scoped-apply-iam.json.tmpl b/lib/hcptf-bootstrap/scoped-apply-iam.json.tmpl index 487eb7a..284903f 100644 --- a/lib/hcptf-bootstrap/scoped-apply-iam.json.tmpl +++ b/lib/hcptf-bootstrap/scoped-apply-iam.json.tmpl @@ -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__" + ] } } }, diff --git a/scripts/create-hcptf-bootstrap-roles.sh b/scripts/create-hcptf-bootstrap-roles.sh index 0dcb465..09b4cbf 100755 --- a/scripts/create-hcptf-bootstrap-roles.sh +++ b/scripts/create-hcptf-bootstrap-roles.sh @@ -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-, 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"