Skip to content

feat: drive Zitadel SMTP from a platform SMTPSender#12

Merged
patrickleet merged 1 commit into
mainfrom
feat/smtp-sender-integration
May 29, 2026
Merged

feat: drive Zitadel SMTP from a platform SMTPSender#12
patrickleet merged 1 commit into
mainfrom
feat/smtp-sender-integration

Conversation

@patrickleet
Copy link
Copy Markdown
Contributor

@patrickleet patrickleet commented May 29, 2026

Summary

Adds spec.smtp to AuthStack so the running Zitadel instance's SMTP provider is configured from a platform SMTPSender (AWS SES + Cloudflare DKIM) — entirely declaratively. The operator flips one boolean and references the SMTPSender; no helm values, no secret plumbing.

spec:
  smtp:
    enabled: true
    smtpSenderRef: { name: ops }
    fromAddress: no-reply@ops.com.ai
    fromName: Hops Ops

How it works (render pipeline)

File Role
040-observed-smtp-sender Observe the SMTPSender XR (control-plane default PC) for host/port/username + DKIM
160-external-secret-smtp-password Pull the bare SES password from AWS SM via ESO (whole-secret, no property)
170/175 Assemble a zitadel ProviderConfig from this stack's published iam-admin PAT (push auto-enabled by spec.smtp.enabled)
180-zitadel-smtp-config Drive Zitadel's SMTP via a smtp.zitadel.m.crossplane.io Config MR
000/010/999 State, observed read + fromAddress-domain validation, status surface

Mirrors the email-marketing 400/410/420 consumer pattern.

Lifecycle / destructive-omit safety

