mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 06:53:17 +00:00
Add monitoring + logging layer (audit Day 2: H-1/H-14/M-13) (#6)
- H-1: 15 CIS Section 4 metric filters (4.1-4.15) on the CloudTrail log group, each alarming to a new SSE SNS topic seahaven-cis-alarms (email to adam). ALARM-only actions per Sea Haven preference. 4.16 = Security Hub (Day 1). - H-14: VPC flow logs (ALL traffic) on all 5 VPCs → hardened S3 bucket. Delivery bucket policy cross-reviewed; kept the AWS-required s3:x-amz-acl condition + logs:*:* source-ARN (cross-reviewer wrongly flagged these; verified against AWS flow-logs-s3-permissions docs), dropped the unneeded s3:ListBucket. - M-13: SES configuration set seahaven-email-events capturing bounce/complaint/ reject to CloudWatch for reputation visibility. L-4 (log retention) and L-5 (alarm action) applied via CLI, documented in README.
This commit is contained in:
parent
38d4a5753a
commit
3ba90ddc40
5 changed files with 412 additions and 3 deletions
48
README.md
48
README.md
|
|
@ -138,12 +138,54 @@ Billing Alerts* under Billing → Billing preferences; there is no public API/CL
|
||||||
The M-10 budget already provides cost alerting independent of that metric, so
|
The M-10 budget already provides cost alerting independent of that metric, so
|
||||||
this only affects the legacy `AWS-MonthlyBilling` CloudWatch alarm (L-8).
|
this only affects the legacy `AWS-MonthlyBilling` CloudWatch alarm (L-8).
|
||||||
|
|
||||||
|
### Monitoring + logging (audit Day 2)
|
||||||
|
|
||||||
|
| Resource | Logical ID | Finding | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| CIS metric filters + alarms | `CisMonitoring/*` | H-1 | 15 filters (CIS 4.1–4.15) on the CloudTrail log group, each with an alarm → `seahaven-cis-alarms`. ALARM-only actions (no OK). 4.16 = Security Hub (Day 1) |
|
||||||
|
| CIS alarm topic | `seahaven-cis-alarms` | H-1 | SNS, SSE (`alias/aws/sns`), email sub to adam@seahavenind.com |
|
||||||
|
| VPC flow logs | `FlowLogs/FlowLog0..4` | H-14 | ALL traffic on all 5 VPCs → S3 |
|
||||||
|
| Flow-logs bucket | `seahaven-vpc-flow-logs-328440206208` | H-14 | Private, SSE-S3, TLS-only, Glacier @90d / expire @365d; delivery bucket policy cross-reviewed |
|
||||||
|
| SES config set | `seahaven-email-events` | M-13 | Bounce/complaint/reject → CloudWatch metrics for reputation visibility |
|
||||||
|
|
||||||
|
**H-1 log group:** the metric filters attach to the existing CloudTrail
|
||||||
|
CloudWatch Logs group by name (`seahaven-account-baseline-TrailLogGroup4CBE3AF5-…`),
|
||||||
|
imported read-only so the live audit trail is never replaced. Stable unless the
|
||||||
|
Trail is recreated.
|
||||||
|
|
||||||
|
**H-14 bucket policy note:** the flow-logs delivery policy keeps
|
||||||
|
`s3:x-amz-acl=bucket-owner-full-control` and the `arn:aws:logs:…:*` source-ARN
|
||||||
|
wildcard — both are required by AWS's documented flow-logs-to-S3 policy
|
||||||
|
(`flow-logs-s3-permissions.html`). A cross-review suggested dropping them; that
|
||||||
|
was rejected as it would break delivery. `s3:ListBucket` was dropped (not needed).
|
||||||
|
|
||||||
|
**M-13 follow-up:** associate `seahaven-email-events` as the default config set
|
||||||
|
on the live sending identities to capture events from existing senders:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws sesv2 put-email-identity-configuration-set-attributes \
|
||||||
|
--email-identity int.seahaven.com --configuration-set-name seahaven-email-events
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log-group retention + alarm wiring (audit L-4, L-5)
|
||||||
|
|
||||||
|
Applied via CLI (auto-created groups spread across stacks; one alarm in another
|
||||||
|
stack). Applied 2026-06-02.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# L-4 90-day retention on the 13 never-expire log groups (CodeBuild + CDK helpers)
|
||||||
|
for lg in <the 13 groups>; do aws logs put-retention-policy --log-group-name "$lg" --retention-in-days 90; done
|
||||||
|
|
||||||
|
# L-5 wire the actionless forgejo backup-verification alarm to site-alerts
|
||||||
|
aws cloudwatch put-metric-alarm --alarm-name forgejo-backup-verification-errors \
|
||||||
|
--alarm-actions arn:aws:sns:us-east-1:328440206208:site-alerts # (preserve existing alarm config)
|
||||||
|
```
|
||||||
|
|
||||||
## Roadmap (same stack)
|
## Roadmap (same stack)
|
||||||
|
|
||||||
Detective layer multi-region expansion (GuardDuty/Config/Security Hub beyond
|
Detective layer multi-region expansion (GuardDuty/Config/Security Hub beyond
|
||||||
us-east-1). H-1: CIS Section 4 metric filters/alarms onto the CloudTrail log
|
us-east-1). Backup phase 2: expand past the phase-1 set via tag-based selection
|
||||||
group. Backup phase 2: expand past the phase-1 set via tag-based selection and
|
and graduate the offsite vault to compliance mode.
|
||||||
graduate the offsite vault to compliance mode.
|
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ import * as cloudtrail from "aws-cdk-lib/aws-cloudtrail";
|
||||||
import { Construct } from "constructs";
|
import { Construct } from "constructs";
|
||||||
import { DetectiveControls } from "./detective-controls";
|
import { DetectiveControls } from "./detective-controls";
|
||||||
import { GovernanceToggles } from "./governance-toggles";
|
import { GovernanceToggles } from "./governance-toggles";
|
||||||
|
import { CisMonitoring } from "./cis-monitoring";
|
||||||
|
import { FlowLogs } from "./flow-logs";
|
||||||
|
import { SesMonitoring } from "./ses-monitoring";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account-level security baseline for Sea Haven (account 328440206208).
|
* Account-level security baseline for Sea Haven (account 328440206208).
|
||||||
|
|
@ -143,6 +146,15 @@ export class AccountBaselineStack extends cdk.Stack {
|
||||||
alertEmail: props.budgetAlertEmail,
|
alertEmail: props.budgetAlertEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Day 2 monitoring + logging ──
|
||||||
|
// CIS Section 4 metric filters/alarms (H-1), VPC flow logs (H-14),
|
||||||
|
// SES bounce/complaint config set (M-13).
|
||||||
|
new CisMonitoring(this, "CisMonitoring", {
|
||||||
|
alarmEmail: props.budgetAlertEmail,
|
||||||
|
});
|
||||||
|
new FlowLogs(this, "FlowLogs");
|
||||||
|
new SesMonitoring(this, "SesMonitoring");
|
||||||
|
|
||||||
cdk.Tags.of(this).add("Project", "account-baseline");
|
cdk.Tags.of(this).add("Project", "account-baseline");
|
||||||
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
|
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
|
||||||
cdk.Tags.of(this).add("Environment", "prod");
|
cdk.Tags.of(this).add("Environment", "prod");
|
||||||
|
|
|
||||||
197
lib/cis-monitoring.ts
Normal file
197
lib/cis-monitoring.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import * as logs from "aws-cdk-lib/aws-logs";
|
||||||
|
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
|
||||||
|
import * as cwactions from "aws-cdk-lib/aws-cloudwatch-actions";
|
||||||
|
import * as sns from "aws-cdk-lib/aws-sns";
|
||||||
|
import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
|
||||||
|
import * as kms from "aws-cdk-lib/aws-kms";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CIS AWS Foundations Benchmark v3.0 Section 4 — Monitoring (audit H-1).
|
||||||
|
*
|
||||||
|
* 15 metric filters on the account CloudTrail log group, each backed by a
|
||||||
|
* CloudWatch alarm that notifies a dedicated SNS topic. Closes CIS 4.1–4.15.
|
||||||
|
* (4.16 "Security Hub enabled" is not a metric filter — done Day 1, H-4.)
|
||||||
|
*
|
||||||
|
* Alarms fire on ALARM only (no OK/recovery actions) per Sea Haven preference.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The account CloudTrail (C-1) delivers to this CloudWatch Logs group. It is
|
||||||
|
// created by the L2 cloudtrail.Trail in account-baseline-stack.ts; we import it
|
||||||
|
// by name rather than replace it, so the live audit trail is never disrupted.
|
||||||
|
// Stable as long as the Trail is not recreated.
|
||||||
|
const TRAIL_LOG_GROUP_NAME =
|
||||||
|
"seahaven-account-baseline-TrailLogGroup4CBE3AF5-e7hMDCzj8e4d";
|
||||||
|
|
||||||
|
interface CisControl {
|
||||||
|
readonly id: string;
|
||||||
|
readonly metricName: string;
|
||||||
|
readonly pattern: string;
|
||||||
|
readonly description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CIS_CONTROLS: CisControl[] = [
|
||||||
|
{
|
||||||
|
id: "UnauthorizedApiCalls",
|
||||||
|
metricName: "UnauthorizedAPICalls",
|
||||||
|
pattern:
|
||||||
|
'{ ($.errorCode = "*UnauthorizedOperation") || ($.errorCode = "AccessDenied*") && ($.sourceIPAddress != "delivery.logs.amazonaws.com") && ($.eventName != "HeadBucket") }',
|
||||||
|
description: "CIS 4.1 — unauthorized API calls",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ConsoleSigninNoMfa",
|
||||||
|
metricName: "ConsoleSigninWithoutMFA",
|
||||||
|
pattern:
|
||||||
|
'{ ($.eventName = "ConsoleLogin") && ($.additionalEventData.MFAUsed != "Yes") && ($.userIdentity.type = "IAMUser") && ($.responseElements.ConsoleLogin = "Success") }',
|
||||||
|
description: "CIS 4.2 — console sign-in without MFA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "RootAccountUsage",
|
||||||
|
metricName: "RootAccountUsage",
|
||||||
|
pattern:
|
||||||
|
'{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }',
|
||||||
|
description: "CIS 4.3 — root account usage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "IamPolicyChanges",
|
||||||
|
metricName: "IAMPolicyChanges",
|
||||||
|
pattern:
|
||||||
|
"{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}",
|
||||||
|
description: "CIS 4.4 — IAM policy changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "CloudTrailConfigChanges",
|
||||||
|
metricName: "CloudTrailConfigChanges",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }",
|
||||||
|
description: "CIS 4.5 — CloudTrail configuration changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ConsoleAuthFailures",
|
||||||
|
metricName: "ConsoleAuthenticationFailures",
|
||||||
|
pattern:
|
||||||
|
'{ ($.eventName = ConsoleLogin) && ($.errorMessage = "Failed authentication") }',
|
||||||
|
description: "CIS 4.6 — console authentication failures",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "CmkDisableOrDelete",
|
||||||
|
metricName: "CMKDisableOrScheduledDelete",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventSource = kms.amazonaws.com) && (($.eventName = DisableKey) || ($.eventName = ScheduleKeyDeletion)) }",
|
||||||
|
description: "CIS 4.7 — disabling or scheduled deletion of CMKs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "S3BucketPolicyChanges",
|
||||||
|
metricName: "S3BucketPolicyChanges",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }",
|
||||||
|
description: "CIS 4.8 — S3 bucket policy changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ConfigChanges",
|
||||||
|
metricName: "AWSConfigChanges",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventSource = config.amazonaws.com) && (($.eventName=StopConfigurationRecorder)||($.eventName=DeleteDeliveryChannel)||($.eventName=PutDeliveryChannel)||($.eventName=PutConfigurationRecorder)) }",
|
||||||
|
description: "CIS 4.9 — AWS Config configuration changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SecurityGroupChanges",
|
||||||
|
metricName: "SecurityGroupChanges",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }",
|
||||||
|
description: "CIS 4.10 — security group changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "NaclChanges",
|
||||||
|
metricName: "NetworkACLChanges",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }",
|
||||||
|
description: "CIS 4.11 — network ACL changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "NetworkGatewayChanges",
|
||||||
|
metricName: "NetworkGatewayChanges",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventName = CreateCustomerGateway) || ($.eventName = DeleteCustomerGateway) || ($.eventName = AttachInternetGateway) || ($.eventName = CreateInternetGateway) || ($.eventName = DeleteInternetGateway) || ($.eventName = DetachInternetGateway) }",
|
||||||
|
description: "CIS 4.12 — network gateway changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "RouteTableChanges",
|
||||||
|
metricName: "RouteTableChanges",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventName = CreateRoute) || ($.eventName = CreateRouteTable) || ($.eventName = ReplaceRoute) || ($.eventName = ReplaceRouteTableAssociation) || ($.eventName = DeleteRouteTable) || ($.eventName = DeleteRoute) || ($.eventName = DisassociateRouteTable) }",
|
||||||
|
description: "CIS 4.13 — route table changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "VpcChanges",
|
||||||
|
metricName: "VPCChanges",
|
||||||
|
pattern:
|
||||||
|
"{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }",
|
||||||
|
description: "CIS 4.14 — VPC changes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "OrganizationsChanges",
|
||||||
|
metricName: "OrganizationsChanges",
|
||||||
|
pattern:
|
||||||
|
'{ ($.eventSource = organizations.amazonaws.com) && (($.eventName = "AcceptHandshake") || ($.eventName = "AttachPolicy") || ($.eventName = "CreateAccount") || ($.eventName = "CreateOrganizationalUnit") || ($.eventName = "CreatePolicy") || ($.eventName = "DeclineHandshake") || ($.eventName = "DeleteOrganization") || ($.eventName = "DeleteOrganizationalUnit") || ($.eventName = "DeletePolicy") || ($.eventName = "DetachPolicy") || ($.eventName = "DisablePolicyType") || ($.eventName = "EnablePolicyType") || ($.eventName = "InviteAccountToOrganization") || ($.eventName = "LeaveOrganization") || ($.eventName = "MoveAccount") || ($.eventName = "RemoveAccountFromOrganization") || ($.eventName = "UpdatePolicy") || ($.eventName = "UpdateOrganizationalUnit")) }',
|
||||||
|
description: "CIS 4.15 — AWS Organizations changes",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface CisMonitoringProps {
|
||||||
|
/** Email subscribed to the CIS alarm topic. */
|
||||||
|
readonly alarmEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CisMonitoring extends Construct {
|
||||||
|
constructor(scope: Construct, id: string, props: CisMonitoringProps) {
|
||||||
|
super(scope, id);
|
||||||
|
|
||||||
|
// Dedicated topic for security/CIS alarms, encrypted with the AWS-managed
|
||||||
|
// SNS key (also clears audit L-14 for this topic).
|
||||||
|
const topic = new sns.Topic(this, "CisAlarmTopic", {
|
||||||
|
topicName: "seahaven-cis-alarms",
|
||||||
|
displayName: "Sea Haven CIS / security alarms",
|
||||||
|
masterKey: kms.Alias.fromAliasName(this, "SnsKey", "alias/aws/sns"),
|
||||||
|
});
|
||||||
|
topic.addSubscription(new subscriptions.EmailSubscription(props.alarmEmail));
|
||||||
|
|
||||||
|
const logGroup = logs.LogGroup.fromLogGroupName(
|
||||||
|
this,
|
||||||
|
"TrailLogGroup",
|
||||||
|
TRAIL_LOG_GROUP_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const c of CIS_CONTROLS) {
|
||||||
|
const mf = new logs.MetricFilter(this, `${c.id}Filter`, {
|
||||||
|
logGroup,
|
||||||
|
filterPattern: logs.FilterPattern.literal(c.pattern),
|
||||||
|
metricNamespace: "CISBenchmark",
|
||||||
|
metricName: c.metricName,
|
||||||
|
metricValue: "1",
|
||||||
|
defaultValue: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const alarm = mf
|
||||||
|
.metric({
|
||||||
|
statistic: "Sum",
|
||||||
|
period: cdk.Duration.minutes(5),
|
||||||
|
})
|
||||||
|
.createAlarm(this, `${c.id}Alarm`, {
|
||||||
|
alarmName: `cis-${c.metricName}`,
|
||||||
|
alarmDescription: c.description,
|
||||||
|
threshold: 1,
|
||||||
|
comparisonOperator:
|
||||||
|
cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
||||||
|
evaluationPeriods: 1,
|
||||||
|
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ALARM-only notification (no OK/recovery action) per Sea Haven preference.
|
||||||
|
alarm.addAlarmAction(new cwactions.SnsAction(topic));
|
||||||
|
}
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "CisAlarmTopicArn", { value: topic.topicArn });
|
||||||
|
}
|
||||||
|
}
|
||||||
108
lib/flow-logs.ts
Normal file
108
lib/flow-logs.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
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 ec2 from "aws-cdk-lib/aws-ec2";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VPC flow logs for every VPC, delivered to a hardened S3 bucket (audit H-14,
|
||||||
|
* CIS 3.9/5.6). S3 destination (not CloudWatch Logs) for cost — query
|
||||||
|
* forensically via Athena. ALL traffic (accept + reject).
|
||||||
|
*
|
||||||
|
* S3 delivery needs no IAM role; instead the bucket policy grants the
|
||||||
|
* `delivery.logs.amazonaws.com` service principal write access, scoped to this
|
||||||
|
* account. That bucket policy is the Day 2 cross-review item.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7.
|
||||||
|
const VPC_IDS = [
|
||||||
|
"vpc-061d66990b6a4d1fb",
|
||||||
|
"vpc-0542a9e934b417d23",
|
||||||
|
"vpc-062d200c68bd4ca0e",
|
||||||
|
"vpc-0d3d4b67bd0cf8a68",
|
||||||
|
"vpc-02c10a89d66f6f9b8",
|
||||||
|
];
|
||||||
|
|
||||||
|
export class FlowLogs extends Construct {
|
||||||
|
constructor(scope: Construct, id: string) {
|
||||||
|
super(scope, id);
|
||||||
|
|
||||||
|
const stack = cdk.Stack.of(this);
|
||||||
|
|
||||||
|
const bucket = new s3.Bucket(this, "FlowLogsBucket", {
|
||||||
|
bucketName: `seahaven-vpc-flow-logs-${stack.account}`,
|
||||||
|
encryption: s3.BucketEncryption.S3_MANAGED,
|
||||||
|
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
||||||
|
enforceSSL: true,
|
||||||
|
versioned: false,
|
||||||
|
lifecycleRules: [
|
||||||
|
{
|
||||||
|
id: "transition-and-expire",
|
||||||
|
transitions: [
|
||||||
|
{
|
||||||
|
storageClass: s3.StorageClass.GLACIER,
|
||||||
|
transitionAfter: cdk.Duration.days(90),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expiration: cdk.Duration.days(365),
|
||||||
|
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log-delivery service permissions (scoped to this account) — the standard
|
||||||
|
// VPC-flow-logs-to-S3 bucket policy.
|
||||||
|
bucket.addToResourcePolicy(
|
||||||
|
new iam.PolicyStatement({
|
||||||
|
sid: "AWSLogDeliveryWrite",
|
||||||
|
effect: iam.Effect.ALLOW,
|
||||||
|
principals: [new iam.ServicePrincipal("delivery.logs.amazonaws.com")],
|
||||||
|
actions: ["s3:PutObject"],
|
||||||
|
resources: [bucket.arnForObjects(`AWSLogs/${stack.account}/*`)],
|
||||||
|
conditions: {
|
||||||
|
StringEquals: {
|
||||||
|
"s3:x-amz-acl": "bucket-owner-full-control",
|
||||||
|
"aws:SourceAccount": stack.account,
|
||||||
|
},
|
||||||
|
ArnLike: {
|
||||||
|
"aws:SourceArn": `arn:${stack.partition}:logs:${stack.region}:${stack.account}:*`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
bucket.addToResourcePolicy(
|
||||||
|
new iam.PolicyStatement({
|
||||||
|
sid: "AWSLogDeliveryAclCheck",
|
||||||
|
effect: iam.Effect.ALLOW,
|
||||||
|
principals: [new iam.ServicePrincipal("delivery.logs.amazonaws.com")],
|
||||||
|
// AWS's documented flow-logs-to-S3 policy uses GetBucketAcl only
|
||||||
|
// (verified against flow-logs-s3-permissions.html); ListBucket is not
|
||||||
|
// needed and would be over-permissioned.
|
||||||
|
actions: ["s3:GetBucketAcl"],
|
||||||
|
resources: [bucket.bucketArn],
|
||||||
|
conditions: {
|
||||||
|
StringEquals: { "aws:SourceAccount": stack.account },
|
||||||
|
ArnLike: {
|
||||||
|
"aws:SourceArn": `arn:${stack.partition}:logs:${stack.region}:${stack.account}:*`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
VPC_IDS.forEach((vpcId, i) => {
|
||||||
|
const flowLog = new ec2.CfnFlowLog(this, `FlowLog${i}`, {
|
||||||
|
resourceId: vpcId,
|
||||||
|
resourceType: "VPC",
|
||||||
|
trafficType: "ALL",
|
||||||
|
logDestinationType: "s3",
|
||||||
|
logDestination: bucket.bucketArn,
|
||||||
|
maxAggregationInterval: 600,
|
||||||
|
tags: [{ key: "Name", value: `flow-log-${vpcId}` }],
|
||||||
|
});
|
||||||
|
flowLog.node.addDependency(bucket.policy!);
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "FlowLogsBucketName", { value: bucket.bucketName });
|
||||||
|
}
|
||||||
|
}
|
||||||
50
lib/ses-monitoring.ts
Normal file
50
lib/ses-monitoring.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import * as ses from "aws-cdk-lib/aws-ses";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SES configuration set capturing bounce/complaint events (audit M-13).
|
||||||
|
*
|
||||||
|
* Gives reputation visibility beyond the suppression list by emitting bounce,
|
||||||
|
* complaint, and reject events to CloudWatch metrics (dimensioned by config
|
||||||
|
* set). Associating this set as the default on the live sending identities is a
|
||||||
|
* follow-up CLI step (documented in the README) — creating it here does not
|
||||||
|
* change current sending behaviour.
|
||||||
|
*/
|
||||||
|
export class SesMonitoring extends Construct {
|
||||||
|
constructor(scope: Construct, id: string) {
|
||||||
|
super(scope, id);
|
||||||
|
|
||||||
|
const configSetName = "seahaven-email-events";
|
||||||
|
|
||||||
|
const configSet = new ses.CfnConfigurationSet(this, "ConfigSet", {
|
||||||
|
name: configSetName,
|
||||||
|
reputationOptions: { reputationMetricsEnabled: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventDest = new ses.CfnConfigurationSetEventDestination(
|
||||||
|
this,
|
||||||
|
"BounceComplaintDest",
|
||||||
|
{
|
||||||
|
configurationSetName: configSetName,
|
||||||
|
eventDestination: {
|
||||||
|
name: "bounce-complaint-cw",
|
||||||
|
enabled: true,
|
||||||
|
matchingEventTypes: ["bounce", "complaint", "reject"],
|
||||||
|
cloudWatchDestination: {
|
||||||
|
dimensionConfigurations: [
|
||||||
|
{
|
||||||
|
defaultDimensionValue: "none",
|
||||||
|
dimensionName: "ses:configuration-set",
|
||||||
|
dimensionValueSource: "messageTag",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
eventDest.node.addDependency(configSet);
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "SesConfigSetName", { value: configSetName });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue