carlos / platform Public

mail: the enable walk — mint, derive, deliver

Delivers the five variables seapointish already reads, so the v1 criterion
needs no diff in that repo. The DERIVED password goes out, never the IAM
secret; a test pins that, because delivering the secret would authenticate
against nothing and put a live AWS credential in an app's environment. A
From address outside every verified domain is refused here rather than at
runtime, where the policy condition would make it look like email simply
does not work.
Paul Campbell 204535ac0d84eb26a51b2929a64ac802da054609 parent 6603dda
2 files changed, +445 −0
  • internal/mail/walk.go +241 −0
  • internal/mail/walk_enable_test.go +204 −0
diff --git a/internal/mail/walk.go b/internal/mail/walk.go
index a887fa8d0..10cb56ec6 100644
--- a/internal/mail/walk.go
+++ b/internal/mail/walk.go
@@ -5,11 +5,14 @@ package mail
import (
"context"
+ "crypto/ecdh"
"errors"
"fmt"
+ "sort"
"strings"
"time"
+ "github.com/carlosframework/platform/internal/appconfig"
"github.com/carlosframework/platform/internal/dnsrec"
"github.com/carlosframework/platform/internal/release"
)
@@ -206,3 +209,241 @@ func VerifyDomain(ctx context.Context, s release.Store, p Provisioner,
}
return rec, nil
}
+
+// SMTPPort is the submission port every delivered credential uses. 587 with
+// STARTTLS is the whole of the delivered contract's transport (spec §1).
+const SMTPPort = "587"
+
+// SMTPHost is the submission endpoint for a region. Delivery resolves this
+// per instance, which is what makes region-local sending a lookup rather
+// than a subsystem (spec §8).
+func SMTPHost(region string) string { return "email-smtp." + region + ".amazonaws.com" }
+
+// Declare records the member's half. It does NOT validate the From address
+// against a domain, because a member may declare before any domain is
+// verified; Enable is where the two meet and where a mismatch is refused.
+func Declare(ctx context.Context, s release.Store, acct, app, envPrefix, from, actor string,
+ now time.Time) (*Record, error) {
+
+ if envPrefix == "" {
+ envPrefix = DefaultEnvPrefix
+ }
+ if !ValidEnvPrefix(envPrefix) {
+ return nil, fmt.Errorf("mail: invalid env prefix %q — want ^[A-Z][A-Z0-9_]{0,31}$", envPrefix)
+ }
+ if !strings.Contains(from, "@") {
+ return nil, fmt.Errorf("mail: From %q is not an address", from)
+ }
+ rec, etag, err := Get(ctx, s, acct, app)
+ if err != nil {
+ return nil, err
+ }
+ if rec == nil {
+ rec = &Record{}
+ }
+ // The env prefix is LOCKED once delivery has happened: moving it would
+ // leave the old variables standing in every instance's config, and the
+ // app would keep reading the stale pair.
+ if rec.Declared != nil && rec.Provisioned != nil && rec.Declared.EnvPrefix != envPrefix {
+ return nil, fmt.Errorf("mail: env prefix is %q and cannot move after delivery",
+ rec.Declared.EnvPrefix)
+ }
+ rec.Declared = &Declared{EnvPrefix: envPrefix, From: from, At: now, By: actor}
+ if _, err := Put(ctx, s, acct, app, rec, etag); err != nil {
+ return nil, err
+ }
+ return rec, nil
+}
+
+// grantedDomains is every domain in the account whose record names this app
+// and which has at least one VERIFIED region.
+func grantedDomains(ctx context.Context, s release.Store, acct, app string) ([]*DomainRecord, error) {
+ all, err := ListDomains(ctx, s, acct)
+ if err != nil {
+ return nil, err
+ }
+ var out []*DomainRecord
+ for _, d := range all {
+ if len(d.VerifiedARNs()) == 0 {
+ continue
+ }
+ for _, a := range d.Apps {
+ if a == app {
+ out = append(out, d)
+ break
+ }
+ }
+ }
+ return out, nil
+}
+
+// EnvDelivered reports which delivered variables are NOT present, of the
+// right kind, in the app's config. It is the walk's idempotence check and
+// the console's "what is missing" answer, and it exists because "the record
+// says provisioned" is not the same claim as "the app can actually send".
+func EnvDelivered(ctx context.Context, s release.Store, acct, app, prefix string) ([]string, error) {
+ man, err := appconfig.Get(ctx, s, acct, app)
+ if err != nil {
+ return nil, err
+ }
+ host, port, user, pass, from := EnvNames(prefix)
+ var missing []string
+ check := func(name, wantKind string) {
+ v, ok := man.Vars[name]
+ if !ok || v.Kind != wantKind {
+ missing = append(missing, name)
+ }
+ }
+ check(host, "env")
+ check(port, "env")
+ check(from, "env")
+ check(user, "secret")
+ check(pass, "secret")
+ sort.Strings(missing)
+ return missing, nil
+}
+
+// Enable provisions the declared app's credential and delivers it.
+//
+// Idempotent: a healthy provisioned app makes this a no-op — no fresh key,
+// no rewrite — because minting on every pass would burn IAM's two-key
+// ceiling and churn every instance's config. "Healthy" means the record
+// says provisioned AND every delivered variable is present, of the right
+// kind; anything less and the walk runs, because a missing variable means
+// the app cannot send.
+func Enable(ctx context.Context, s release.Store, p Provisioner, sealPub *ecdh.PublicKey,
+ d Deployment, acct, app string, now time.Time) (*Record, error) {
+
+ rec, etag, err := Get(ctx, s, acct, app)
+ if err != nil {
+ return nil, err
+ }
+ if rec == nil || rec.Declared == nil {
+ return nil, ErrNotDeclared
+ }
+
+ domains, err := grantedDomains(ctx, s, acct, app)
+ if err != nil {
+ return nil, err
+ }
+ if len(domains) == 0 {
+ return nil, ErrNoDomains
+ }
+ var arns, names []string
+ for _, dom := range domains {
+ arns = append(arns, dom.VerifiedARNs()...)
+ names = append(names, dom.Domain)
+ }
+ sort.Strings(arns)
+ sort.Strings(names)
+
+ // The From address must be at a granted domain, or the policy's
+ // ses:FromAddress condition refuses every send at RUNTIME — which
+ // presents as "email silently does not work" instead of as this refusal.
+ at := strings.LastIndex(rec.Declared.From, "@")
+ fromDomain := rec.Declared.From[at+1:]
+ if !containsString(names, fromDomain) {
+ return nil, fmt.Errorf("mail: From %q is not at any verified domain (%s)",
+ rec.Declared.From, strings.Join(names, ", "))
+ }
+
+ // Idempotence short-circuit.
+ if rec.Provisioned != nil {
+ missing, err := EnvDelivered(ctx, s, acct, app, rec.Declared.EnvPrefix)
+ if err != nil {
+ return nil, err
+ }
+ if len(missing) == 0 {
+ return rec, nil
+ }
+ }
+
+ user := UserName(acct, app)
+ arn, err := p.EnsureUser(ctx, user, UserPath, arns, names)
+ if err != nil {
+ return nil, fmt.Errorf("mail: ensure user %s: %w", user, err)
+ }
+ // Reap before minting: reaching here means delivery is missing, so any
+ // current key is either undelivered (its secret died with a failed
+ // config write) or stranded by a crash between mint and record. No
+ // record names either; only list-and-delete finds them. A pending-finish
+ // key from an unfinished rotation is kept for its explicit finish.
+ var keep []string
+ if rec.Provisioned != nil && rec.Provisioned.PrevKeyID != "" {
+ keep = append(keep, rec.Provisioned.PrevKeyID)
+ }
+ if err := p.ReapKeys(ctx, user, keep); err != nil {
+ return nil, fmt.Errorf("mail: reap keys for %s: %w", user, err)
+ }
+ keyID, secret, err := p.MintKey(ctx, user)
+ if err != nil {
+ return nil, fmt.Errorf("mail: mint key for %s: %w", user, err)
+ }
+
+ if err := writeSMTPEnv(ctx, s, sealPub, acct, app, rec.Declared.EnvPrefix,
+ d.Region, keyID, secret, rec.Declared.From); err != nil {
+ return nil, err
+ }
+
+ prev := ""
+ if rec.Provisioned != nil {
+ prev = rec.Provisioned.PrevKeyID
+ }
+ rec.Provisioned = &Provisioned{
+ IAMUser: user, UserARN: arn, KeyID: keyID, PrevKeyID: prev, Active: true, At: now,
+ }
+ if _, err := Put(ctx, s, acct, app, rec, etag); err != nil {
+ return nil, err
+ }
+ return rec, nil
+}
+
+func containsString(xs []string, want string) bool {
+ for _, x := range xs {
+ if x == want {
+ return true
+ }
+ }
+ return false
+}
+
+// writeSMTPEnv delivers the five variables. The DERIVED password goes out,
+// never the IAM secret — delivering the secret would authenticate against
+// nothing and would put a live AWS credential into an app's environment.
+//
+// Both credential halves are sealed. The username is not itself sensitive,
+// but a plaintext access key id in a config manifest is a free inventory of
+// the estate for anyone who reads one, and sealing both costs nothing.
+func writeSMTPEnv(ctx context.Context, s release.Store, sealPub *ecdh.PublicKey,
+ acct, app, prefix, region, keyID, secret, from string) error {
+
+ hostName, portName, userName, passName, fromName := EnvNames(prefix)
+ password := SMTPPassword(secret, region)
+
+ sets := map[string]appconfig.Var{
+ hostName: {Kind: "env", Value: SMTPHost(region)},
+ portName: {Kind: "env", Value: SMTPPort},
+ fromName: {Kind: "env", Value: from},
+ }
+ // Validate BEFORE sealing. A sealed value reaches appconfig as an opaque
+ // envelope, so an unrenderable byte would sail through and stall the
+ // app's whole config render on every host, hours later and out of sight.
+ // The errors name the VARIABLE and never the value — these strings reach
+ // logs and HTTP bodies, and one half is a live credential.
+ for _, half := range []struct{ name, plain string }{
+ {userName, keyID}, {passName, password},
+ } {
+ if err := appconfig.ValidVarValue(half.plain); err != nil {
+ return fmt.Errorf("mail: minted credential for %s is not EnvironmentFile-safe: %w", half.name, err)
+ }
+ env, err := appconfig.Seal(sealPub, []byte(half.plain))
+ if err != nil {
+ return fmt.Errorf("mail: seal %s: %w", half.name, err)
+ }
+ sets[half.name] = appconfig.Var{Kind: "secret", Envelope: env}
+ }
+ if _, err := appconfig.Apply(ctx, s, acct, app, sets, nil); err != nil {
+ return fmt.Errorf("mail: deliver smtp config for %s/%s: %w", acct, app, err)
+ }
+ return nil
+}
diff --git a/internal/mail/walk_enable_test.go b/internal/mail/walk_enable_test.go
new file mode 100644
index 000000000..49701750b
--- /dev/null
+++ b/internal/mail/walk_enable_test.go
@@ -0,0 +1,204 @@
+package mail
+
+import (
+ "context"
+ "crypto/ecdh"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/appconfig"
+ "github.com/carlosframework/platform/internal/dnsrec"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// enableFixture seeds a declared app with one VERIFIED domain — the state
+// Enable expects to find.
+func enableFixture(t *testing.T) (context.Context, release.Store, *MemProvisioner, *ecdh.PublicKey, time.Time) {
+ t.Helper()
+ ctx := context.Background()
+ s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
+ now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
+
+ _, pubB64, err := appconfig.GenerateKey()
+ if err != nil {
+ t.Fatalf("GenerateKey: %v", err)
+ }
+ pub, err := appconfig.ParsePublicKey(pubB64, "test")
+ if err != nil {
+ t.Fatalf("ParsePublicKey: %v", err)
+ }
+
+ if _, err := Declare(ctx, s, "bag", "seapointish", "SEAPOINTISH_SMTP",
+ "noreply@seapointish.bag.oncarlos.com", "paul@rslw.com", now); err != nil {
+ t.Fatalf("Declare: %v", err)
+ }
+ if _, _, err := EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(),
+ "bag", "seapointish", "seapointish.bag.oncarlos.com", now); err != nil {
+ t.Fatalf("EnsureDomain: %v", err)
+ }
+ p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com",
+ Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"})
+ if _, err := VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now); err != nil {
+ t.Fatalf("VerifyDomain: %v", err)
+ }
+ return ctx, s, p, pub, now
+}
+
+// The v1 criterion in one test: the five variables seapointish already
+// reads arrive, with the right values, and the app changes nothing.
+func TestEnableDeliversSeapointishsEnv(t *testing.T) {
+ ctx, s, p, pub, now := enableFixture(t)
+ rec, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now)
+ if err != nil {
+ t.Fatalf("Enable: %v", err)
+ }
+ if rec.Provisioned == nil || rec.Provisioned.KeyID == "" {
+ t.Fatalf("nothing provisioned: %+v", rec)
+ }
+ man, err := appconfig.Get(ctx, s, "bag", "seapointish")
+ if err != nil {
+ t.Fatalf("appconfig.Get: %v", err)
+ }
+ if got := man.Vars["SEAPOINTISH_SMTP_HOST"].Value; got != "email-smtp.eu-west-1.amazonaws.com" {
+ t.Fatalf("HOST = %q", got)
+ }
+ if got := man.Vars["SEAPOINTISH_SMTP_PORT"].Value; got != "587" {
+ t.Fatalf("PORT = %q", got)
+ }
+ if got := man.Vars["SEAPOINTISH_SMTP_FROM"].Value; got != "noreply@seapointish.bag.oncarlos.com" {
+ t.Fatalf("FROM = %q", got)
+ }
+ // The username is the access key id and is NOT a secret; the password
+ // is, and must arrive sealed.
+ if man.Vars["SEAPOINTISH_SMTP_USER"].Kind != "secret" {
+ t.Fatal("USER must be delivered as a secret alongside PASS")
+ }
+ if man.Vars["SEAPOINTISH_SMTP_PASS"].Kind != "secret" {
+ t.Fatal("PASS was not delivered as a sealed secret")
+ }
+ if man.Vars["SEAPOINTISH_SMTP_PASS"].Envelope == "" {
+ t.Fatal("PASS has no envelope")
+ }
+}
+
+// The delivered password is the DERIVED one, not the raw IAM secret. This
+// is the test that catches delivering the secret access key by mistake —
+// which would authenticate against nothing and leak a real AWS credential
+// into an app's environment.
+//
+// This opens the actual delivered envelope rather than just re-deriving the
+// expected value from the fake's plaintext: a version that only checked
+// "the derivation function returns something different from its input"
+// would pass even if Enable delivered the raw secret, because it never
+// looks at what was actually sealed and sent.
+func TestEnableDeliversTheDerivedPasswordNotTheSecret(t *testing.T) {
+ ctx, s, p, _, now := enableFixture(t)
+ privB64, pubB64, err := appconfig.GenerateKey()
+ if err != nil {
+ t.Fatalf("GenerateKey: %v", err)
+ }
+ priv, err := appconfig.ParsePrivateKey(privB64, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ ownPub, err := appconfig.ParsePublicKey(pubB64, "test")
+ if err != nil {
+ t.Fatalf("ParsePublicKey: %v", err)
+ }
+
+ rec, err := Enable(ctx, s, p, ownPub, testDeployment(), "bag", "seapointish", now)
+ if err != nil {
+ t.Fatalf("Enable: %v", err)
+ }
+ raw := p.Users[UserName("bag", "seapointish")].Keys[rec.Provisioned.KeyID].Secret
+ want := SMTPPassword(raw, "eu-west-1")
+ if want == raw {
+ t.Fatal("test is not meaningful — derivation returned the input")
+ }
+
+ man, err := appconfig.Get(ctx, s, "bag", "seapointish")
+ if err != nil {
+ t.Fatalf("appconfig.Get: %v", err)
+ }
+ env := man.Vars["SEAPOINTISH_SMTP_PASS"].Envelope
+ if env == "" {
+ t.Fatal("PASS has no envelope")
+ }
+ plain, err := appconfig.Open(priv, env)
+ if err != nil {
+ t.Fatalf("Open: %v", err)
+ }
+ if string(plain) != want {
+ t.Fatalf("delivered password = %q, want the derived password %q", plain, want)
+ }
+ if string(plain) == raw {
+ t.Fatal("Enable delivered the raw IAM secret instead of the derived SMTP password")
+ }
+}
+
+// A healthy provisioned app makes a re-run a no-op: no fresh key, no
+// rewrite. Minting on every pass would burn IAM's two-key ceiling and
+// churn every instance's config.
+func TestEnableIsIdempotent(t *testing.T) {
+ ctx, s, p, pub, now := enableFixture(t)
+ first, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now)
+ if err != nil {
+ t.Fatalf("first Enable: %v", err)
+ }
+ second, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now)
+ if err != nil {
+ t.Fatalf("second Enable: %v", err)
+ }
+ if first.Provisioned.KeyID != second.Provisioned.KeyID {
+ t.Fatal("a re-run minted a fresh key")
+ }
+}
+
+// Enable must refuse an app with no VERIFIED domain. A credential with no
+// identity is a principal that can send nothing while looking provisioned.
+func TestEnableRefusesWithoutAVerifiedDomain(t *testing.T) {
+ ctx := context.Background()
+ s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
+ now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
+ _, pubB64, _ := appconfig.GenerateKey()
+ pub, _ := appconfig.ParsePublicKey(pubB64, "test")
+
+ Declare(ctx, s, "bag", "seapointish", "SEAPOINTISH_SMTP", "n@x.com", "paul@rslw.com", now)
+ EnsureDomain(ctx, s, p, dnsrec.NewMemWriter(), testDeployment(),
+ "bag", "seapointish", "seapointish.bag.oncarlos.com", now) // left pending
+ if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now); err == nil {
+ t.Fatal("Enable succeeded with only a pending domain")
+ }
+}
+
+func TestEnableRefusesAnUndeclaredApp(t *testing.T) {
+ ctx := context.Background()
+ s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
+ now := time.Now().UTC()
+ _, pubB64, _ := appconfig.GenerateKey()
+ pub, _ := appconfig.ParsePublicKey(pubB64, "test")
+ if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now); err != ErrNotDeclared {
+ t.Fatalf("Enable on an undeclared app = %v, want ErrNotDeclared", err)
+ }
+}
+
+// The From address must be at a granted domain. Otherwise the policy's
+// ses:FromAddress condition refuses every send at runtime — a failure that
+// surfaces as "email silently does not work" rather than as a refusal here.
+//
+// Declare itself accepts any From (a member may declare before any domain
+// is verified); Enable, which knows the granted domains, is what refuses
+// one outside every verified domain.
+func TestDeclareAcceptsAnyFromButEnableRefusesAnUngrantedOne(t *testing.T) {
+ ctx, s, p, pub, now := enableFixture(t)
+ _ = p
+ _ = pub
+ if _, err := Declare(ctx, s, "bag", "seapointish", "SEAPOINTISH_SMTP",
+ "noreply@somewhere-else.com", "paul@rslw.com", now); err != nil {
+ t.Fatalf("Declare itself may accept any From: %v", err)
+ }
+ // but Enable, which knows the granted domains, must refuse it
+ if _, err := Enable(ctx, s, p, pub, testDeployment(), "bag", "seapointish", now); err == nil {
+ t.Fatal("Enable accepted a From address outside every granted domain")
+ }
+}