mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 05:43:17 +00:00
* Add account detective layer + budget (audit Day 1: H-2/H-3/H-4/M-5/M-10) Adds to the seahaven-account-baseline stack: - AWS Config recorder (all + global resources) + delivery channel + role + hardened delivery bucket (H-2, CIS 3.3/3.5). Recorder role IAM cross-reviewed. - GuardDuty detector, us-east-1 (H-3) - Security Hub with AWS FSBP v1.0.0 + CIS v3.0.0 standards, depends on Config (H-4) - IAM Access Analyzer, account scope (M-5) - Monthly cost budget $1,200 with 80/100% actual + 100% forecast alerts to adam@seahavenind.com (M-10) Scope us-east-1 only (all workloads here); multi-region is a follow-up. The CLI-applied governance toggles (M-6/M-3/M-7/L-8/M-11) are documented separately in the README runbook. * Document Day 1 detective layer + CLI governance toggles in README * Move Config recorder+channel to CLI (L1 stabilization deadlock) The L1 AWS::Config::ConfigurationRecorder hangs the stack: it never reaches CREATE_COMPLETE until recording is active (needs a delivery channel), and the delivery channel cannot be created until the recorder completes — a deadlock that hung the deploy ~27 min before manual cancel (2026-06-01). Keep the cross-reviewed recorder role + delivery bucket in IaC; create the recorder, delivery channel, and start recording via CLI (documented in README). Security Hub no longer takes a CFN dependency on the recorder; CIS/FSBP controls evaluate once Config is recording. Verified live: recording=true, SUCCESS.
191 lines
8.1 KiB
TypeScript
191 lines
8.1 KiB
TypeScript
import * as cdk from "aws-cdk-lib";
|
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
import * as guardduty from "aws-cdk-lib/aws-guardduty";
|
|
import * as securityhub from "aws-cdk-lib/aws-securityhub";
|
|
import * as accessanalyzer from "aws-cdk-lib/aws-accessanalyzer";
|
|
import { Construct } from "constructs";
|
|
|
|
/**
|
|
* Account-level detective controls (audit Day 1).
|
|
*
|
|
* Closes:
|
|
* H-2 AWS Config recorder + delivery channel (CIS 3.3/3.5)
|
|
* H-3 GuardDuty detector
|
|
* H-4 Security Hub with AWS FSBP + CIS v3.0 standards
|
|
* M-5 IAM Access Analyzer (account-scoped external-access analyzer)
|
|
*
|
|
* Scope is us-east-1 only — all workloads live here (Adam's call, Day 1).
|
|
* Multi-region coverage is a documented follow-up.
|
|
*/
|
|
export class DetectiveControls extends Construct {
|
|
constructor(scope: Construct, id: string) {
|
|
super(scope, id);
|
|
|
|
const stack = cdk.Stack.of(this);
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// H-2 AWS Config
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// Delivery bucket for Config snapshots/history. Private, TLS-only,
|
|
// versioned, SSE-S3 (Config writes here; SSE-S3 avoids a second KMS-grant
|
|
// failure mode and is sufficient — CIS does not require a CMK here).
|
|
const configBucket = new s3.Bucket(this, "ConfigBucket", {
|
|
bucketName: `seahaven-config-${stack.account}`,
|
|
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
enforceSSL: true,
|
|
versioned: true,
|
|
lifecycleRules: [
|
|
{
|
|
id: "expire-old-config",
|
|
expiration: cdk.Duration.days(365),
|
|
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
|
|
},
|
|
],
|
|
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
});
|
|
|
|
// Bucket policy that lets the Config service principal verify ownership
|
|
// and deliver objects (scoped to this account, owner-full-control ACL).
|
|
configBucket.addToResourcePolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "AWSConfigBucketPermissionsCheck",
|
|
effect: iam.Effect.ALLOW,
|
|
principals: [new iam.ServicePrincipal("config.amazonaws.com")],
|
|
actions: ["s3:GetBucketAcl", "s3:ListBucket"],
|
|
resources: [configBucket.bucketArn],
|
|
conditions: {
|
|
StringEquals: { "aws:SourceAccount": stack.account },
|
|
},
|
|
})
|
|
);
|
|
configBucket.addToResourcePolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "AWSConfigBucketDelivery",
|
|
effect: iam.Effect.ALLOW,
|
|
principals: [new iam.ServicePrincipal("config.amazonaws.com")],
|
|
actions: ["s3:PutObject"],
|
|
resources: [
|
|
configBucket.arnForObjects(`AWSLogs/${stack.account}/Config/*`),
|
|
],
|
|
conditions: {
|
|
StringEquals: {
|
|
"s3:x-amz-acl": "bucket-owner-full-control",
|
|
"aws:SourceAccount": stack.account,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
// Recorder role — assumed by Config. AWS_ConfigRole grants the read/describe
|
|
// permissions Config needs to record every resource type; the inline policy
|
|
// grants delivery to the bucket above. **This role is the Day 1 cross-review
|
|
// item (IAM change per CLAUDE.md).**
|
|
const recorderRole = new iam.Role(this, "ConfigRecorderRole", {
|
|
roleName: "seahaven-config-recorder-role",
|
|
assumedBy: new iam.ServicePrincipal("config.amazonaws.com"),
|
|
managedPolicies: [
|
|
iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWS_ConfigRole"),
|
|
],
|
|
});
|
|
recorderRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "ConfigDeliveryToBucket",
|
|
effect: iam.Effect.ALLOW,
|
|
actions: ["s3:PutObject"],
|
|
resources: [
|
|
configBucket.arnForObjects(`AWSLogs/${stack.account}/Config/*`),
|
|
],
|
|
conditions: {
|
|
StringEquals: { "s3:x-amz-acl": "bucket-owner-full-control" },
|
|
},
|
|
})
|
|
);
|
|
recorderRole.addToPolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "ConfigBucketAcl",
|
|
effect: iam.Effect.ALLOW,
|
|
actions: ["s3:GetBucketAcl"],
|
|
resources: [configBucket.bucketArn],
|
|
})
|
|
);
|
|
|
|
// NOTE — the Config recorder + delivery channel are provisioned via CLI,
|
|
// not CloudFormation. The L1 AWS::Config::ConfigurationRecorder is a
|
|
// stabilizing resource that will not reach CREATE_COMPLETE until recording
|
|
// is active, which needs a delivery channel; the delivery channel cannot be
|
|
// created until the recorder resource completes — a deadlock that hangs the
|
|
// stack indefinitely (observed 2026-06-01). The role + delivery bucket above
|
|
// stay in IaC (the role is the cross-reviewed IAM); the recorder/channel are
|
|
// created with the commands documented in the README, referencing this role
|
|
// ARN and bucket name (exported below).
|
|
|
|
new cdk.CfnOutput(this, "ConfigRecorderRoleArn", {
|
|
value: recorderRole.roleArn,
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// H-3 GuardDuty
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
new guardduty.CfnDetector(this, "GuardDutyDetector", {
|
|
enable: true,
|
|
findingPublishingFrequency: "FIFTEEN_MINUTES",
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// H-4 Security Hub (FSBP + CIS v3.0)
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// CIS/FSBP controls evaluate against the Config recording set up via CLI;
|
|
// no CFN dependency is needed (findings populate once Config is recording).
|
|
const hub = new securityhub.CfnHub(this, "SecurityHub", {
|
|
enableDefaultStandards: false,
|
|
controlFindingGenerator: "SECURITY_CONTROL",
|
|
autoEnableControls: true,
|
|
});
|
|
|
|
const fsbpArn = cdk.Arn.format(
|
|
{
|
|
service: "securityhub",
|
|
region: stack.region,
|
|
account: "",
|
|
resource: "standards",
|
|
resourceName: "aws-foundational-security-best-practices/v/1.0.0",
|
|
},
|
|
stack
|
|
);
|
|
const cisArn = cdk.Arn.format(
|
|
{
|
|
service: "securityhub",
|
|
region: stack.region,
|
|
account: "",
|
|
resource: "standards",
|
|
resourceName: "cis-aws-foundations-benchmark/v/3.0.0",
|
|
},
|
|
stack
|
|
);
|
|
|
|
const fsbp = new securityhub.CfnStandard(this, "StandardFSBP", {
|
|
standardsArn: fsbpArn,
|
|
});
|
|
fsbp.node.addDependency(hub);
|
|
|
|
const cis = new securityhub.CfnStandard(this, "StandardCIS", {
|
|
standardsArn: cisArn,
|
|
});
|
|
cis.node.addDependency(hub);
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// M-5 IAM Access Analyzer (free, account-scoped external-access)
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
new accessanalyzer.CfnAnalyzer(this, "AccountAnalyzer", {
|
|
analyzerName: "seahaven-account-analyzer",
|
|
type: "ACCOUNT",
|
|
});
|
|
|
|
new cdk.CfnOutput(this, "ConfigBucketName", {
|
|
value: configBucket.bucketName,
|
|
});
|
|
}
|
|
}
|