SMTP Credentials Rotation Runbook
The author-nudge cron in tutorials-srv sends mail via SMTP. All five transport fields — SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_FROM, SMTP_PASS — live in BTP Credential Store and are managed through the admin Secrets UI at /admin-ui/#secrets. No values are sourced from cf set-env, mtaext, or GitHub Actions secrets in normal operation; those paths exist only as emergency overrides (see "Disaster recovery" below).
The mail client at srv/lib/mail-client.js reads all 5 fields via the shared secret-resolver: credstore-first, process-env fallback (defense-in-depth, not normally used), 5-min TTL cache, warn-once-per-window logging. A rotation through the admin UI propagates within 5 minutes — the resolver cache hot-flushes immediately on save, but the mail-client's cached transporter is rebuilt on the next TTL refresh, not on the admin write.
When to rotate
- After a suspected leak.
- On the schedule the SMTP relay owner sets (today:
smtpauth.mail.net.sap— rotation cadence TBD with SAP IT). - After an SMTP authentication failure surfaces in
cf logs tutorials-srv --recentor the admin Job Log.
Steps
1. Issue or rotate the SMTP credential at the relay
For smtpauth.mail.net.sap: open a ticket with SAP IT. Capture the new password.
2. Write the new password to credstore
Open /admin-ui/#secrets in the deployed environment.
- If
SMTP_PASSis in the list: select the row → "Update Value" → paste the new password → Save. - If
SMTP_PASSis NOT in the list: "Add Secret" → keySMTP_PASS→ value (the new password) → description "SMTP password for author-nudge emails" → rotation owner (the relay owner contact) → Save.
The admin UI calls writeSecret('SMTP_PASS', value) against the credstore service. The 5-minute TTL cache in mail-client.js means propagation is automatic — no app restart needed.
3. Verify SMTP
Open /admin-ui/#operations (nav: Content → Featured Tasks). In the List Report toolbar click Send test notification email, enter the recipient and a template level (0=first, 1=second, 2=third, 3=final), and Send. A success toast confirms the send; a warning surfaces the server-side error (e.g. No mail transport configured, which means the message was queued to FailedEmails because the SMTP_* secrets are unset for this env).
The button lives on the (frequently empty) Featured Tasks table — it is a global toolbar action, so it works even when no featured tasks exist.
Equivalently, call the unbound action directly:
curl -X POST "$BASE_URL/admin/testNotificationEmail" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to": "your-address@sap.com", "level": 0}'Expected response: {"success": true, "error": ""}. Check your inbox.
4. If the test send fails
Check cf logs tutorials-srv --recent for the SMTP authentication error.
Common causes:
| Symptom | Likely cause | Fix |
|---|---|---|
535 Authentication failed | Wrong password in credstore | Re-paste SMTP_PASS in /admin-ui/#secrets |
535 with the right password | Wrong username | Update SMTP_USER row in /admin-ui/#secrets |
ECONNREFUSED | Wrong relay host or port | Update SMTP_HOST / SMTP_PORT rows in /admin-ui/#secrets |
550 sender rejected | Wrong SMTP_FROM, not authorized to send as that address | Update SMTP_FROM row in /admin-ui/#secrets |
No mail transport configured log | All 5 fields null | Confirm /admin-ui/#secrets has values for SMTP_HOST + SMTP_PASS at minimum |
Roll back by writing the OLD value(s) to the same admin-UI row(s).
5. Once verified, ensure the cron is enabled
Open /admin-ui/#operations. Confirm isNotificationSendingAllowed=true in the displayed ImsConfig values. If false, click the Toggle Notifications control to enable. The next Monday-09:00-UTC cron will fire with real recipients.
6. Record the rotation
In /admin-ui/#secrets, edit the SMTP_PASS row's lastRotatedAt to today's date.
First-time cutover (when SMTP fields do not yet exist on a fresh environment)
This is a one-time sequence for enabling author-nudge emails on a fresh deploy:
- Confirm the seed-secrets script has populated the 5 metadata rows in the Secrets table for this environment. Open
/admin-ui/#secretsand look forSMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_FROM,SMTP_PASS. If any are missing, runnpx cds bind --exec -- node scripts/seed-secrets.cjs --commit— idempotent onkey, so re-running is safe. - For each row in the admin Secrets UI: click → "Set Value" → paste the real value → Save. The admin handler hot-flushes the resolver cache on save; the mail-client's transporter cache rebuilds within 5 minutes (its own TTL).
- Verify with the "Test Notification Email" action (Step 3 above).
- Flip the
isNotificationSendingAllowedflag in/admin-ui/#operations.
Disaster recovery — credstore is down
The mail-client falls through to process.env.SMTP_* if credstore throws or returns null for an alias. This is a defense-in-depth path — no normal operation should rely on it. If you absolutely must send mail during a credstore outage:
cf set-env tutorials-srv SMTP_HOST <value>
cf set-env tutorials-srv SMTP_PORT <value>
cf set-env tutorials-srv SMTP_USER <value>
cf set-env tutorials-srv SMTP_FROM <value>
cf set-env tutorials-srv SMTP_PASS <value>
cf restart tutorials-srvOnce credstore is back, remove the overrides:
cf unset-env tutorials-srv SMTP_HOST
# ... repeat for the other 4 fields
cf restart tutorials-srvThe credstore values resume precedence within 5 minutes (or immediately on the first admin-UI save after credstore returns).
Caveat:
cf set-envvalues do NOT survive the next MTA redeploy. Use only as a short-term emergency override.
Per-author "Last Chance" emails (#622)
In addition to testNotificationEmail (single-recipient SMTP check), /admin-ui/#operations exposes two admin-triggered actions for authors whose tutorials have not responded to the weekly cron's escalating reminders:
- Per-author send (
sendLastChanceEmail) — surgical "last chance" email to one author covering all their stale tutorials. UsedryRun: truefirst to verify the recipient list, thendryRun: falseto send. - Bulk sweep (
sendLastChanceEmailsAllDormant) — fires per-author last-chance emails to every author whose worst tutorial is atlastChanceMinLevel(default 3, inImsConfig) AND whose last notification is older thanlastChanceDormancyDays(default 60, alsoImsConfig). Both knobs are admin-tunable.dryRun: truereturns a preview of qualifying authors;dryRun: falsefans out the sends serially.
Both actions use a dedicated last-chance.html template distinct from the cron's automated level-3 — Riley/Tom can edit the human-tone copy without disturbing the weekly cron's tone progression. Same SMTP path (credstore-fronted resolver); same FailedEmails retry queue.
The weekly cron itself now also defaults to per-author digest mode (one email per author per cycle, grouping all of their stale tutorials, escalating to the worst-case level present). Controlled by ImsConfig.useDigestNotifications (default true); flip to false for one-click rollback to the legacy per-tutorial loop. See the #622 design spec for the full rationale.
Related runbooks
- GitHub Dispatch PAT rotation — same credstore-first pattern for the GitHub workflow_dispatch token.
- Spec: Author-nudge emails design — full design rationale for #545.