mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 04:33:15 +00:00
[INFRA-95] Shared DynamoDB CMK for sensitive finance/PII tables (M-3) (#21)
This commit is contained in:
parent
77d9d6c574
commit
ec620e4e79
2 changed files with 116 additions and 0 deletions
11
bin/app.ts
11
bin/app.ts
|
|
@ -5,6 +5,7 @@ import { AccountBaselineStack } from "../lib/account-baseline-stack";
|
|||
import { BackupOffsiteStack } from "../lib/backup-offsite-stack";
|
||||
import { BackupStack } from "../lib/backup-stack";
|
||||
import { RegionalBaselineStack } from "../lib/regional-baseline-stack";
|
||||
import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack";
|
||||
|
||||
const ACCOUNT = "328440206208";
|
||||
|
||||
|
|
@ -17,6 +18,16 @@ new AccountBaselineStack(app, "account-baseline", {
|
|||
budgetAlertEmail: "adam@seahavenind.com",
|
||||
});
|
||||
|
||||
// ── Shared DynamoDB CMK (INFRA-95 / M-3) ─────────────────────────────────────
|
||||
// Dedicated, standalone stack so the customer-managed key for sensitive
|
||||
// finance/PII DynamoDB tables is an independent shared dependency for the owning
|
||||
// app repos (payments-dashboard, procurement-ingest, exec-aide). Its ARN is
|
||||
// published to SSM (/seahaven/dynamodb/cmk-arn) for those stacks to consume.
|
||||
new DynamoDbCmkStack(app, "dynamodb-cmk", {
|
||||
stackName: "seahaven-dynamodb-cmk",
|
||||
env: { account: ACCOUNT, region: "us-east-1" },
|
||||
});
|
||||
|
||||
// ── Secondary-region baselines (INFRA-16, INFRA-91) ──────────────────────────
|
||||
// The us-east-1 baseline above is region-pinned by design. These stacks extend
|
||||
// a minimal detective/logging footprint into the secondary regions, codifying
|
||||
|
|
|
|||
105
lib/dynamodb-cmk-stack.ts
Normal file
105
lib/dynamodb-cmk-stack.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
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 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue