From c19c7fda72da9c26e96de89f67b3d48655fdc47c Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:54:10 -0400 Subject: [PATCH] fix(infra): send proposals from seahaven.com domain identity (primary SES domain) (#230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the API Lambda's SES sender from the email identity proposals@seahavenind.com to proposals@seahaven.com under the seahaven.com DOMAIN identity — the org's primary sending domain for all workload stacks. The domain identity is account-shared and managed out-of-band in seahaven-prod (Easy-DKIM verified in the seahaven.com Route53 zone; production-access request submitted), so this stack references its ARN instead of creating an EmailIdentity (avoids per-stack duplication + cross-account DNS at deploy). Send scoped to identity/seahaven.com with a required ses:FromAddress condition. GPT-4.1 cross-review APPROVE. Drops the unused aws-ses import. Follow-up: codify the seahaven.com identity in the seahaven-prod account baseline. --- infra/lib/compute-stack.ts | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/infra/lib/compute-stack.ts b/infra/lib/compute-stack.ts index 867d38c..c0b7230 100644 --- a/infra/lib/compute-stack.ts +++ b/infra/lib/compute-stack.ts @@ -5,7 +5,6 @@ import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2'; import * as apigatewayv2Authorizers from 'aws-cdk-lib/aws-apigatewayv2-authorizers'; import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; import * as iam from 'aws-cdk-lib/aws-iam'; -import * as ses from 'aws-cdk-lib/aws-ses'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as sns from 'aws-cdk-lib/aws-sns'; @@ -216,7 +215,7 @@ export class ComputeStack extends cdk.Stack { Auth__CognitoDomain: `${config.cognitoDomainPrefix}.auth.${this.region}.amazoncognito.com`, COGNITO_WEB_CLIENT_ID: props.webClientId, COGNITO_MOBILE_CLIENT_ID: props.mobileClientId, - SES_FROM_ADDRESS: 'proposals@seahavenind.com', + SES_FROM_ADDRESS: 'proposals@seahaven.com', }, tracing: lambda.Tracing.ACTIVE, logRetention: logs.RetentionDays.TWO_MONTHS, @@ -234,31 +233,27 @@ export class ComputeStack extends cdk.Stack { resources: [props.userPool.userPoolArn], })); - // PR4: SES email identity for proposal delivery. - // NOTE: New SES identities start in the **sandbox**. While in sandbox mode the - // account can only send email to *verified* recipient addresses. To send to - // arbitrary recipients, submit a production-access request via the AWS SES console - // (Support Center → "SES Sending Limits Increase"). The email identity itself - // must also be verified — AWS sends a click-link email to the address below after - // deployment; someone with access to the mailbox must click it. - const sesFromAddress = 'proposals@seahavenind.com'; - const sesSenderIdentity = new ses.EmailIdentity(this, 'SesSenderIdentity', { - identity: ses.Identity.email(sesFromAddress), - }); + // SES sender: proposals@seahaven.com. seahaven.com is the org's PRIMARY sending + // domain for all workload stacks. The domain identity is an ACCOUNT-SHARED resource + // managed OUT OF BAND in seahaven-prod (verified via Easy DKIM in the seahaven.com + // Route53 zone; production-access request submitted 2026-07-15) — this stack + // references it rather than owning it, so it is not re-created per stack and does not + // depend on cross-account DNS at deploy time. Do NOT create an EmailIdentity here + // (CloudFormation would fail "already exists" against the out-of-band identity). + // TODO: codify the seahaven.com identity in the seahaven-prod account baseline. + const sesFromAddress = 'proposals@seahaven.com'; - // Grant the API Lambda least-privilege SES send permission, scoped to the - // sender identity ARN only (not ses:* / resource:*). - const senderIdentityArn = `arn:aws:ses:${this.region}:${this.account}:identity/${sesFromAddress}`; + // Least-privilege SES send: scope to the seahaven.com domain identity ARN. A domain + // identity authorizes sending as ANY user@seahaven.com, so the ses:FromAddress + // condition is REQUIRED (not optional) to pin the Lambda to proposals@seahaven.com. + const senderIdentityArn = `arn:aws:ses:${this.region}:${this.account}:identity/seahaven.com`; apiFunction.addToRolePolicy(new iam.PolicyStatement({ actions: ['ses:SendEmail', 'ses:SendRawEmail'], resources: [senderIdentityArn], - // Defense-in-depth (cross-review): only send AS this address, even if the - // identity scope is later widened. conditions: { StringEquals: { 'ses:FromAddress': sesFromAddress }, }, })); - void sesSenderIdentity; // Fix: LAM-C1/INF-H1 — require IAM auth on Function URL (was authType NONE). // NOTE: Lambda HTTP clients (suggestions, pdf-extract, pdf-generate, library-ingest)