seahaven-org-baseline/lib/org-governance-stack.ts
Adam Moussa 22b04c4e75
Some checks are pending
Deploy / deploy-management (push) Waiting to run
Deploy / deploy-external-dev (push) Waiting to run
Org governance: OU skeleton + generalized SCPs (Phase 2) (#44)
* Add org-governance stack: OU skeleton + generalized SCPs

Phase 2 of the multi-account segregation plan: codifies the OU tree
(workloads/prod/nonprod, security, sandbox, graveyard) and three
org-wide SCPs (workloads-region-lock, protect-security-baseline,
deny-root-user) generalized from the proven external-dev guardrails.
All resources Retain — CFN must never detach a live guardrail. New SCPs
attach only to the new empty OUs; extending to external-dev is a
separate gated targetIds change after live verification.

* Record SCP cross-review dispositions in org-governance

Root hardening must precede the OU move (deny-root-user blocks root MFA
enrollment), delegated-admin flows ride service-linked roles that SCPs
never evaluate, and the cdk exec-role exemption is accepted risk
mirroring the external-dev guardrails.

* Add org-governance to the management deploy job

Explicit stack selectors require every new stack to join exactly one
CD job (SH-ORG-005 discipline documented in this file).
2026-07-14 14:02:30 -04:00

236 lines
10 KiB
TypeScript

import * as cdk from "aws-cdk-lib";
import * as organizations from "aws-cdk-lib/aws-organizations";
import { Construct } from "constructs";
/**
* 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);
// 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);
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 });
}
}