mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 06:53:17 +00:00
[INFRA-96] CMK-encrypt sensitive CloudWatch log groups (M-24) (#20)
* [INFRA-96] CMK-encrypt sensitive CloudWatch log groups (M-24) Add a dedicated customer-managed CMK (alias/seahaven-logs) for encrypting the sensitive CloudWatch Logs groups (CloudTrail + finance/PII Lambdas). - lib/logs-key.ts: LogsKey construct. Key policy grants the CloudWatch Logs service principal (logs.us-east-1.amazonaws.com) Encrypt*/Decrypt*/ ReEncrypt*/GenerateDataKey*/DescribeKey, scoped by the kms:EncryptionContext:aws:logs:arn condition (REQUIRED per AWS docs or log delivery breaks). Cross-reviewed (GPT-4.1): tightened Describe* -> DescribeKey; CreateGrant omitted (not needed for plain log-group encryption). - account-baseline-stack.ts: instantiate LogsKey and set KmsKeyId on the L2 Trail's CloudWatch log group in place (escape hatch on the existing AWS::Logs::LogGroup) so it keeps the same logical id + physical name - additive, no replacement, CIS Section-4 metric filters (which import the group by name) keep working, live audit trail not disrupted. Gated by context `encryptTrailLogGroup` so the CMK can be smoke-tested on a low-risk Lambda group before the most-sensitive CloudTrail group. Finance/PII Lambda log groups (exec-aide-*, payments-*, po-email-processor, vendor-reply-processor) are owned by other stacks and associated to this CMK via the CLI for now; codifying KmsKeyId in those repos is tracked as drift. * [INFRA-96] Document sensitive-logs CMK (M-24) in README
This commit is contained in:
parent
5002ed86d8
commit
77d9d6c574
3 changed files with 112 additions and 0 deletions
18
README.md
18
README.md
|
|
@ -179,6 +179,24 @@ this only affects the legacy `AWS-MonthlyBilling` CloudWatch alarm (L-8).
|
|||
| 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 |
|
||||
| Sensitive-logs CMK | `LogsKey/Key` (`alias/seahaven-logs`) | M-24 | Encrypts sensitive CloudWatch Logs groups. Key policy grants `logs.us-east-1.amazonaws.com` Encrypt*/Decrypt*/ReEncrypt*/GenerateDataKey*/DescribeKey scoped by `kms:EncryptionContext:aws:logs:arn` (required or log delivery breaks). Rotation on, RETAIN. Applied in place to `TrailLogGroup` via escape hatch (same logical id/name). Cross-reviewed |
|
||||
|
||||
**M-24 sensitive log groups:** `alias/seahaven-logs` encrypts the CloudTrail CW
|
||||
log group (codified here) plus the finance/PII Lambda groups owned by other
|
||||
stacks — `exec-aide-*`, `payments-*`, `po-email-processor`, `vendor-reply-processor`
|
||||
— which are associated via `aws logs associate-kms-key` and tracked as drift to
|
||||
codify in their owning repos. The CloudTrail group is encrypted in place (escape
|
||||
hatch on the existing `AWS::Logs::LogGroup`) so it is additive: same logical id +
|
||||
physical name, no replacement, CIS Section-4 metric filters keep working. A
|
||||
context flag `encryptTrailLogGroup` (default `true`) allows rolling the CMK out
|
||||
and smoke-testing it on a low-risk Lambda group before the CloudTrail group:
|
||||
|
||||
```bash
|
||||
# Phase 1: deploy CMK only, validate on a low-risk group
|
||||
cdk deploy account-baseline --context encryptTrailLogGroup=false
|
||||
# Phase 2: encrypt the CloudTrail group (default)
|
||||
cdk deploy account-baseline
|
||||
```
|
||||
|
||||
**H-1 log group:** the metric filters attach to the existing CloudTrail
|
||||
CloudWatch Logs group by name (`seahaven-account-baseline-TrailLogGroup4CBE3AF5-…`),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|||
import * as logs from "aws-cdk-lib/aws-logs";
|
||||
import * as cloudtrail from "aws-cdk-lib/aws-cloudtrail";
|
||||
import { Construct } from "constructs";
|
||||
import { LogsKey } from "./logs-key";
|
||||
import { DetectiveControls } from "./detective-controls";
|
||||
import { GovernanceToggles } from "./governance-toggles";
|
||||
import { BedrockLogging } from "./bedrock-logging";
|
||||
|
|
@ -177,6 +178,30 @@ export class AccountBaselineStack extends cdk.Stack {
|
|||
managementEvents: cloudtrail.ReadWriteType.ALL,
|
||||
});
|
||||
|
||||
// ── Sensitive CloudWatch Logs CMK (M-24 / INFRA-96) ──
|
||||
// Dedicated key for encrypting the CloudTrail CW log group and the
|
||||
// finance/PII Lambda log groups (those live in other stacks and are
|
||||
// associated via CLI until codified in their owning repos — see README).
|
||||
const logsKey = new LogsKey(this, "LogsKey");
|
||||
|
||||
// CMK-encrypt the Trail's CloudWatch Logs group in place. The L2 Trail
|
||||
// construct owns this group (path Trail/LogGroup) and does not expose an
|
||||
// encryptionKey prop for it, so we set KmsKeyId via escape hatch. This keeps
|
||||
// the same logical ID and physical name, so it is additive (no replacement)
|
||||
// and the CIS Section 4 metric filters that import the group by name (H-1)
|
||||
// keep working, and the live audit trail is never disrupted.
|
||||
//
|
||||
// Gated by context `encryptTrailLogGroup` (default true) so the CMK can be
|
||||
// rolled out and smoke-tested on a low-risk Lambda log group first, before
|
||||
// applying it to the most-sensitive CloudTrail group (INFRA-96 step 3).
|
||||
const encryptTrailLogGroup =
|
||||
this.node.tryGetContext("encryptTrailLogGroup") !== "false";
|
||||
if (encryptTrailLogGroup && trail.logGroup) {
|
||||
const cfnTrailLogGroup = trail.logGroup.node
|
||||
.defaultChild as logs.CfnLogGroup;
|
||||
cfnTrailLogGroup.kmsKeyId = logsKey.key.keyArn;
|
||||
}
|
||||
|
||||
// ── Day 1 detective layer + governance toggles ──
|
||||
// Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5).
|
||||
new DetectiveControls(this, "DetectiveControls");
|
||||
|
|
|
|||
69
lib/logs-key.ts
Normal file
69
lib/logs-key.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import * as kms from "aws-cdk-lib/aws-kms";
|
||||
import * as iam from "aws-cdk-lib/aws-iam";
|
||||
import { Construct } from "constructs";
|
||||
|
||||
/**
|
||||
* Dedicated customer-managed CMK for encrypting sensitive CloudWatch Logs
|
||||
* groups (audit M-24 / INFRA-96).
|
||||
*
|
||||
* Used by the account CloudTrail log group and the finance/PII Lambda log
|
||||
* groups (exec-aide, payments, po/vendor email processors, qbo). This is a
|
||||
* SEPARATE key from `alias/seahaven-alarm-topics` (SNS alarm topics) and
|
||||
* `alias/cloudtrail-logs` (the CloudTrail S3 log-file CMK) — those have
|
||||
* different service principals and encryption contexts.
|
||||
*
|
||||
* The key policy MUST grant the CloudWatch Logs service principal use of the
|
||||
* key, scoped by the `kms:EncryptionContext:aws:logs:arn` condition, or log
|
||||
* delivery to any CMK-encrypted group silently stops. (AWS docs: "Encrypt log
|
||||
* data in CloudWatch Logs using AWS KMS".) Cross-reviewed 2026-06-08
|
||||
* (INFRA-96): dropped `Describe*` to the specific `DescribeKey`; CreateGrant is
|
||||
* not required for plain log-group encryption so it is omitted.
|
||||
*/
|
||||
export class LogsKey extends Construct {
|
||||
public readonly key: kms.Key;
|
||||
|
||||
constructor(scope: Construct, id: string) {
|
||||
super(scope, id);
|
||||
|
||||
const stack = cdk.Stack.of(this);
|
||||
|
||||
this.key = new kms.Key(this, "Key", {
|
||||
alias: "seahaven-logs",
|
||||
description:
|
||||
"Encrypts sensitive CloudWatch Logs groups (CloudTrail + finance/PII Lambdas) — M-24/INFRA-96",
|
||||
enableKeyRotation: true,
|
||||
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||
});
|
||||
|
||||
// CloudWatch Logs service principal must be able to use the key to deliver
|
||||
// (encrypt) and read (decrypt) log events. The encryption-context condition
|
||||
// binds the grant to this account's log groups only, so the key cannot be
|
||||
// used to decrypt arbitrary data under the logs service principal.
|
||||
this.key.addToResourcePolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "AllowCloudWatchLogsUseOfKey",
|
||||
effect: iam.Effect.ALLOW,
|
||||
principals: [
|
||||
new iam.ServicePrincipal(`logs.${stack.region}.amazonaws.com`),
|
||||
],
|
||||
actions: [
|
||||
"kms:Encrypt*",
|
||||
"kms:Decrypt*",
|
||||
"kms:ReEncrypt*",
|
||||
"kms:GenerateDataKey*",
|
||||
"kms:DescribeKey",
|
||||
],
|
||||
resources: ["*"],
|
||||
conditions: {
|
||||
ArnLike: {
|
||||
"kms:EncryptionContext:aws:logs:arn": `arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:*`,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
new cdk.CfnOutput(this, "LogsKeyArn", { value: this.key.keyArn });
|
||||
new cdk.CfnOutput(this, "LogsKeyAlias", { value: "alias/seahaven-logs" });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue