# email-connect transactional receiver — railiance01 Deploy the narrow invitation/verification HTTP receiver so **user-engine** can send mail without holding SMTP credentials, and so unrelated workloads cannot reach the send API. **Target cluster:** railiance01 only (`KUBECONFIG=~/.kube/config-hosteurope`). Stable in-cluster URL after apply: ```text http://email-connect.email-connect.svc.cluster.local:8080 ``` ## Ownership split | Concern | Owner | Where | | --- | --- | --- | | Package, manifests, probes, NetworkPolicy, rollback | `email-connect` | this directory | | OpenBao path, ESO role/policy, ClusterSecretStore | `railiance-platform` | CCR-2026-0010, `openbao/policies/` | | Credential routing catalog | `ops-warden` | `email-connect-transactional` | Never paste SMTP passwords, ingest tokens, or OpenBao tokens into Git, State Hub, workplans, logs, or chat. Route first: ```bash warden route find "email-connect SMTP transactional" --json warden route show email-connect-transactional --json ``` ## Runtime secrets (OpenBao → ESO) | Item | Value | | --- | --- | | KV path | `platform/workloads/email-connect/transactional` | | Fields | `EMAIL_CONNECT_INGEST_TOKEN`, `EMAIL_CONNECT_SMTP_USERNAME`, `EMAIL_CONNECT_SMTP_PASSWORD` | | Read / ESO policy | `external-secrets-email-connect` (+ `workload-kv-read-email-connect-transactional` for operator OIDC) | | ClusterSecretStore | `openbao-email-connect` (namespace-limited; token auth like activity-core/reuse) | | ESO token Secret | `external-secrets/openbao-email-connect-eso-token` (via `openbao-eso-token-apply.sh`) | | ExternalSecret | `email-connect/email-connect-runtime` → Secret `email-connect-runtime` | | ops-warden catalog | `email-connect-transactional` (draft until provisioned) | Non-secret config (host, port, sender, portal URL) lives in ConfigMap `email-connect-config`. ### Operator provision (attended, after CCR approval) ```bash # Values from mode-0600 files only; shred after. bao kv put platform/workloads/email-connect/transactional \ EMAIL_CONNECT_INGEST_TOKEN=@/path/to/ingest.token \ EMAIL_CONNECT_SMTP_USERNAME=@/path/to/smtp.user \ EMAIL_CONNECT_SMTP_PASSWORD=@/path/to/smtp.pass ``` user-engine must receive the **same** ingest token under its own runtime secret path so it can call `Authorization: Bearer …` without ever seeing SMTP fields. ## Network posture - **Default deny** ingress and egress in `email-connect`. - **Ingress:** only pods labeled `app.kubernetes.io/name=user-engine` in namespace `user-engine`, TCP 8080. - **Egress:** kube-system DNS + TCP 587 to the public internet (IONOS STARTTLS). No cluster-lateral egress and no general HTTPS. Bearer auth on `/v1/*` is the second gate; NetworkPolicy is the outer gate. ## Apply Prerequisites: 1. CCR-2026-0010 approved. 2. KV secret provisioned at `platform/workloads/email-connect/transactional` (fields non-empty; do not log values). 3. ESO delivery token + ClusterSecretStore on railiance01 (token pattern matches `openbao-activity-core` / `openbao-reuse`, not Kubernetes auth yet). 4. Image available on the node/registry at the digest pinned in `deployment.yaml`. ```bash export KUBECONFIG=~/.kube/config-hosteurope # 1) Policy + limited ESO child token → external-secrets Secret ./deploy/k8s/railiance/openbao-eso-token-apply.sh # 2) Namespace-scoped ClusterSecretStore (platform add-on copy) kubectl apply -f ~/railiance-platform/argocd/platform-addons/openbao-secretstore/openbao-email-connect.clustersecretstore.yaml # 3) Optional: load a freshly built image when the registry is not yet updated # docker build -f Containerfile -t forgejo.coulomb.social/coulomb/email-connect:local . # docker save … | ssh railiance01 sudo k3s ctr -n k8s.io images import - # 4) Package kubectl apply -k deploy/k8s/railiance kubectl -n email-connect rollout status deployment/email-connect kubectl -n email-connect get pods,svc,networkpolicy,externalsecret,secret ``` Expect ExternalSecret `SecretSynced` and a Ready pod. If the Secret is missing, do not create a bootstrap Secret with values from chat — fix OpenBao/ESO. ## Smoke (no SMTP proof yet) From a **user-engine** pod (or a temporary pod in that namespace with the same labels — NetworkPolicy will otherwise drop the packet): ```bash # Health only (no auth) kubectl -n user-engine exec deploy/user-engine -- \ wget -qO- http://email-connect.email-connect.svc.cluster.local:8080/healthz # Auth check: wrong token must be 401; correct token is exercised by T04. ``` Negative checks: - A pod in another namespace cannot reach port 8080 (connection timeout/reset). - user-engine runtime must not contain `EMAIL_CONNECT_SMTP_*` keys. ## Rollback ```bash kubectl -n email-connect rollout undo deployment/email-connect kubectl -n email-connect rollout status deployment/email-connect ``` `revisionHistoryLimit: 5` keeps prior digests. SQLite on the PVC is additive; rolling back does not clear delivery/idempotency rows (by design). ## Done criteria (EMAIL-WP-0004-T03) - [ ] user-engine calls the cluster-local receiver without possessing SMTP credentials - [ ] Unrelated workloads cannot open TCP 8080 on the receiver - [ ] SMTP password and ingest token exist only in OpenBao + the scoped K8s Secret - [ ] Probes green; NetworkPolicy default-deny + SMTP-only egress applied - [ ] Documented rollback path works against the previous revision