From 0fe4faaf4fd370b83efefcc0b1aa9271380918d8 Mon Sep 17 00:00:00 2001 From: Omer C <639682+omercnet@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:42:59 +0200 Subject: [PATCH] feat: add encryption at rest for Control Tower compliance Fixes #61 - Enable SQS managed SSE on IssueDetectionQueue and BillingQueue - Add KMS encryption to IssueStream (Kinesis) - Add AES256 server-side encryption to Storage and PublicStorage buckets - Add optional KMS key parameter for CloudFormation LogGroup encryption --- infra/billing.ts | 5 +++++ infra/issues.ts | 32 +++++++++++++++++++++++++++++++- infra/storage.ts | 16 ++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/infra/billing.ts b/infra/billing.ts index 646620d4..3c5b645f 100644 --- a/infra/billing.ts +++ b/infra/billing.ts @@ -5,6 +5,11 @@ import { allSecrets, assumable } from "./secret"; const queue = new sst.aws.Queue("BillingQueue", { fifo: true, visibilityTimeout: "180 seconds", + transform: { + queue: { + sqsManagedSseEnabled: true, + }, + }, }); queue.subscribe( diff --git a/infra/issues.ts b/infra/issues.ts index 1c603d2f..76876c47 100644 --- a/infra/issues.ts +++ b/infra/issues.ts @@ -10,13 +10,25 @@ import { multiregion, regions } from "./regions"; export const issueDetectionQueue = new sst.aws.Queue("IssueDetectionQueue", { fifo: true, visibilityTimeout: "5 minutes", + transform: { + queue: { + sqsManagedSseEnabled: true, + }, + }, }); issueDetectionQueue.subscribe({ handler: "packages/backend/src/function/issues/detected.handler", link: [database, email], }); -const stream = new sst.aws.KinesisStream("IssueStream"); +const stream = new sst.aws.KinesisStream("IssueStream", { + transform: { + stream: { + encryptionType: "KMS", + kmsKeyId: "alias/aws/kinesis", + }, + }, +}); stream.subscribe( "IssueStreamSubscriber", { @@ -123,6 +135,17 @@ const cfnTemplate = $jsonStringify({ Type: "String", Description: "The template URL", }, + logGroupKmsKeyArn: { + Type: "String", + Default: "", + Description: + "Optional KMS key ARN for encrypting CloudWatch logs at rest. Leave empty to disable encryption.", + }, + }, + Conditions: { + HasLogGroupKmsKey: { + "Fn::Not": [{ "Fn::Equals": [{ Ref: "logGroupKmsKeyArn" }, ""] }], + }, }, Resources: { SubscriberRole: { @@ -213,6 +236,13 @@ const cfnTemplate = $jsonStringify({ "Fn::Sub": "/aws/lambda/sst-console-issue-${workspaceID}", }, RetentionInDays: 1, + KmsKeyId: { + "Fn::If": [ + "HasLogGroupKmsKey", + { Ref: "logGroupKmsKeyArn" }, + { Ref: "AWS::NoValue" }, + ], + }, }, }, SubscriberPermission: { diff --git a/infra/storage.ts b/infra/storage.ts index 50ec9569..defdd1af 100644 --- a/infra/storage.ts +++ b/infra/storage.ts @@ -8,6 +8,15 @@ export const storage = new sst.aws.Bucket("Storage", { ignorePublicAcls: false, restrictPublicBuckets: false, }, + bucket: { + serverSideEncryptionConfiguration: { + rule: { + applyServerSideEncryptionByDefault: { + sseAlgorithm: "AES256", + }, + }, + }, + }, }, }); @@ -73,6 +82,13 @@ export const publicStorage = multiregion((region, provider) => { transform: { bucket(args, opts) { args.bucket = `sst-public-${$app.stage}-${region}`; + args.serverSideEncryptionConfiguration = { + rule: { + applyServerSideEncryptionByDefault: { + sseAlgorithm: "AES256", + }, + }, + }; // opts.import = args.bucket; // opts.ignoreChanges = ["*"]; },