From 59bcf495bf8769c9a2b022ec71aa05b5de47966e Mon Sep 17 00:00:00 2001 From: Greg Kilmer Date: Tue, 7 Jul 2026 22:10:17 -0400 Subject: [PATCH] Allow self signed cert for stalwart --- .env.example | 4 ++++ docs/DEPLOYMENT.md | 18 ++++++++++++++++++ src/pages/api/contact.ts | 9 +++++++++ 3 files changed, 31 insertions(+) diff --git a/.env.example b/.env.example index ab930f8..dbe9ea6 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,10 @@ SMTP_USER= SMTP_PASS= CONTACT_TO=contact@gregkilmer.com SMTP_FROM="Greg's Site " +# Set to "true" to accept a self-signed TLS certificate on the SMTP connection. +# Only enable when the relay is on a trusted private network (e.g. the homelab +# Docker network) and presents a self-signed cert. Defaults to false (verify). +SMTP_ALLOW_SELFSIGNED= # Optional webhook fallback (JSON POST). Only used if SMTP_HOST is unset. # CONTACT_WEBHOOK_URL= diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index a5cbb58..ae377e0 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -25,6 +25,7 @@ delivery, set **either** SMTP vars **or** `CONTACT_WEBHOOK_URL` (see | `SITE_URL` | Canonical URL for metadata | `https://gregkilmer.com` | | `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` | SMTP relay | unset | | `SMTP_FROM` | From address for outgoing mail | derived from `SITE_URL` | +| `SMTP_ALLOW_SELFSIGNED` | Accept self-signed cert (internal hop) | `false` | | `CONTACT_TO` | Inbox that receives submissions | `contact@gregkilmer.com` | | `CONTACT_WEBHOOK_URL` | JSON webhook fallback (Discord/Slack/n8n) | unset | | `CONTACT_RATE_LIMIT` / `CONTACT_RATE_WINDOW_MIN` | Per-IP rate limit | `5` / `15` | @@ -123,6 +124,23 @@ not `gmail.com` — SPF on a domain you don't own will reject or sink the mail. Test delivery + spam score with [mail-tester.com](https://www.mail-tester.com/) (send a form submission to the unique address it gives you). +#### Self-signed certificates on an internal relay + +If the SMTP relay (e.g. Stalwart) is on the same private Docker network as the +site container and presents a self-signed TLS certificate, nodemailer's default +certificate verification will reject the connection with +`Error: self-signed certificate` (`code: ESOCKET`). For a trusted internal hop +that never traverses an untrusted network, set: + +``` +SMTP_ALLOW_SELFSIGNED=true +``` + +This disables TLS certificate verification for the SMTP connection only. It is +`false` by default; only enable it when the relay is on a trusted private +network. For a relay reachable over the public internet, prefer installing a +real certificate (Let's Encrypt) on the relay instead. + ### Cloudflare Turnstile (bot protection) The form includes an invisible Cloudflare Turnstile challenge. To enable it: diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts index 32dc67a..5251c6b 100644 --- a/src/pages/api/contact.ts +++ b/src/pages/api/contact.ts @@ -167,11 +167,20 @@ async function deliver(msg: Message): Promise { const nodemailer = (await import('nodemailer')).default; const port = Number(smtpPort() ?? 587); const user = smtpUser(); + // Accept self-signed certificates when explicitly opted in. Intended for + // internal homelab hops where the SMTP relay (e.g. Stalwart on the same + // Docker network) presents a self-signed cert and the connection never + // traverses an untrusted network. Defaults to false so production-grade + // TLS verification is the default; the override is a deliberate choice. + const allowSelfSigned = env('SMTP_ALLOW_SELFSIGNED') === 'true'; const transport = nodemailer.createTransport({ host, port, secure: port === 465, auth: user ? { user, pass: smtpPass() } : undefined, + ...(allowSelfSigned + ? { tls: { rejectUnauthorized: false } } + : {}), }); const hostname = (() => { try {