carlos / platform Public

infra: flagship SES multi-tenant mail (Kass first) + operator queue

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 9be1c80541b206f3dcf3782cbc0dadc7fdb7f5b7 parent 4c610fb
2 files changed, +157 −0
  • infra/flagship/README.md +42 −0
  • infra/flagship/mail.tf +115 −0
diff --git a/infra/flagship/README.md b/infra/flagship/README.md
new file mode 100644
index 000000000..3e564ab09
--- /dev/null
+++ b/infra/flagship/README.md
@@ -0,0 +1,42 @@
+# ๐Ÿค– Flagship deployment โ€” operator notes
+
+> ๐Ÿค– Written by an LLM (Claude), per factor X.
+
+AWS account **271376211898** (adopted from Keymail, spec ยง12), profile
+`keymail`, eu-west-1. State: `s3://carlos-tfstate-271376211898/flagship/`.
+
+## Operator queue (actions the agent could not take unattended)
+
+1. **Apply the base stack** (15 resources, plan reviewed โ€” buckets, IE
+ box with SSM-only access, IAM):
+
+ ```
+ cd infra/flagship && tofu apply plan.out
+ ```
+
+ (If `plan.out` is stale, `tofu plan -out=plan.out` first and re-review:
+ everything should be `carlos-*` named, nothing keymail-touching.)
+
+2. **Apply the mail stack** (after 1; needs `DNSIMPLE_TOKEN` in env for
+ the DKIM CNAMEs):
+
+ ```
+ cd infra/flagship && DNSIMPLE_TOKEN=... tofu apply
+ ```
+
+3. **Root email re-home** (whenever): move this account's root email to a
+ carlosframework.com / carlosplatform.com address (spec ยง12).
+
+## Kass cutover go/no-go checklist (Plan B task B6)
+
+- [ ] Peppers copied to `/carlos/kass/...` SSM and byte-verified
+- [ ] SES DKIM Success + test send delivered (production access, no sandbox)
+- [ ] Kass restored on flagship box; `/api/library` 200 via `--resolve`
+- [ ] `carlos ops restore-verify` green against `carlos-data-ie-โ€ฆ/kass/`
+- [ ] Rehearsal freeze-to-serving time recorded (target < 10 min)
+- [ ] Rollback rehearsed: DNSimple `home.kass.training` A back to old EIP
+ + old `kass.service` restart
+
+Cutover: stop old kass โ†’ final litestream delta โ†’ start new โ†’ smoke via
+`--resolve` โ†’ flip DNSimple `home` A record (TTL 300) โ†’ watch cert issue +
+real passkey sign-in. Old account frozen 30 days, then a teardown plan.
diff --git a/infra/flagship/mail.tf b/infra/flagship/mail.tf
new file mode 100644
index 000000000..60d92bec4
--- /dev/null
+++ b/infra/flagship/mail.tf
@@ -0,0 +1,115 @@
+# Outbound email for flagship tenants โ€” multi-tenant SES (platform spec
+# ยง7 svc/mail, v1 shape: per-app identities + configuration sets + scoped
+# SMTP users; the fronting service comes later). This account has
+# production SES access โ€” the reason it was adopted as the flagship
+# account (spec ยง12).
+#
+# First tenant: Kass. Its domain identity re-verifies here with NEW DKIM
+# tokens; the old account's DKIM records coexist in DNSimple until the
+# old account winds down.
+
+terraform {
+ required_providers {
+ dnsimple = {
+ source = "dnsimple/dnsimple"
+ version = "~> 1.5"
+ }
+ }
+}
+
+# Reads DNSIMPLE_TOKEN from the environment; never written to a file here.
+provider "dnsimple" {
+ account = "74881"
+}
+
+resource "aws_ses_domain_identity" "kass" {
+ domain = "kass.training"
+}
+
+resource "aws_ses_domain_dkim" "kass" {
+ domain = aws_ses_domain_identity.kass.domain
+}
+
+resource "dnsimple_zone_record" "kass_dkim" {
+ count = 3
+ zone_name = "kass.training"
+ name = "${aws_ses_domain_dkim.kass.dkim_tokens[count.index]}._domainkey"
+ type = "CNAME"
+ value = "${aws_ses_domain_dkim.kass.dkim_tokens[count.index]}.dkim.amazonses.com"
+ ttl = 3600
+}
+
+resource "aws_sns_topic" "deliverability" {
+ name = "carlos-ses-deliverability"
+}
+
+resource "aws_sns_topic_subscription" "deliverability_email" {
+ topic_arn = aws_sns_topic.deliverability.arn
+ protocol = "email"
+ endpoint = "paul@rslw.com"
+}
+
+resource "aws_sns_topic_policy" "ses" {
+ arn = aws_sns_topic.deliverability.arn
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [{
+ Effect = "Allow"
+ Principal = { Service = "ses.amazonaws.com" }
+ Action = "sns:Publish"
+ Resource = aws_sns_topic.deliverability.arn
+ Condition = { StringEquals = { "AWS:SourceAccount" = local.account } }
+ }]
+ })
+}
+
+# Per-tenant configuration set: reputation and suppression isolated per
+# app (the svc/mail promise, delivered by SES config rather than a
+# service front for now).
+resource "aws_sesv2_configuration_set" "kass_signin" {
+ configuration_set_name = "kass-signin"
+ delivery_options { tls_policy = "REQUIRE" }
+ reputation_options { reputation_metrics_enabled = true }
+ suppression_options { suppressed_reasons = ["BOUNCE", "COMPLAINT"] }
+}
+
+resource "aws_sesv2_configuration_set_event_destination" "kass_signin_sns" {
+ configuration_set_name = aws_sesv2_configuration_set.kass_signin.configuration_set_name
+ event_destination_name = "deliverability"
+ event_destination {
+ enabled = true
+ matching_event_types = ["BOUNCE", "COMPLAINT", "REJECT", "RENDERING_FAILURE"]
+ sns_destination { topic_arn = aws_sns_topic.deliverability.arn }
+ }
+}
+
+# Send-only SMTP user for the Kass tenant.
+resource "aws_iam_user" "kass_smtp" {
+ name = "carlos-kass-smtp"
+}
+
+resource "aws_iam_user_policy" "kass_smtp_send" {
+ name = "send-only"
+ user = aws_iam_user.kass_smtp.name
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [{
+ Effect = "Allow"
+ Action = ["ses:SendRawEmail", "ses:SendEmail"]
+ Resource = "*"
+ }]
+ })
+}
+
+resource "aws_iam_access_key" "kass_smtp" {
+ user = aws_iam_user.kass_smtp.name
+}
+
+output "kass_smtp_user" {
+ value = aws_iam_access_key.kass_smtp.id
+}
+
+output "kass_smtp_password" {
+ value = aws_iam_access_key.kass_smtp.ses_smtp_password_v4
+ sensitive = true
+}