mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-07 11:28:55 +00:00
seahaven-security account baseline + security-OU guardrails (Phase 3) (#47)
* 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).
This commit is contained in:
parent
57fd67324e
commit
18f0f40e74
4 changed files with 197 additions and 0 deletions
9
.github/workflows/deploy.yaml
vendored
9
.github/workflows/deploy.yaml
vendored
|
|
@ -32,3 +32,12 @@ jobs:
|
||||||
stacks: "external-dev-baseline"
|
stacks: "external-dev-baseline"
|
||||||
secrets:
|
secrets:
|
||||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_EXTDEV }}
|
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 }}
|
||||||
|
|
|
||||||
71
README.md
71
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` | 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-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-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
|
## 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-offsite` | `seahaven-backup-offsite` | 328440206208 | us-west-2 | `lib/backup-offsite-stack.ts` |
|
||||||
| `backup` | `seahaven-backup` | 328440206208 | us-east-1 | `lib/backup-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` |
|
| `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
|
The member-account stack (`external-dev-baseline`) deploys with credentials for
|
||||||
**396287094661** — the CD workflow runs it as a separate job assuming that
|
**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'
|
'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
|
**L-8 (billing-metrics preference) is OUTSTANDING — console only.** Enabling the
|
||||||
CloudWatch `EstimatedCharges` metric in us-east-1 requires turning on *Receive
|
CloudWatch `EstimatedCharges` metric in us-east-1 requires turning on *Receive
|
||||||
Billing Alerts* under Billing → Billing preferences; there is no public API/CLI.
|
Billing Alerts* under Billing → Billing preferences; there is no public API/CLI.
|
||||||
|
|
|
||||||
28
bin/app.ts
28
bin/app.ts
|
|
@ -11,6 +11,7 @@ import { OrgGovernanceStack } from "../lib/org-governance-stack";
|
||||||
|
|
||||||
const ACCOUNT = "328440206208";
|
const ACCOUNT = "328440206208";
|
||||||
const EXTERNAL_DEV_ACCOUNT = "396287094661";
|
const EXTERNAL_DEV_ACCOUNT = "396287094661";
|
||||||
|
const SECURITY_ACCOUNT = "001520130573";
|
||||||
|
|
||||||
// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7.
|
// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7.
|
||||||
// Index-derived logical IDs — append only, never reorder.
|
// Index-derived logical IDs — append only, never reorder.
|
||||||
|
|
@ -72,6 +73,33 @@ new MemberBaselineStack(app, "external-dev-baseline", {
|
||||||
managedByTag: "seahaven-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) ─────────────────────────────────────
|
// ── Shared DynamoDB CMK (INFRA-95 / M-3) ─────────────────────────────────────
|
||||||
// Dedicated, standalone stack so the customer-managed key for sensitive
|
// Dedicated, standalone stack so the customer-managed key for sensitive
|
||||||
// finance/PII DynamoDB tables is an independent shared dependency for the owning
|
// finance/PII DynamoDB tables is an independent shared dependency for the owning
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,95 @@ export class OrgGovernanceStack extends cdk.Stack {
|
||||||
});
|
});
|
||||||
retain(protectSecurity);
|
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
|
// Root-user lockout for member accounts: root has no operational role
|
||||||
// (OrganizationAccountAccessRole + Identity Center cover everything). If a
|
// (OrganizationAccountAccessRole + Identity Center cover everything). If a
|
||||||
// genuinely root-only task ever arises (account closure, certain tax
|
// genuinely root-only task ever arises (account closure, certain tax
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue