OceanSoft IAM Identity Center — From-Scratch Enablement
Scope: TI-2a — minimum clickops fast-track. Every
terraform applystep is marked[HITL]. Agents prepare; HITL executes all mutations. Read fully before opening the AWS console.Minimum clickops: 1 Enable-SSO click + 1 S3 bucket. Everything else is Terraform.
This guide is the fast-track implementation of the reusable terraform-aws/modules/sso module.
The 1st pilot consumer is b2b-commerce/infra/terraform/aws/identity/ (profile-only, ADR-021) —
all root-level paths below refer to that pilot root. The generic public reference root is
terraform-aws/accounts/management-account/.
Section 1 — Sign In / Create the Management Account
Only if no management account exists for [email protected]. If an account already exists,
skip to Section 2.
- Go to https://aws.amazon.com → Create an AWS Account.
- Use email
[email protected](or a sub-addressed variant such as[email protected]to preserve the human inbox — the email must be globally unique across all of AWS). - Complete phone verification and billing details.
- Once the account is active, sign in to the AWS Management Console as the root user.
Branch check:
# READONLY verify — run after sign-in to confirm you are in the right account
aws sts get-caller-identity --profile <YOUR_MANAGEMENT_PROFILE>
# Expected: Account matches your management account ID; no cross-account confusion
If the account already exists and you are signed in, proceed directly to Section 2.
5W1H — Management Account
| Question | Answer |
|---|---|
| Why | IAM Identity Center must be enabled from a management account; a workload account cannot host an org-level instance. |
| What if missing | Cannot enable an organization instance of Identity Center; multi-account permission sets are unavailable. |
| Business value | A dedicated sso@ management account is the trust anchor for the entire OceanSoft landing zone. |
| Purpose | Establishes the root account from which the AWS Organization and Identity Center are governed. |
| Critical thinking | If an account already exists but you are unsure whether it is the management account, run aws organizations describe-organization — the MasterAccountId must match the account you are signed into. |
Section 2 — Enable IAM Identity Center [HITL — the one click]
As the HITL operator, I want to enable IAM Identity Center as an ORGANIZATION instance in my
chosen home region so that modules/sso can discover the instance via data.aws_ssoadmin_instances
and Terraform manages groups, permission sets, and assignments.
When the Identity Center console asks which instance type to create, you MUST choose "Enable with AWS Organizations" (the organization instance).
Do NOT choose the account-level "Enable" button if a separate organization option is shown. An account instance cannot:
- provision permission sets to member accounts
- resolve account names via
enable_organizations_lookup - scale to a multi-account landing zone
If you accidentally enable an account instance, you must delete it and re-enable — there is no migration path. Confirm the console shows organization instance before clicking Enable.
Enabling IAM Identity Center as an organization instance automatically creates an AWS Organization with all features enabled for your management account. You do NOT need to navigate to AWS Organizations and create the organization manually. Doing so first is redundant and adds unnecessary clickops with no correctness benefit.
The old pattern (create Organization → enable Identity Center) is wrong. The correct sequence is: Enable Identity Center (org instance) → Organization is created for you.
Step 2.1 — Choose your home region
The home region is permanent for the lifetime of the instance.
Recommendation: Use <HOME_REGION> (default: ap-southeast-2, the Sydney region, matching
var.sso_region in infra/terraform/aws/identity/variables.tf).
The home region must match the aws.identity_center provider alias in
infra/terraform/aws/identity/providers.tf.
Step 2.2 — Enable Identity Center [HITL]
- Sign in to the AWS Management Console with the management account administrator or root user.
- Navigate to IAM Identity Center (search "Identity Center" in the top bar).
- Click Enable with AWS Organizations — this is the organization instance option.
- Confirm the selected region matches your chosen
<HOME_REGION>. - Click Enable.
Post-enable verify:
# READONLY verify
aws sso-admin list-instances \
--profile <YOUR_MANAGEMENT_PROFILE> \
--region <HOME_REGION>
# Expected: Instances array with exactly 1 entry; IdentityStoreId = d-<YOUR_OWN_ID>
NEVER authenticate against these start URLs. They belong to foreign organizations.
| Instance ID | Foreign owner | Action |
|---|---|---|
d-9767913734 | Foreign org (bound to terraform-aws-sso profile) | BLOCKED |
d-976752e8d5 | Employer landing zone (READONLY-only, foreign) | BLOCKED |
If your aws sso-admin list-instances output contains either of these IDs, STOP.
You are querying the wrong account or the wrong region. Sign out and sign back in to
the correct management account before continuing.
Your OceanSoft start URL will be of the form:
https://d-<YOUR_OWN_ID>.awsapps.com/start
The instance ID in YOUR start URL will NOT match either value above.
5W1H — Identity Center Enablement
| Question | Answer |
|---|---|
| Why | IAM Identity Center is a free, org-level, single-pane-of-glass credential broker; enabling it once in the management account gives every member account access via permission-set assignments — no per-account configuration needed. |
| What if missing | data.aws_ssoadmin_instances in modules/sso fails at plan time; all downstream group/assignment resources cannot be created. |
| Business value | Eliminates per-account IAM user maintenance; all access is managed from one YAML file; audit trail is centralised in CloudTrail under the management account. |
| Purpose | Creates the one immutable anchor (the SSO instance ARN) that every Terraform resource in modules/sso references via a data source. |
| Critical thinking | You CANNOT move an Identity Center instance to another region after creation. If you select the wrong home region, you must delete the instance and lose all permission sets and assignments — there is no migration path. Confirm the region matches var.sso_region before clicking Enable. |
Section 3 — Bootstrap Credential
As the HITL operator, I want a short-lived credential to run the first terraform apply without
leaving a long-lived IAM or root access key in place.
Credential ranking
| Option | Method | Teardown required | Recommendation |
|---|---|---|---|
| A — AWS CloudShell | Open CloudShell in the management-account console | None — session credential expires automatically | PREFERRED |
| B — Temporary IAM user access key | Create an IAM user, generate an access key, delete post-bootstrap | Yes — delete the key and user after apply | ACCEPTABLE |
| C — Root access key | — | — | REJECT — see danger box |
Root access keys cannot be scoped to least-privilege — they carry full account power. AWS explicitly advises against creating them. Any root access key left in place is a standing CPS 234 (APRA) least-privilege finding and an immediate security incident.
If you are tempted to use a root access key for convenience, use CloudShell instead — it is ephemeral, IAM-role-backed, and requires zero credential management.
Option A — AWS CloudShell [PREFERRED]
- In the management-account console, click the CloudShell icon (top navigation bar).
- CloudShell opens a browser-based terminal with your console session credentials.
- Clone the repo or upload your Terraform files; proceed directly to Section 4.
No access key is created. No teardown step is needed. The credential expires with your console session.
Option B — Temporary IAM user access key [ACCEPTABLE]
# [HITL] — Create a bootstrap user with minimum permissions for the identity Terraform root
aws iam create-user --user-name tf-bootstrap-identity \
--profile <YOUR_MANAGEMENT_PROFILE>
aws iam attach-user-policy \
--user-name tf-bootstrap-identity \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
--profile <YOUR_MANAGEMENT_PROFILE>
aws iam create-access-key --user-name tf-bootstrap-identity \
--profile <YOUR_MANAGEMENT_PROFILE>
# Save AccessKeyId and SecretAccessKey — export as AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
Teardown (MANDATORY — run immediately after terraform apply in Section 5 succeeds):
# [HITL] — Delete the temporary credential; admin access via SSO replaces it
aws iam delete-access-key \
--user-name tf-bootstrap-identity \
--access-key-id <ACCESS_KEY_ID> \
--profile <YOUR_MANAGEMENT_PROFILE>
aws iam detach-user-policy \
--user-name tf-bootstrap-identity \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
--profile <YOUR_MANAGEMENT_PROFILE>
aws iam delete-user --user-name tf-bootstrap-identity \
--profile <YOUR_MANAGEMENT_PROFILE>
5W1H — Bootstrap Credential
| Question | Answer |
|---|---|
| Why | A credential is needed for the first terraform apply before SSO permission sets exist; CloudShell is the least-privilege option because it is ephemeral and IAM-role-backed. |
| What if missing | Cannot run terraform apply; no Terraform-managed resources can be created. |
| Business value | Ephemeral credentials leave zero standing access after bootstrap; CPS 234 least-privilege requirement is satisfied by design. |
| Purpose | Provides the one-time Terraform executor credential that is replaced by SSO-managed access after the first apply. |
| Critical thinking | CloudShell is preferred over a temporary IAM user because it requires zero key management; the session credential is tied to your console MFA and expires automatically. |
Section 4 — S3 State Bucket [HITL — the second and last clickops]
As the HITL operator, I want to create the S3 state bucket before running terraform init
so that the backend exists independently of the Terraform root that will use it — preventing
the SELF_REFERENTIAL_TFSTATE bootstrap deadlock.
infra/terraform/aws/identity/backend.tf uses bucket: ${ACCOUNT_ID}-tfstate-<HOME_REGION>
If you run terraform init before this bucket exists, Terraform fails to initialise the backend.
If you attempt to create the bucket WITH Terraform inside the same root that uses it, you create
a bootstrap deadlock — the state backend cannot be written because the bucket does not exist yet.
Always create the state bucket BEFORE running terraform init. Always.
Step 4.1 — Check whether the state bucket already exists
# READONLY verify
aws s3api head-bucket \
--bucket ${ACCOUNT_ID}-tfstate-<HOME_REGION> \
--profile <YOUR_MANAGEMENT_PROFILE> 2>&1
# 200 OK (no error) → bucket exists → skip to Step 4.3
# 404 / NoSuchBucket → continue to Step 4.2
Step 4.2 — Create the state bucket with versioning + SSE [HITL]
- CloudShell one-liner
- Console
# Run inside CloudShell (management account) — [HITL]
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=<HOME_REGION>
BUCKET="${ACCOUNT_ID}-tfstate-${REGION}"
aws s3api create-bucket --bucket "$BUCKET" --region "$REGION" \
--create-bucket-configuration LocationConstraint="$REGION"
aws s3api put-bucket-versioning --bucket "$BUCKET" \
--versioning-configuration Status=Enabled
aws s3api put-bucket-encryption --bucket "$BUCKET" \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
aws s3api put-public-access-block --bucket "$BUCKET" \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
Note: for us-east-1 omit --create-bucket-configuration (it is the default region).
- Navigate to S3 → Create bucket.
- Bucket name:
${ACCOUNT_ID}-tfstate-<HOME_REGION>(use your actual 12-digit account ID). - AWS Region:
<HOME_REGION>. - Block all public access: keep all four checkboxes enabled.
- Bucket Versioning: Enable.
- Default encryption: SSE-S3 (AES-256).
- Click Create bucket.
Step 4.3 — Verify bucket is ready
# READONLY verify
aws s3api get-bucket-versioning \
--bucket ${ACCOUNT_ID}-tfstate-<HOME_REGION> \
--profile <YOUR_MANAGEMENT_PROFILE>
# Expected: {"Status": "Enabled"}
backend.tf sets use_lockfile = true. Terraform native S3 locking is used — no DynamoDB
table is needed. Do NOT create a lock table.
5W1H — S3 State Backend Bootstrap
| Question | Answer |
|---|---|
| Why | Terraform's S3 backend must be initialised before any terraform init is run; the bucket cannot be a resource in the same root that depends on it (anti-pattern: SELF_REFERENTIAL_TFSTATE). |
| What if missing | terraform init fails with NoSuchBucket; the entire Terraform chain is blocked. |
| Business value | State in S3 with versioning enables point-in-time recovery; use_lockfile = true prevents concurrent apply races without a DynamoDB dependency. |
| Purpose | Creates the durable state store that Terraform uses to track all Identity Center resources; this bucket outlives any single terraform apply. |
| Critical thinking | The bucket name convention ${ACCOUNT_ID}-tfstate-<HOME_REGION> is portable. If you rename the bucket later, all engineers must run terraform init -reconfigure. Do not rename after the first terraform apply. |
Section 5 — Everything-as-Code: One Terraform Run
As the HITL operator, I want a single terraform apply in infra/terraform/aws/identity/
to create the B2B member account, SSO admin user, groups, permission sets, and assignments —
with no further console clickops.
This single Terraform run covers:
| Resource | Terraform | Notes |
|---|---|---|
| B2B member account | aws_organizations_account | email = [email protected] (variable) |
| Bootstrap admin user | modules/sso sso_users | Password set via email OTP after apply |
| Admin group | modules/sso sso_groups | |
| Permission sets | modules/sso | AdministratorAccess, PowerUserAccess, ReadOnly, SecurityAudit |
| Account assignments | modules/sso | Group-to-account mapping from YAML |
aws_organizations_account caveats — read before apply- Email must be globally unique:
[email protected]cannot be reused across any AWS account, ever. Use sub-addressing (e.g.,[email protected]) to preserve the human inbox. terraform destroydoes NOT close the account: it removes/suspends the account in Organizations with a 90-day close window. The root email stays locked during that period. Treat member-account creation as near-irreversible. Theterraform applybelow is[HITL]-gated for this reason.
Step 5.1 — Verify SSO instance is live
# READONLY verify — confirm the instance from Section 2 is active
aws sso-admin list-instances \
--profile <YOUR_MANAGEMENT_PROFILE> \
--region <HOME_REGION>
# Expected: 1 instance with your OceanSoft Identity Store ID (not d-9767913734 or d-976752e8d5)
Step 5.2 — Terraform init
cd infra/terraform/aws/identity/
terraform init \
-backend-config="bucket=${ACCOUNT_ID}-tfstate-<HOME_REGION>" \
-backend-config="region=<HOME_REGION>"
# Expected: "Terraform has been successfully initialized!"
Step 5.3 — Tier 1 snapshot tests
# Run inside the devcontainer (nnthanh101/terraform:2.6.0)
task test:tier1 MODULE=sso
# Expected: 10/10 pass, ~2-3 seconds
Step 5.4 — Terraform plan
terraform plan -var-file=terraform.tfvars
# Review output: confirm aws_organizations_account, sso_users, sso_groups, permission sets
# Expected: X to add, 0 to change, 0 to destroy
Step 5.5 — Apply [HITL]
# [HITL] — member-account creation is near-irreversible; review plan output first
terraform apply -var-file=terraform.tfvars
# Type: yes
# Expected: Apply complete! Resources: X added, 0 changed, 0 destroyed.
After apply, the bootstrap admin user receives a password-setup email (OTP). Set a password and MFA before continuing. The bootstrap admin user will receive a password-setup email within 1–5 minutes; check spam if delayed. Do not proceed to Section 6 until the password and MFA are configured.
Step 5.6 — Capture your start URL
# READONLY — retrieve YOUR OceanSoft start URL from the console or CLI
aws sso-admin list-instances \
--profile <YOUR_MANAGEMENT_PROFILE> \
--region <HOME_REGION> \
--query 'Instances[0].IdentityStoreId' \
--output text
# Build your start URL: https://d-<output>.awsapps.com/start
Alternatively: IAM Identity Center console → Settings → Identity source tab → AWS access portal URL.
Save this URL. You will use it in [sso-session oceansoft] in Section 6.
Step 5.7 — Teardown bootstrap credential (if Option B was used)
If you used a temporary IAM user access key (Section 3 Option B), delete it now using the teardown commands listed in that section.
5W1H — Terraform IaC Run
| Question | Answer |
|---|---|
| Why | All post-enablement objects (member accounts, users, groups, permission sets, assignments) must be code-managed; console creation is invisible to version control and unrecoverable from state. |
| What if missing | Identity Center exists but has no groups or assignments; engineers have no permission sets to assume; the B2B account is unreachable via SSO. |
| Business value | Terraform-managed access changes are pull-request-reviewed, audit-trailed, and recoverable from state; APRA CPS 234 requires evidence of controlled access changes. |
| Purpose | Converts the manually-enabled Identity Center instance into a fully code-managed access control layer that scales across additional accounts without console work. |
| Critical thinking | enable_organizations_lookup = true in main.tf requires a live AWS Organization at plan time; the Organization is created automatically when Identity Center is enabled (Section 2) — no manual setup is needed before this step. |
Section 6 — Profiles + Login + Verify
As the HITL operator, I want ready-to-paste ~/.aws/config blocks so that I can authenticate
with aws sso login and run Terraform without hardcoded credentials.
Step 6.1 — Check whether SSO profiles already exist
# READONLY verify
grep -A 6 '\[profile sso\]' ~/.aws/config 2>/dev/null || echo "profile sso NOT FOUND"
grep -A 6 '\[profile b2b\]' ~/.aws/config 2>/dev/null || echo "profile b2b NOT FOUND"
If both profiles are found and sso_start_url matches your OceanSoft start URL → skip to Step 6.3.
If either profile points to a foreign instance (d-9767913734 or d-976752e8d5) → replace as shown.
Step 6.2 — Add SSO profiles to ~/.aws/config [HITL]
[sso-session oceansoft]
sso_start_url = https://d-<YOUR_OWN_ID>.awsapps.com/start
sso_region = <HOME_REGION>
sso_registration_scopes = sso:account:access
[profile sso]
sso_session = oceansoft
sso_account_id = ${MANAGEMENT_ACCOUNT_ID}
sso_role_name = AdministratorAccess
region = <HOME_REGION>
output = json
[profile b2b]
sso_session = oceansoft
sso_account_id = ${B2B_ACCOUNT_ID}
sso_role_name = PowerUserAccess
region = <HOME_REGION>
output = json
Replace:
<YOUR_OWN_ID>→ the Identity Store ID from Step 5.6<HOME_REGION>→ your chosen home region${MANAGEMENT_ACCOUNT_ID}→ 12-digit management account ID${B2B_ACCOUNT_ID}→ 12-digit B2B workload account ID
Step 6.3 — Test SSO login [HITL]
# [HITL] — opens browser for consent
aws sso login --profile sso
Step 6.4 — Final verify chain
# READONLY verify — confirm SSO instance
aws sso-admin list-instances \
--profile sso --region <HOME_REGION>
# Expected: 1 instance, YOUR Identity Store ID (not d-9767913734 or d-976752e8d5)
# READONLY verify — list groups created by Terraform
aws identitystore list-groups \
--identity-store-id <YOUR_IDENTITY_STORE_ID> \
--region <HOME_REGION> --profile sso \
--query 'Groups[*].DisplayName'
# Expected: ["PlatformTeam", "PowerUsers", "AuditTeam", "SecurityTeam"]
Step 6.5 — Deploy chain (subsequent applies)
cd infra/terraform/aws/identity/
terraform init \
-backend-config="bucket=${ACCOUNT_ID}-tfstate-<HOME_REGION>" \
-backend-config="region=<HOME_REGION>"
terraform plan -var-file=terraform.tfvars
task test:tier1 MODULE=sso
# [HITL] — review plan before applying
terraform apply -var-file=terraform.tfvars
5W1H — AWS CLI Profile Wiring
| Question | Answer |
|---|---|
| Why | Terraform and AWS CLI commands require named profiles; static credentials in environment variables are an IAM anti-pattern blocked by the org SCP. |
| What if missing | terraform init -backend-config="..." fails with NoCredentialProviders; the entire deploy chain is blocked. |
| Business value | SSO profiles use short-lived tokens (max 8 hours); no long-lived credentials in CI or local environments; OIDC handles machine identity (TI-2c). |
| Purpose | Maps the two accounts (management, b2b) to their Terraform provider aliases so aws.identity_center and the default provider resolve correctly without any hardcoded secrets. |
| Critical thinking | The sso_role_name in the profile must match a permission set that exists in Identity Center AND has been assigned to the account in terraform.tfvars. If you run Terraform before the assignment was applied, the profile authenticates but Terraform gets access-denied on SSO admin APIs. |
References
- State migration guide:
terraform-aws/accounts/management-account/README.md - Entra ID federation (SAML/SCIM):
identity-center-enablement.mdxsibling →entra-federation.mdx - ADR-006 (S3 locking, no DynamoDB):
terraform-aws/.adlc/adrs/ - ADR-021 (parallel 2-account topology):
docs/content/architecture/adrs/ADR-021-parallel-2-account-topology.md modules/ssov1.3.0:terraform-aws/modules/sso/