A multi-lens adversarial review surfaced (and this PR fixes) a destructive-omit hazard:

  • Durable resources (password Secret, credentials Secret, ProviderConfig) gate on stable intent (smtp.enabled && smtpSenderRef.name) — they survive a transient Observe miss instead of being deleted.
  • The Config MR gates only on observed existence ($smtp.render). It deliberately does not gate on the Zitadel release .ready (a chart upgrade would delete the live SMTP config) nor on dkimVerified (DKIM signing follows the published DNS records, not SES's status reporting).
  • status.smtp.ready reflects Config-MR composition accurately.

Tests

make test23/23 pass, including:

  • smtp-wires-smtpsender-consumer-chain (full chain renders with observed)
  • smtp-durable-resources-render-without-observed (durability guard: durable resources render on intent alone; Config MR absent until observed)

Verified end-to-end on pat-local

Installed the Configuration on colima, enabled spec.smtp on the live pat-local AuthStack:

  • All 5 SMTP Object MRs Synced/Ready=True; AuthStack XR Synced/Ready=True
  • Zitadel SMTP config created — ID 375049110438290685 (Config MR Synced/Ready=True)
  • zitadel-smtp Secret: 44-char SES password; zitadel-smtp-credentials: valid creds JSON (domain=auth.ops.com.ai)

Prerequisite (tracked separately — not in this PR)

The 040 Observe of the SMTPSender XR requires the control-plane provider-kubernetes ServiceAccount to have read+dry-run access to the *.hops.ops.com.ai XR groups. The platform doesn't grant this by default (the provider's :system role doesn't aggregate XR roles). It was satisfied for the e2e via a temporary colima ClusterRole; the permanent home is the hops local CLI provider bootstrap. Listmonk/OpenPanel consumers will need the same.

No breaking changes — additive (spec.smtp defaults off).

🤖 Generated with Claude Code

Summary by CodeRabbit

New Features

  • AuthStack now supports SMTP configuration for external email delivery
  • Added new example demonstrating SMTP setup with email provider integration
  • Status fields extended to report SMTP readiness and connection details (host, port, authentication, DKIM verification)

Review Change Stack

Add spec.smtp to AuthStack so the running Zitadel instance's SMTP provider
is configured from a platform SMTPSender (SES + Cloudflare DKIM), entirely
declaratively:

- 040 observes the referenced SMTPSender for host/port/username + DKIM status
- 160 pulls the bare SES password from AWS SM via ESO (whole-secret, no property)
- 170/175 assemble a zitadel ProviderConfig from this stack's published
  iam-admin PAT (push auto-enabled by spec.smtp.enabled)
- 180 drives Zitadel's SMTP via a smtp.zitadel Config MR

Gating avoids destructive-omit: the durable Secrets + ProviderConfig gate on
stable intent (smtp.enabled + smtpSenderRef.name); only the Config MR gates on
observed existence ($smtp.render). No .ready/dkim gates (would delete the live
SMTP config on a chart upgrade or status flip). fromAddress is validated
against the SMTPSender's verified SES domain once observed.

Verified end-to-end on pat-local: Zitadel SMTP config 375049110438290685
created (Synced/Ready). 23/23 render tests pass.

Implements [[tasks/authstack-smtp-sender]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19093e1e-170b-4aec-9283-45cd94508e1b

📥 Commits

Reviewing files that changed from the base of the PR and between 6ae1a76 and dd6e7ed.

📒 Files selected for processing (12)
  • Makefile
  • apis/authstacks/definition.yaml
  • examples/authstacks/with-smtp.yaml
  • functions/render/000-state-init.yaml.gotmpl
  • functions/render/010-state-status.yaml.gotmpl
  • functions/render/040-observed-smtp-sender.yaml.gotmpl
  • functions/render/160-external-secret-smtp-password.yaml.gotmpl
  • functions/render/170-zitadel-smtp-credentials.yaml.gotmpl
  • functions/render/175-zitadel-smtp-providerconfig.yaml.gotmpl
  • functions/render/180-zitadel-smtp-config.yaml.gotmpl
  • functions/render/999-status.yaml.gotmpl
  • tests/test-render/main.k

📝 Walkthrough

Walkthrough

This PR adds SMTP support to AuthStack by extending the CRD schema with spec.smtp and status.smtp blocks, implementing state initialization and observation logic, composing five Crossplane resources for SMTP credential and configuration management, and testing the complete flow with two validation scenarios.

Changes

SMTP Configuration Support for AuthStack

Layer / File(s) Summary
CRD Schema and Example
apis/authstacks/definition.yaml, examples/authstacks/with-smtp.yaml, Makefile
AuthStack CRD extended with spec.smtp (enable flag, SMTPSender reference, sender identity, TLS/activation settings, secret store configuration) and status.smtp (observed connection fields, DKIM status, readiness). Example manifest demonstrates SMTP configuration with SMTPSender reference and email headers. Example added to Makefile build pipeline.
SMTP State Initialization
functions/render/000-state-init.yaml.gotmpl
Reads spec.smtp configuration and defaults fields into runtime state variables. Ties SMTP enablement to automatic activation of push credentials for ProviderConfig wiring.
SMTP Observation and Readiness
functions/render/010-state-status.yaml.gotmpl
Observes SMTPSender resource status to extract host/port/username/awsSecretsManagerPath/DKIM verification. Validates required fields (smtpSenderRef.name, fromAddress) and enforces domain ownership constraint. Computes smtp.render readiness boolean and populates status.smtp.
SMTP Resource Composition
functions/render/040-observed-smtp-sender.yaml.gotmpl, functions/render/160-external-secret-smtp-password.yaml.gotmpl, functions/render/170-zitadel-smtp-credentials.yaml.gotmpl, functions/render/175-zitadel-smtp-providerconfig.yaml.gotmpl, functions/render/180-zitadel-smtp-config.yaml.gotmpl
Five Crossplane Object templates conditionally render SMTP infrastructure: observes SMTPSender, materializes SES SMTP password via ExternalSecret, creates Zitadel credentials ExternalSecret with access token, generates Zitadel ProviderConfig, and applies Zitadel SMTP Config with host:port, sender address, from/reply-to headers, and TLS settings.
Status Rendering and Tests
functions/render/999-status.yaml.gotmpl, tests/test-render/main.k
Renders status.smtp section when enabled. Test 14 validates full composition with observed SMTPSender and ready Zitadel Helm Release. Test 15 verifies durable intent behavior: credentials and ProviderConfig render from intent alone; Config MR awaits observation readiness.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 An AuthStack finds its voice in digital mail,
SMTP credentials flowing without fail,
Zitadel speaks through Crossplane's gentle hand,
Secrets and configs now perfectly planned,
Email delivery blooms across the land! 📬✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: drive Zitadel SMTP from a platform SMTPSender' directly describes the primary objective—integrating Zitadel SMTP configuration with a platform SMTPSender resource—which is the main focus of all file changes across the CRD, templates, examples, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/smtp-sender-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Published Crossplane Package

The following Crossplane package was published as part of this PR:

Package: ghcr.io/hops-ops/auth-stack:pr-12-3411fe947d0157a1fbca81269abcf84112459445

View Package

@patrickleet patrickleet merged commit 6ff46d6 into main May 29, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant