mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 08:03:19 +00:00
* 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).
236 lines
10 KiB
TypeScript
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 });
|
|
}
|
|
}
|