mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +00:00
105 lines
4.6 KiB
TypeScript
105 lines
4.6 KiB
TypeScript
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 * as ssm from "aws-cdk-lib/aws-ssm";
|
|
import { Construct } from "constructs";
|
|
|
|
/**
|
|
* Shared customer-managed CMK for sensitive DynamoDB tables (INFRA-95 / M-3).
|
|
*
|
|
* Replaces the default AWS-owned key on tables holding FINANCIAL / PII data so
|
|
* that the encryption key is account-controlled, rotated, and auditable:
|
|
* `PaymentsDashboard`, `purchase-orders`, `exec-aide`, `WorkOrders`,
|
|
* `WorkOrderComments`.
|
|
*
|
|
* Lives in its OWN CloudFormation stack (not the account-baseline stack) so the
|
|
* key is an independent, shared dependency for three separate owning repos
|
|
* (payments-dashboard SAM, procurement-ingest CDK, exec-aide CDK) and so its
|
|
* deploys never contend with the account-baseline stack.
|
|
*
|
|
* Key-policy design (cross-reviewed by GPT-4.1, 2026-06-08):
|
|
* - The consuming Lambda/Fargate roles carry CFN hash suffixes that change on
|
|
* replacement, and `purchase-orders` has CROSS-STACK readers (seahaven-bot's
|
|
* po-sync + wo-po-lookup). Hardcoding role ARNs in the key policy would be
|
|
* fragile and would silently break access on any role replacement.
|
|
* - Instead this uses the delegation-to-IAM pattern: the key policy authorizes
|
|
* the whole account to use the key, but ONLY when the request reaches KMS via
|
|
* DynamoDB in us-east-1 (`kms:ViaService`). Actual authZ is then gated by each
|
|
* consumer role's identity policy, which must separately grant
|
|
* `kms:Decrypt`/`kms:GenerateDataKey`/`kms:DescribeKey` on this CMK ARN.
|
|
* - `kms:CreateGrant` is in the resource policy because DynamoDB SSE-KMS
|
|
* operates through a grant: when a table is associated with the CMK, DynamoDB
|
|
* calls CreateGrant on behalf of the deploy principal. The deploy roles also
|
|
* need `kms:CreateGrant` in their identity policy — the CDK exec role has
|
|
* AdministratorAccess; the SAM `github-cfn-execution-role` was granted it
|
|
* (scoped `kms:GrantIsForAWSResource:true`) for the PaymentsDashboard
|
|
* conversion.
|
|
* - `kms:CallerAccount` is kept as cheap defense-in-depth against a future
|
|
* cross-account confused-deputy on the same key.
|
|
* - `kms:ReEncrypt*` deliberately omitted — DynamoDB SSE-KMS never calls it
|
|
* (uses GenerateDataKey + Decrypt); CMK rotation re-encryption is handled by
|
|
* AWS via the grant.
|
|
*
|
|
* The key ARN is published to SSM (`/seahaven/dynamodb/cmk-arn`) so consumer
|
|
* stacks in other repos can resolve it without a hard CFN cross-stack export.
|
|
*
|
|
* removalPolicy RETAIN — deleting this CMK while any table still has data
|
|
* encrypted under it would make that data permanently unrecoverable.
|
|
*/
|
|
export class DynamoDbCmkStack extends cdk.Stack {
|
|
public readonly key: kms.Key;
|
|
|
|
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
|
super(scope, id, props);
|
|
|
|
const account = this.account;
|
|
const region = this.region;
|
|
|
|
this.key = new kms.Key(this, "Key", {
|
|
alias: "seahaven-dynamodb",
|
|
description:
|
|
"SSE for sensitive DynamoDB tables (PaymentsDashboard, purchase-orders, exec-aide, WorkOrders, WorkOrderComments) — INFRA-95/M-3",
|
|
enableKeyRotation: true,
|
|
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
});
|
|
|
|
// Account-wide use of the key, but ONLY via DynamoDB in this region. The
|
|
// per-role identity grants (in each consumer stack) are what actually scope
|
|
// which principals can read/write the encrypted tables.
|
|
this.key.addToResourcePolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "AllowDynamoDbSSEViaService",
|
|
effect: iam.Effect.ALLOW,
|
|
principals: [new iam.AccountRootPrincipal()],
|
|
actions: [
|
|
"kms:Encrypt",
|
|
"kms:Decrypt",
|
|
"kms:GenerateDataKey*",
|
|
"kms:DescribeKey",
|
|
"kms:CreateGrant",
|
|
],
|
|
resources: ["*"],
|
|
conditions: {
|
|
StringEquals: {
|
|
"kms:ViaService": `dynamodb.${region}.amazonaws.com`,
|
|
"kms:CallerAccount": account,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
new ssm.StringParameter(this, "CmkArnParam", {
|
|
parameterName: "/seahaven/dynamodb/cmk-arn",
|
|
stringValue: this.key.keyArn,
|
|
description:
|
|
"ARN of the shared customer-managed CMK for sensitive DynamoDB tables (INFRA-95/M-3)",
|
|
});
|
|
|
|
cdk.Tags.of(this).add("Project", "account-baseline");
|
|
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
|
|
cdk.Tags.of(this).add("Environment", "prod");
|
|
cdk.Tags.of(this).add("ManagedBy", "cdk");
|
|
|
|
new cdk.CfnOutput(this, "DynamoDbCmkArn", { value: this.key.keyArn });
|
|
}
|
|
}
|