From 18f0f40e74a9badef4aa0b2656fbfcd01c0789ea Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:32:50 -0400 Subject: [PATCH] seahaven-security account baseline + security-OU guardrails (Phase 3) (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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). --- .github/workflows/deploy.yaml | 9 ++++ README.md | 71 ++++++++++++++++++++++++++++ bin/app.ts | 28 +++++++++++ lib/org-governance-stack.ts | 89 +++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index d21575a..cdb57a9 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -32,3 +32,12 @@ jobs: stacks: "external-dev-baseline" secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_EXTDEV }} + + deploy-security: + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main + 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 21cd8ea..2fd03cb 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Stacks (deployed by the CD workflow — one job per target account): | `seahaven-backup` | 328440206208 | us-east-1 | Primary AWS Backup vault + plan + role (C-7) | | `seahaven-backup-offsite` | 328440206208 | us-west-2 | Governance-locked offsite copy vault (C-7) | | `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | Member-account baseline: Config, GuardDuty, Security Hub (FSBP + CIS v3.0), Access Analyzer, flow logs, budget | +| `seahaven-security-baseline` | 001520130573 | us-east-1 | Member-account baseline for the delegated security-admin account (same construct set) | ## CDK app @@ -57,6 +58,7 @@ the TypeScript source — no separate compile step needed for `cdk synth` / | `backup-offsite` | `seahaven-backup-offsite` | 328440206208 | us-west-2 | `lib/backup-offsite-stack.ts` | | `backup` | `seahaven-backup` | 328440206208 | us-east-1 | `lib/backup-stack.ts` | | `external-dev-baseline` | `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | `lib/member-baseline-stack.ts` | +| `security-baseline` | `seahaven-security-baseline` | 001520130573 | us-east-1 | `lib/member-baseline-stack.ts` | The member-account stack (`external-dev-baseline`) deploys with credentials for **396287094661** — the CD workflow runs it as a separate job assuming that @@ -242,6 +244,75 @@ aws ce update-cost-allocation-tags-status --cost-allocation-tags-status \ 'TagKey=Project,Status=Active' 'TagKey=Owner,Status=Active' 'TagKey=Environment,Status=Active' ``` +### Delegated security administration (Phase 3, no CloudFormation resource) + +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) +aws guardduty enable-organization-admin-account --admin-account-id 001520130573 +# then AS 001520130573: update-organization-configuration --auto-enable-organization-members ALL +# + create-members for pre-existing accounts (mgmt, external-dev) + +# Security Hub: delegate + auto-enable new members +aws securityhub enable-organization-admin-account --admin-account-id 001520130573 +# then AS 001520130573: update-organization-configuration --auto-enable + +# IAM Access Analyzer: delegate + ORGANIZATION-scoped analyzer +aws organizations register-delegated-administrator \ + --account-id 001520130573 --service-principal access-analyzer.amazonaws.com +# then AS 001520130573: create-analyzer --type ORGANIZATION + +# Config: delegate the aggregator (recorders stay per-account in the baselines; +# the aggregator's recorder-status view is the drift detector) +aws organizations register-delegated-administrator \ + --account-id 001520130573 --service-principal config.amazonaws.com +# then AS 001520130573: put-configuration-aggregator --organization-aggregation-source + +# Inspector2: delegate + associate members +aws inspector2 enable-delegated-admin-account --delegated-admin-account-id 001520130573 +``` + +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 Billing Alerts* under Billing → Billing preferences; there is no public API/CLI. diff --git a/bin/app.ts b/bin/app.ts index 15e81a4..813ee3f 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -11,6 +11,7 @@ import { OrgGovernanceStack } from "../lib/org-governance-stack"; const ACCOUNT = "328440206208"; const EXTERNAL_DEV_ACCOUNT = "396287094661"; +const SECURITY_ACCOUNT = "001520130573"; // All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7. // Index-derived logical IDs — append only, never reorder. @@ -72,6 +73,33 @@ new MemberBaselineStack(app, "external-dev-baseline", { managedByTag: "seahaven-external-dev-baseline", }); +// ── Member-account baseline: seahaven-security (Phase 3) ──────────────────── +// 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", +}); + // ── Shared DynamoDB CMK (INFRA-95 / M-3) ───────────────────────────────────── // Dedicated, standalone stack so the customer-managed key for sensitive // finance/PII DynamoDB tables is an independent shared dependency for the owning 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