import * as fs from "fs"; import * as path from "path"; import * as cdk from "aws-cdk-lib"; import * as organizations from "aws-cdk-lib/aws-organizations"; import { Construct } from "constructs"; /** Byte-exact live SCP content (lib/scp/*.json) — see import block below. */ const scpContent = (name: string): Record => JSON.parse( fs.readFileSync(path.join(__dirname, "scp", `${name}.json`), "utf8") ); /** * AWS Organizations structure for org o-9kufuzz6b4: OU skeleton + generalized * service-control policies (multi-account segregation plan Phase 2, * 2026-07-14). Deploys to the MANAGEMENT account only — Organizations OU/SCP * APIs are management-account-scoped. * * Target OU tree (root r-nbuj): * workloads/ new production + nonprod member accounts * prod/ seahaven-prod (Phase 5) * nonprod/ seahaven-dev (Phase 4) * security/ seahaven-security (Phase 3, delegated admin) * sandbox/ experiments / personal workloads (optional) * graveyard/ closed/suspended accounts (637423252038) * external-dev/ EXISTING (ou-nbuj-q34yz3ql) — adopted via `cdk import` * together with its 3 existing SCPs; see README runbook. * * INVARIANTS (safety-critical — reviewed under the mandatory IAM gates): * - Every resource here carries RemovalPolicy.RETAIN (DeletionPolicy + * UpdateReplacePolicy). CFN must never detach/delete a live guardrail via * stack delete or logical-id churn. Keep it that way permanently. * - CfnPolicy.targetIds is the EXACT live attachment set. Removing an entry * DETACHES that guardrail on the next deploy — every targetIds edit is a * live IAM change requiring GPT-4.1 cross-review + /sh-security-review. * - Policy content must stay a JSON OBJECT (not a string) or drift detection * on content/attachments silently stops working. * - SCPs do NOT bind the management account; region-lock exempts global * services via NotAction (pattern proven on p-i59g24mz). * * Rollout discipline (Phase 2 canary): new SCPs attach to the NEW (empty) OUs * only. Extending any of them to the external-dev OU is a separate, gated * targetIds change made only after live access verification in 396287094661. * * 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. * - 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 * denied, exemptions change only through the mandatory gates. * - `arn:aws:iam::*:role/cdk-hnb659fds-*` exemption is ACCEPTED RISK (same * decision as the external-dev guardrails): it is the only generic * cross-account expression for CDK exec roles; member baselines protect * those roles from takeover (ProtectPrivilegedRoles pattern). * - Region-lock NotAction list deliberately matches battle-tested * p-i59g24mz; regional services (s3, kms, logs, ssm...) stay region-locked * BY DESIGN — do not add them to NotAction (that would exempt them). */ export class OrgGovernanceStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const ROOT_ID = "r-nbuj"; // ── OU skeleton ───────────────────────────────────────────────────────── const retain = (resource: organizations.CfnOrganizationalUnit | organizations.CfnPolicy) => { resource.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; resource.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN; }; const workloadsOu = new organizations.CfnOrganizationalUnit(this, "WorkloadsOu", { name: "workloads", parentId: ROOT_ID, }); retain(workloadsOu); const prodOu = new organizations.CfnOrganizationalUnit(this, "ProdOu", { name: "prod", parentId: workloadsOu.attrId, }); retain(prodOu); const nonprodOu = new organizations.CfnOrganizationalUnit(this, "NonprodOu", { name: "nonprod", parentId: workloadsOu.attrId, }); retain(nonprodOu); const securityOu = new organizations.CfnOrganizationalUnit(this, "SecurityOu", { name: "security", parentId: ROOT_ID, }); retain(securityOu); const sandboxOu = new organizations.CfnOrganizationalUnit(this, "SandboxOu", { name: "sandbox", parentId: ROOT_ID, }); retain(sandboxOu); const graveyardOu = new organizations.CfnOrganizationalUnit(this, "GraveyardOu", { name: "graveyard", parentId: ROOT_ID, }); retain(graveyardOu); // ── Generalized SCPs ──────────────────────────────────────────────────── // Patterns generalized from the external-dev OU guardrails (p-i59g24mz / // p-ivmwtipw), which stay attached to that OU unchanged. Exemption // principals use cross-account ArnLike patterns because these policies // serve every future member account. // Region lock for workload accounts: us-east-1 (primary) + us-west-2 // (offsite backup/DR). Global services exempted via NotAction — the same // list proven on the external-dev region lock. const workloadsRegionLock = new organizations.CfnPolicy(this, "WorkloadsRegionLock", { name: "workloads-region-lock", type: "SERVICE_CONTROL_POLICY", description: "Deny workload member accounts outside us-east-1 (primary) and us-west-2 (backup/DR)", targetIds: [workloadsOu.attrId], content: { Version: "2012-10-17", Statement: [ { Sid: "DenyRegionsOutsideApproved", Effect: "Deny", NotAction: [ "iam:*", "organizations:*", "account:*", "sts:*", "route53:*", "route53domains:*", "cloudfront:*", "waf:*", "shield:*", "globalaccelerator:*", "budgets:*", "ce:*", "cur:*", "health:*", "support:*", "supportplans:*", "trustedadvisor:*", "artifact:*", "aws-portal:*", ], Resource: "*", Condition: { StringNotEquals: { "aws:RequestedRegion": ["us-east-1", "us-west-2"], }, }, }, ], }, }); retain(workloadsRegionLock); // Protect detective/security services in every member account. Exemptions // are the operational principals that legitimately manage these controls: // the org break-glass role, CDK exec roles, and the baseline Config // custom-resource roles (their onDelete stops the recorder by design). const protectSecurity = new organizations.CfnPolicy(this, "ProtectSecurityBaseline", { name: "protect-security-baseline", type: "SERVICE_CONTROL_POLICY", description: "Deny disabling CloudTrail/Config/GuardDuty/SecurityHub/AccessAnalyzer/Inspector2 and org-leave in member accounts", targetIds: [ workloadsOu.attrId, prodOu.attrId, nonprodOu.attrId, securityOu.attrId, sandboxOu.attrId, graveyardOu.attrId, ], content: { Version: "2012-10-17", Statement: [ { Sid: "DenyDisablingSecurityServices", Effect: "Deny", Action: [ "cloudtrail:StopLogging", "cloudtrail:DeleteTrail", "cloudtrail:UpdateTrail", "guardduty:DeleteDetector", "guardduty:UpdateDetector", "guardduty:DisassociateFromMasterAccount", "guardduty:DisassociateFromAdministratorAccount", "config:StopConfigurationRecorder", "config:DeleteConfigurationRecorder", "config:DeleteDeliveryChannel", "securityhub:DisableSecurityHub", "securityhub:BatchDisableStandards", "securityhub:DisassociateFromAdministratorAccount", "accessanalyzer:DeleteAnalyzer", "inspector2:Disable", ], Resource: "*", Condition: { ArnNotLike: { "aws:PrincipalArn": [ "arn:aws:iam::*:role/OrganizationAccountAccessRole", "arn:aws:iam::*:role/cdk-hnb659fds-*", "arn:aws:iam::*:role/seahaven-*-config-custom-resource-role", ], }, }, }, { Sid: "DenyLeavingOrganization", Effect: "Deny", Action: ["organizations:LeaveOrganization"], Resource: "*", }, ], }, }); retain(protectSecurity); // Guardrails specific to the delegated-security-admin OU (SEC-BASE-C): // the security account is the org's highest-blast-radius member, so it // gets the external-dev-style IAM guardrails plus protection of its // delegated-admin MEMBERSHIP surface (a compromised principal must not be // able to silently eject prod/extdev from org-wide detection). Break-glass // = OrganizationAccountAccessRole; CDK exec roles exempt where they must // manage stack-owned IAM. const securityGuardrails = new organizations.CfnPolicy(this, "SecurityGuardrails", { name: "security-guardrails", type: "SERVICE_CONTROL_POLICY", description: "security OU: region lock, IAM user/key lockout, privileged-role protection, delegated-admin membership protection", targetIds: [securityOu.attrId], content: { Version: "2012-10-17", Statement: [ { Sid: "DenyRegionsOutsideApproved", Effect: "Deny", NotAction: [ "iam:*", "organizations:*", "account:*", "sts:*", "route53:*", "route53domains:*", "cloudfront:*", "waf:*", "shield:*", "globalaccelerator:*", "budgets:*", "ce:*", "cur:*", "health:*", "support:*", "supportplans:*", "trustedadvisor:*", "artifact:*", "aws-portal:*", ], Resource: "*", Condition: { StringNotEquals: { "aws:RequestedRegion": ["us-east-1", "us-west-2"], }, }, }, { Sid: "DenyIamUserAndAccessKeyCreation", Effect: "Deny", Action: ["iam:CreateUser", "iam:CreateAccessKey", "iam:CreateLoginProfile"], Resource: "*", Condition: { ArnNotLike: { "aws:PrincipalArn": ["arn:aws:iam::*:role/OrganizationAccountAccessRole"], }, }, }, { Sid: "ProtectPrivilegedRoles", Effect: "Deny", Action: [ "iam:UpdateAssumeRolePolicy", "iam:AttachRolePolicy", "iam:DetachRolePolicy", "iam:PutRolePolicy", "iam:DeleteRolePolicy", "iam:DeleteRole", "iam:UpdateRole", "iam:TagRole", "iam:UntagRole", ], Resource: [ "arn:aws:iam::*:role/OrganizationAccountAccessRole", "arn:aws:iam::*:role/cdk-hnb659fds-*", "arn:aws:iam::*:role/githubdeploy-*", "arn:aws:iam::*:role/seahaven-security-config-*", "arn:aws:iam::*:role/aws-service-role/*", ], Condition: { ArnNotLike: { "aws:PrincipalArn": [ "arn:aws:iam::*:role/OrganizationAccountAccessRole", "arn:aws:iam::*:role/cdk-hnb659fds-*", ], }, }, }, { Sid: "ProtectDelegatedAdminMembership", Effect: "Deny", Action: [ "guardduty:DisassociateMembers", "guardduty:DeleteMembers", "guardduty:StopMonitoringMembers", "securityhub:DisassociateMembers", "securityhub:DeleteMembers", "inspector2:DisassociateMember", ], Resource: "*", Condition: { ArnNotLike: { "aws:PrincipalArn": ["arn:aws:iam::*:role/OrganizationAccountAccessRole"], }, }, }, ], }, }); 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. const denyRootUser = new organizations.CfnPolicy(this, "DenyRootUser", { name: "deny-root-user", type: "SERVICE_CONTROL_POLICY", description: "Deny all root-user actions in member accounts", targetIds: [ workloadsOu.attrId, prodOu.attrId, nonprodOu.attrId, securityOu.attrId, sandboxOu.attrId, graveyardOu.attrId, ], content: { Version: "2012-10-17", Statement: [ { Sid: "DenyRootUser", Effect: "Deny", Action: "*", Resource: "*", Condition: { StringLike: { "aws:PrincipalArn": "arn:aws:iam::*:root" }, }, }, ], }, }); retain(denyRootUser); // ── Adopted (cdk-imported) external-dev OU + its 3 SCPs ───────────────── // 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 // describe-policy). RULES: content stays a JSON object (string form kills // drift detection); targetIds is the exact live attachment set — any edit // here detaches/attaches a LIVE guardrail on the isolated contractor // account and requires the mandatory review gates; never rename these // logical ids (rename = delete+create; Retain would orphan, not detach, // but the stack would lose the resource). const externalDevOu = new organizations.CfnOrganizationalUnit(this, "ExternalDevOu", { name: "external-dev", parentId: ROOT_ID, }); retain(externalDevOu); const externalDevRegionLock = new organizations.CfnPolicy(this, "ExternalDevRegionLock", { name: "external-dev-region-lock", type: "SERVICE_CONTROL_POLICY", description: "external-dev OU guardrail: external-dev-region-lock", targetIds: ["ou-nbuj-q34yz3ql"], content: scpContent("external-dev-region-lock"), }); retain(externalDevRegionLock); const externalDevIamGuardrails = new organizations.CfnPolicy(this, "ExternalDevIamGuardrails", { name: "external-dev-iam-guardrails", type: "SERVICE_CONTROL_POLICY", description: "external-dev OU guardrail: external-dev-iam-guardrails", targetIds: ["ou-nbuj-q34yz3ql"], content: scpContent("external-dev-iam-guardrails"), }); retain(externalDevIamGuardrails); const externalDevProtectSecurity = new organizations.CfnPolicy(this, "ExternalDevProtectSecurity", { name: "external-dev-protect-security", type: "SERVICE_CONTROL_POLICY", description: "external-dev OU guardrail: external-dev-protect-security", targetIds: ["ou-nbuj-q34yz3ql"], content: scpContent("external-dev-protect-security"), }); retain(externalDevProtectSecurity); new cdk.CfnOutput(this, "ExternalDevOuId", { value: externalDevOu.attrId }); new cdk.CfnOutput(this, "WorkloadsOuId", { value: workloadsOu.attrId }); new cdk.CfnOutput(this, "ProdOuId", { value: prodOu.attrId }); new cdk.CfnOutput(this, "NonprodOuId", { value: nonprodOu.attrId }); new cdk.CfnOutput(this, "SecurityOuId", { value: securityOu.attrId }); new cdk.CfnOutput(this, "SandboxOuId", { value: sandboxOu.attrId }); new cdk.CfnOutput(this, "GraveyardOuId", { value: graveyardOu.attrId }); } }