mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +00:00
* Add seahaven-security member baseline (Phase 3) Account 001520130573 is the org's delegated security administrator. Same member-baseline construct set as external-dev; own CD job under its own OIDC role. Created at org root pending manual root hardening before the OU move (deny-root-user invariant). * Document delegated security administration runbook Delegation to seahaven-security has no CloudFormation types; the CLI sequence is the record, same pattern as the other account toggles. * Apply Phase-3 security-review findings Delegation runbook marked PENDING with hard preconditions (baseline deployed, root MFA verified, account inside the security OU) — it had read as applied before execution, the org's known claimed-done-but-NOT failure mode (SEC-BASE-A/B). New security-guardrails SCP on the security OU: region lock, IAM user/key lockout, privileged-role protection, delegated-admin membership protection (SEC-BASE-C, cross-reviewed APPROVE). deploy-security gains stack-name pre-flight (SEC-BASE-D). Default VPC in 001520130573 deleted; empty flow-log list and aws@ alert routing documented as deliberate (SEC-BASE-F/H).
378 lines
16 KiB
TypeScript
378 lines
16 KiB
TypeScript
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<string, unknown> =>
|
|
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 });
|
|
}
|
|
}
|