mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +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.
This commit is contained in:
parent
3ab3bc773a
commit
97f27bd8d7
2 changed files with 229 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ import { BackupStack } from "../lib/backup-stack";
|
|||
import { RegionalBaselineStack } from "../lib/regional-baseline-stack";
|
||||
import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack";
|
||||
import { MemberBaselineStack } from "../lib/member-baseline-stack";
|
||||
import { OrgGovernanceStack } from "../lib/org-governance-stack";
|
||||
|
||||
const ACCOUNT = "328440206208";
|
||||
const EXTERNAL_DEV_ACCOUNT = "396287094661";
|
||||
|
|
@ -45,6 +46,14 @@ new AccountBaselineStack(app, "account-baseline", {
|
|||
// matching the currently deployed stack.
|
||||
const EXTDEV_FLOW_LOG_VPC_IDS: string[] = [];
|
||||
|
||||
// ── Org structure: OUs + generalized SCPs (management account only) ─────────
|
||||
// Existing external-dev OU + its 3 SCPs are adopted into this stack via
|
||||
// `cdk import` post-deploy — see lib/org-governance-stack.ts header + README.
|
||||
new OrgGovernanceStack(app, "org-governance", {
|
||||
stackName: "seahaven-org-governance",
|
||||
env: { account: ACCOUNT, region: "us-east-1" },
|
||||
});
|
||||
|
||||
new MemberBaselineStack(app, "external-dev-baseline", {
|
||||
stackName: "seahaven-external-dev-baseline",
|
||||
env: { account: EXTERNAL_DEV_ACCOUNT, region: "us-east-1" },
|
||||
|
|
|
|||
220
lib/org-governance-stack.ts
Normal file
220
lib/org-governance-stack.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
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.
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue