From ed26ff937d724a5b2a762e48990b14cd95b2d1ec Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:32:40 -0400 Subject: [PATCH] Document centralized root access: README runbook + stack comment updates (#52) Comment/docs only, no template change (synth verified). Records the 2026-07-14 rollout: features enabled, member root credentials deleted, recovery runbook (manual detach, inheritance + propagation gotchas), new-account flow superseding root-harden-before-OU-move, extdev 5-SCP quota saturation. --- README.md | 65 +++++++++++++++++++++++++++++++++++++ lib/org-governance-stack.ts | 21 +++++++++--- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 38fb4f5..c507461 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,71 @@ aws ce update-cost-allocation-tags-status --cost-allocation-tags-status \ 'TagKey=Project,Status=Active' 'TagKey=Owner,Status=Active' 'TagKey=Environment,Status=Active' ``` +### Centralized root access management (org-level, no CloudFormation resource) + +**STATUS: ENABLED 2026-07-14, all member root credentials DELETED** (evidence: +`~/Documents/repositories/_audits/centralized-root-access-evidence-2026-07-14.md`). +Member accounts have NO root credentials; the only root path is a privileged +session from the management account. The management account's own root is NOT +centrally manageable and stays password+MFA hardened. + +```bash +# Enable (mgmt account). ORDER MATTERS: trusted access must be enabled +# explicitly first — enable-organizations-root-credentials-management does +# NOT auto-enable it (fails ServiceAccessNotEnabledException). +aws organizations enable-aws-service-access --service-principal iam.amazonaws.com +aws iam enable-organizations-root-credentials-management +aws iam enable-organizations-root-sessions + +# Periodic verification (add to governance checks): expect BOTH features +aws iam list-organizations-features +``` + +**Audit / delete member root credentials** (task-scoped root sessions, 15-min): + +```bash +aws sts assume-root --target-principal \ + --task-policy-arn arn=arn:aws:iam::aws:policy/root-task/IAMAuditRootUserCredentials +# then, with the session creds (no --user-name; root has none): +# get-login-profile / list-mfa-devices / list-access-keys / list-signing-certificates +aws sts assume-root --target-principal \ + --task-policy-arn arn=arn:aws:iam::aws:policy/root-task/IAMDeleteRootUserCredentials +# delete-login-profile; deactivate-mfa-device --serial-number +# GOTCHA: the delete task policy explicitly DENIES iam:DeleteVirtualMFADevice — +# remove the orphaned virtual-device OBJECT via OrganizationAccountAccessRole. +# DONE = four surfaces clear: login profile NoSuchEntity, MFA/keys/certs all empty. +``` + +**Root recovery runbook** (proven by drill on prod 2026-07-14): + +1. `deny-root-user` (p-2idoxozz) DENIES root sessions in every covered OU + (SCPs evaluate `sts:AssumeRoot` sessions — the principal is the member + root ARN). Recovery therefore starts with a **manual, temporary detach** + (`aws organizations detach-policy` — NOT a CDK deploy), timeboxed minutes. +2. GOTCHA (inheritance): p-2idoxozz is attached to `workloads` AND its child + OUs — for an account under workloads/, detach from BOTH the child OU and + workloads, or the inherited deny still applies. Allow ~10s propagation. +3. Freeze deploys of `seahaven-org-governance` for the window (a concurrent + deploy would re-attach mid-recovery); verify no CD run in flight first. +4. `aws sts assume-root --target-principal --task-policy-arn + arn=arn:aws:iam::aws:policy/root-task/IAMCreateRootUserPassword` → + `create-login-profile` (no args) restores a login profile. +5. Do the root-only task, DELETE the credentials again (four-surface verify), + reattach the SCP(s), confirm `list-targets-for-policy` matches the + pre-detach capture and stack drift is IN_SYNC. +6. extdev extra: `external-dev-iam-guardrails` also denies + `iam:CreateLoginProfile` — recovery there needs that SCP temporarily + detached too. The extdev OU sits at the **5-SCP hard quota**: any new + guardrail for extdev must attach at the ACCOUNT (396287094661) or + consolidate into an existing policy. + +**New-account flow (supersedes root-harden-before-OU-move):** create the +account at the org ROOT → it has no root credentials from birth (verify with +the audit session) → bootstrap + deploy role + baseline → `move-account` into +the target OU → verify SCP inheritance + region-lock canary. No mailbox or +MFA enrollment step. Root-usage monitoring: GuardDuty +`Policy:IAMUser/RootCredentialUsage` + CIS 4.3 alarm remain active. + ### Delegated security administration (Phase 3, no CloudFormation resource) Account **seahaven-security (001520130573)** is the org's delegated diff --git a/lib/org-governance-stack.ts b/lib/org-governance-stack.ts index d680b89..bfb80f6 100644 --- a/lib/org-governance-stack.ts +++ b/lib/org-governance-stack.ts @@ -44,8 +44,10 @@ const scpContent = (name: string): Record => * * Cross-review dispositions (GPT-4.1, 2026-07-14): * - deny-root-user blocks root MFA enrollment (iam:EnableMFADevice as root). - * OPERATIONAL REQUIREMENT: create new accounts at the org ROOT, complete - * root hardening (MFA, contacts), THEN move-account into the target OU. + * SUPERSEDED same day by centralized root access management: member root + * credentials are DELETED (none exist to harden), so new accounts go + * create-at-ROOT → verify credential-free → baseline → move-account. + * Recovery = assume-root + temporary manual SCP detach (README runbook). * - Delegated-admin ops (Phase 3) are unaffected by protect-security-baseline: * org-managed GuardDuty/SecurityHub act on members via service-linked * roles, which SCPs do not evaluate. If a legitimate admin action is ever @@ -291,9 +293,14 @@ export class OrgGovernanceStack extends cdk.Stack { retain(securityGuardrails); // Root-user lockout for member accounts: root has no operational role - // (OrganizationAccountAccessRole + Identity Center cover everything). If a - // genuinely root-only task ever arises (account closure, certain tax - // settings), detach temporarily via a gated targetIds change. + // (OrganizationAccountAccessRole + Identity Center cover everything), and + // since 2026-07-14 member root credentials are DELETED via centralized + // root access management. This deny ALSO catches centralized root + // sessions (sts:AssumeRoot runs as the member root principal; member + // SCPs apply) — root recovery starts with a MANUAL, timeboxed + // detach-policy call (never a deploy: a concurrent deploy re-attaches), + // and for accounts under workloads/ the detach must cover BOTH the child + // OU and workloads (inherited attachment). Full runbook in the README. const denyRootUser = new organizations.CfnPolicy(this, "DenyRootUser", { name: "deny-root-user", type: "SERVICE_CONTROL_POLICY", @@ -325,6 +332,10 @@ export class OrgGovernanceStack extends cdk.Stack { retain(denyRootUser); // ── Adopted (cdk-imported) external-dev OU + its 3 SCPs ───────────────── + // QUOTA: with deny-root-user attached (2026-07-14) this OU carries 5 SCPs + // = the AWS hard limit per target. Any new guardrail for external-dev + // must attach at the ACCOUNT (396287094661, own 5-slot budget) or + // consolidate into one of these policies — an OU-level attach will fail. // Imported 2026-07-14 by Id (ou-nbuj-q34yz3ql, p-i59g24mz, p-8yty5mnd, // p-ivmwtipw). Properties are byte-exact to the live resources at import // time (content JSON in lib/scp/, descriptions/targets verified via