diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 7297f08..cdb57a9 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -38,5 +38,6 @@ jobs: with: node-version: "24" stacks: "security-baseline" + stack-name: "seahaven-security-baseline" secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_SECURITY }} diff --git a/README.md b/README.md index c0c011d..2fd03cb 100644 --- a/README.md +++ b/README.md @@ -246,10 +246,31 @@ aws ce update-cost-allocation-tags-status --cost-allocation-tags-status \ ### Delegated security administration (Phase 3, no CloudFormation resource) -Account **seahaven-security (001520130573)** is the org's delegated -administrator for the detective services. Delegation has no CFN types and is -applied via CLI from the **management account** (applied 2026-07-14; recorded -here as the runbook — safe to re-run, all calls are idempotent): +Account **seahaven-security (001520130573)** will be the org's delegated +administrator for the detective services. **STATUS: PENDING — delegation has +NOT been applied yet.** Flip this section to "applied" (with verification +output) only in the commit that accompanies actual execution. + +HARD PRECONDITIONS — do not run the first `enable-organization-admin-account` +call until ALL of these verify true (security review SEC-BASE-B/D: delegating +to an account with unhardened root and no SCPs hands the org's detection +nerve center to the weakest credential; delegating before the baseline deploy +wedges the stack CREATE on the auto-created detector/hub, and the retry +collides with the orphaned RETAIN fixed-name buckets): + +```bash +# 1. Baseline deployed: +aws cloudformation describe-stacks --stack-name seahaven-security-baseline \ + --query 'Stacks[0].StackStatus' # expect CREATE_COMPLETE/UPDATE_COMPLETE (as 001520130573) +# 2. Root hardened (manual, Adam): MFA enabled, no root keys, alternate contacts set +aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled' # expect 1 (as 001520130573) +# 3. Account moved into the security OU (SCPs in effect): +aws organizations list-parents --child-id 001520130573 \ + --query 'Parents[0].Id' # expect ou-nbuj-v0s9630u +``` + +Delegation is then applied via CLI from the **management account** (all calls +idempotent): ```bash # GuardDuty: delegate + auto-enable all org members (adopts existing detectors) @@ -276,9 +297,21 @@ aws organizations register-delegated-administrator \ aws inspector2 enable-delegated-admin-account --delegated-admin-account-id 001520130573 ``` -New member accounts are detected/enrolled automatically after this — do NOT -add per-account GuardDuty/SecurityHub resources to future member baselines -(slimming the existing member stacks is a separate, verification-gated change). +ONLY once delegation is live AND auto-enrollment is verified (a new member +shows enrolled in the security account's GuardDuty/Security Hub consoles): +new member accounts are then detected/enrolled automatically, and future +member baselines can drop per-account GuardDuty/SecurityHub resources. +Until then, every member baseline MUST keep them (slimming the existing +member stacks is a separate, verification-gated change; note the DA +account's own CFN-owned detector/hub become co-managed after delegation — +never rename/remove them via CFN while the account is delegated admin). + +Accepted read-surface note (SEC-BASE-I): the org Config aggregator + +ORGANIZATION Access Analyzer give principals in 001520130573 org-wide READ of +resource configurations (including recorded Lambda env vars) and IAM policies. +Main-branch write access to this repo therefore implies that read surface — +verify no prod Lambda keeps secrets in env vars before creating the +aggregator, and keep branch protection tight. **L-8 (billing-metrics preference) is OUTSTANDING — console only.** Enabling the CloudWatch `EstimatedCharges` metric in us-east-1 requires turning on *Receive diff --git a/bin/app.ts b/bin/app.ts index 3eb5aa8..813ee3f 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -74,18 +74,28 @@ new MemberBaselineStack(app, "external-dev-baseline", { }); // ── Member-account baseline: seahaven-security (Phase 3) ──────────────────── -// The org's delegated security administrator (GuardDuty / Security Hub / IAM -// Access Analyzer / Config aggregator / Inspector2 — delegation itself is CLI -// + README runbook, no CFN types). Created 2026-07-14 at org ROOT; moves into -// the security OU only after manual root hardening (deny-root-user invariant, -// see lib/org-governance-stack.ts). +// The org's delegated security administrator-to-be (GuardDuty / Security Hub / +// IAM Access Analyzer / Config aggregator / Inspector2 — delegation is CLI + +// README runbook with HARD preconditions, no CFN types). Created 2026-07-14 at +// org ROOT; moves into the security OU only after manual root hardening +// (deny-root-user invariant, see lib/org-governance-stack.ts). Delegation runs +// ONLY after the OU move (SEC-BASE-B). +// LIFECYCLE (SEC-BASE-E): once delegation is live, this stack's GuardDuty +// detector + Security Hub hub are co-managed by the org admin config — never +// rename/remove those constructs via CFN while the account is delegated admin. new MemberBaselineStack(app, "security-baseline", { stackName: "seahaven-security-baseline", env: { account: SECURITY_ACCOUNT, region: "us-east-1" }, namePrefix: "seahaven-security", monthlyBudgetUsd: 50, + // aws@ (not a per-account mailbox) is deliberate: Adam's 2026-07-14 + // direction routes all AWS notifications to aws@seahaven.com; extdev's + // dedicated mailbox predates that direction. budgetAlertEmail: "aws@seahaven.com", ownerEmail: "adam@seahaven.com", + // Empty is deliberate: the account's default VPC is DELETED (a delegated + // security-admin account runs no workloads — SEC-BASE-F; also clears the + // default-VPC CIS/FSBP controls). Any future VPC id gets appended via PR. flowLogVpcIds: [], managedByTag: "seahaven-org-baseline", }); diff --git a/lib/org-governance-stack.ts b/lib/org-governance-stack.ts index 040d8b7..41b42e9 100644 --- a/lib/org-governance-stack.ts +++ b/lib/org-governance-stack.ts @@ -201,6 +201,95 @@ export class OrgGovernanceStack extends cdk.Stack { }); 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