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).
This commit is contained in:
Adam Moussa 2026-07-14 15:27:37 -04:00
parent 11f6e30a42
commit 3817b7acf5
No known key found for this signature in database
4 changed files with 145 additions and 12 deletions

View file

@ -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 }}

View file

@ -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

View file

@ -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",
});

View file

@ -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