openova/platform/stalwart-sovereign/blueprint.yaml
e3mrah 9077016466
feat(bp-stalwart-sovereign): per-Sovereign Stalwart for Console mail (#924) (#931)
Phase-2 follow-up to #883: replace mothership Stalwart relay
(mail.openova.io:587) with a Sovereign-local Stalwart so Console
PIN/magic-link mail originates from `noreply@<sovereignFQDN>` with
per-Sovereign SPF/DKIM/DMARC posture, eliminating the mothership
SMTP SPOF for Sovereign Console login.

What ships:

  1. NEW blueprint platform/stalwart-sovereign/ (otech-level — distinct
     from per-tenant bp-stalwart-tenant). Single Stalwart instance per
     Sovereign cluster, scoped to Sovereign Console system mail. NO
     Keycloak OIDC, NO webmail UI — Sovereign Console is the only
     consumer. Auto-provisioned admin + submission Secrets via the
     lookup-or-generate pattern (#898/#830/#887). Post-install Job:
       - registers the noreply submission principal in Stalwart
       - allows send-as for noreply@<sovereignFQDN>
       - reads DKIM public key, patches dns-records ConfigMap
       - materialises catalyst-system/sovereign-smtp-credentials with
         Sovereign-local infrastructure addresses + credentials,
         carrying BOTH key shapes (smtp-user/smtp-pass + legacy
         user/password) so the consumer chart works either way.

  2. NEW bootstrap-kit slot 95 (clusters/_template/bootstrap-kit/
     95-bp-stalwart-sovereign.yaml). dependsOn: bp-cert-manager,
     bp-catalyst-platform. Sequenced after bp-catalyst-platform (slot
     13) so the chart's post-install Job lands its mirror Secret in
     an already-existing catalyst-system namespace.

  3. bp-catalyst-platform 1.4.19 → 1.4.20: SOURCE-wins precedence
     extended to (a) non-secret fields smtp-host/smtp-port/smtp-from
     so Sovereign-local infra addresses (`mail.<sovereignFQDN>`) take
     over from mothership defaults (`mail.openova.io`) on the next
     reconcile after slot 95 lands, and (b) canonical key shape
     `smtp-user`/`smtp-pass` in addition to legacy `user`/`password`
     source key shape.

  4. expected-bootstrap-deps.yaml: declare slot 95 graph edge.

  5. catalyst-api handler/sovereign_smtp_seed.go: documentation-only
     update to note this Phase-1 step is now a graceful fallback —
     the Phase-2 chart's post-install Job overwrites the mirror
     Secret on first reconcile so the cutover from mothership relay
     to Sovereign-local relay is automatic, no operator action.

Verification:
  - `helm template smoke ./platform/stalwart-sovereign/chart` clean
    (smoke-render-safe; per-template gates skip when sovereignFQDN unset).
  - `helm template smoke -f operator-values.yaml` emits StatefulSet,
    LoadBalancer Service, ClusterIP HTTP Service, DKIM-signing config,
    dns-records ConfigMap, Setup Job + RBAC.
  - `chart/tests/sovereign-render.sh` 3 cases all PASS.
  - `helm template smoke ./products/catalyst/chart` (1.4.20) clean.
  - `helm lint` both charts: clean (only icon-recommended INFO).
  - `bash scripts/check-bootstrap-deps.sh` PASSED — bootstrap-kit
    dependency graph audit, 0 drift, 0 cycles.
  - `go test -run TestSeedSovereignSMTP` — Phase-1 seed tests pass.
  - `go test -run TestBootstrapKit_TemplateClusterParses` — slot 95
    YAML parses cleanly.

Out of scope (sub-PR follow-up under #924):
  - DKIM keypair generation in catalyst-api orchestrator + DNS records
    (MX/A/SPF/DMARC/DKIM-pubkey) registration via PDM dynadot adapter
    at omani.works.
  - Hetzner PTR (rDNS) auto-registration via the Hetzner cloud API.
  - Cert-manager Certificate adding mail.<sovereignFQDN> SAN to the
    Sovereign wildcard cert (chart relies on the existing wildcard
    cert from bp-catalyst-platform 1.4.0+'s per-zone Certificate
    template — when that wildcard chain covers the Sovereign FQDN,
    `mail.<sovereignFQDN>` is already covered).

Acceptance (lands when sub-PR follow-up ships):
  - Sovereign Console PIN delivery uses noreply@<sov-fqdn>.
  - External mail server (e.g. Gmail) accepts mail with valid SPF + DKIM.
  - Mothership SMTP no longer SPOF for Sovereign Console login.

Co-authored-by: hatiyildiz <hatiyildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:20:16 +04:00

158 lines
5.6 KiB
YAML

apiVersion: catalyst.openova.io/v1alpha1
kind: Blueprint
metadata:
name: bp-stalwart-sovereign
labels:
catalyst.openova.io/category: communication
catalyst.openova.io/section: pts-4-5-communication
spec:
# 0.1.0 (#924) — initial release. Phase-2 follow-up to the Phase-1
# SMTP seed seam (#883): the Sovereign Console's PIN/magic-link mail
# is delivered by a Sovereign-LOCAL Stalwart instance instead of being
# relayed through the mothership at mail.openova.io:587. Sender flips
# from `noreply@openova.io` to `noreply@<sovereignFQDN>` and SPF/DKIM/
# DMARC posture is per-Sovereign, eliminating the mothership-as-SPOF.
#
# Distinct from `bp-stalwart-tenant`:
# - bp-stalwart-tenant — per-SME (per-vcluster) instance, OIDC-authn
# against the per-tenant Keycloak realm, customer mailboxes.
# - bp-stalwart-sovereign (THIS) — single instance per Sovereign,
# scoped to Sovereign Console mail (PIN delivery, ops alerts, the
# `noreply@<sovereignFQDN>` system mailbox). Authn is the local
# admin principal materialised by the chart's auto-provisioned
# `sovereign-smtp-credentials` Secret (read by bp-catalyst-platform's
# `catalyst-openova-kc-credentials` chart-rendered Secret via
# Helm `lookup`). NO Keycloak OIDC — Console is the only client.
#
# Per #817 Chart.yaml version MUST equal blueprint.yaml spec.version.
version: 0.1.0
card:
title: Stalwart (Sovereign Console mail)
summary: |
Sovereign-local Stalwart for Sovereign Console PIN/magic-link
mail delivery from `noreply@<sovereignFQDN>`. Phase-2 sovereignty
replacement for the mothership Stalwart relay (Phase-1 #883).
Single instance per Sovereign cluster. Materialises the
`catalyst-system/sovereign-smtp-credentials` Secret read by
bp-catalyst-platform 1.4.17+'s
`catalyst-openova-kc-credentials` chart-rendered Secret. DKIM
keypair generated at first install, public component published
via the `stalwart-sovereign-dns-records-required` ConfigMap for
the orchestrator to register at the Sovereign's parent zone via
PDM (sub-PR follow-up — chart ships first per #924 scope split).
External mail surface: `mail.<sovereignFQDN>` LoadBalancer
(SMTP 25, submission 587, submissions 465, IMAP 143/993). NO
webmail UI in the Sovereign-level instance — Sovereign Console
is the only consumer.
icon: stalwart.svg
category: communication
tags: [mail, smtp, sovereign, console, pin, dkim, spf, dmarc]
documentation: https://stalw.art/
license: AGPL-3.0
visibility: listed
owner:
team: platform
contact: catalyst@openova.io
configSchema:
type: object
required: []
properties:
sovereignFQDN:
type: string
description: |
Sovereign FQDN (e.g. `omantel.omani.works`). Drives the
SMTP sender domain (`noreply@<sovereignFQDN>`), the public
MX hostname (`mail.<sovereignFQDN>`), and the DKIM signing
domain. Resolved from `global.sovereignFQDN` at render time
per the bootstrap-kit overlay convention.
persistence:
type: object
properties:
spool:
type: object
properties:
size:
type: string
default: 10Gi
storageClassName:
type: string
description: Empty = cluster default; per-Sovereign overlay supplies.
admin:
type: object
properties:
secretName:
type: string
default: stalwart-sovereign-admin
username:
type: string
default: postmaster
smtp:
type: object
description: |
Submission credential the chart materialises into Stalwart's
runtime AND mirrors into `catalyst-system/sovereign-smtp-credentials`
for bp-catalyst-platform's `catalyst-openova-kc-credentials`
chart to read via Helm `lookup`. The chart auto-generates
a random 32-char password on first install; subsequent
reconciles read the existing bytes via `lookup`.
properties:
submissionUser:
type: string
default: noreply
submissionSecretName:
type: string
default: stalwart-sovereign-submission
dns:
type: object
properties:
dkim:
type: object
properties:
selector:
type: string
default: dkim
algorithm:
type: string
default: ed25519-sha256
spf:
type: object
properties:
policy:
type: string
enum: ["-all", "~all"]
default: "~all"
dmarc:
type: object
properties:
policy:
type: string
enum: [reject, quarantine, none]
default: quarantine
rua:
type: string
description: Empty = composed as `postmaster@<sovereignFQDN>`.
placementSchema:
modes: [single-region]
default: single-region
manifests:
chart: ./chart
depends:
- blueprint: bp-cert-manager
version: ^1.0
alias: certs
- blueprint: bp-cert-manager-powerdns-webhook
version: ^1.0
alias: certs-dns01
optional: true
- blueprint: bp-powerdns
version: ^1.0
alias: dns
optional: true
upgrades:
from: []
observability:
metrics: prometheus
logs: stdout