Goodbye Gmail App Passwords: DKIM-Signed Alerts With Resend and a Locked-Down Relay

For a long time, every alert in my homelab left the same way: Beszel and the APC UPS card both sent through smtp.gmail.com with a Gmail app password, using addresses like beszel@king-ppap.net. Inbound mail for the domain goes through Cloudflare Email Routing into Gmail, and Gmail's "Send mail as" covered the outbound side.

Then the alerts stopped arriving. Instead I got this:

550 5.7.26 Unauthenticated email from king-ppap.net is not accepted
due to domain's DMARC policy.

Why Gmail SMTP Stopped Working

king-ppap.net has a DMARC record with p=reject. To pass DMARC, a message needs SPF or DKIM to pass for the domain in the From: header.

Mail relayed through a personal Gmail account gets neither:

  • SPF is checked against the envelope sender, which Gmail rewrites to @gmail.com. It passes, but for the wrong domain. Gmail's own message details show mailed-by: gmail.com.
  • DKIM is signed d=gmail.com. A free Gmail account can't sign for your own domain.

So DMARC fails, and p=reject does exactly what it says. Adding Google to the domain's SPF record doesn't help either, because SPF is never evaluated against king-ppap.net in the first place.

The fix is to send through something that signs DKIM as king-ppap.net.

Picking a Relay

OptionFree?Notes
Cloudflare Email SendingNoCan't be enabled without Workers Paid ($5/mo)
Brevo300/dayMarketing-first platform, one account-wide SMTP key
Resend100/day, 3,000/moAPI keys can be scoped to one domain, send-only

For homelab alerts, 100 a day is plenty, and domain-scoped keys mean each device gets its own revocable credential. I went with Resend.

Setting Up Resend

When adding the domain I picked the Tokyo region and turned off click and open tracking. Click tracking rewrites every link through a redirect subdomain, which breaks the direct URLs in alert mails. Open tracking is a hidden pixel I don't need.

Resend supports Cloudflare's Domain Connect, so the DNS records went in with one Authorize click:

TypeNamePurpose
TXTresend._domainkeyDKIM public key
CNAMEsendReturn-path for bounces
CNAMErsendRegional sending subdomain
CNAMElinksTracking (unused with tracking off)

The root MX, SPF and DMARC records don't change. Inbound mail still goes through Cloudflare Email Routing.

The SMTP settings are the same for every app:

Host:     smtp.resend.com
Port:     465 (implicit TLS) or 587 (STARTTLS)
Username: resend
Password: <API key>

The first Beszel test mail went Resend → Cloudflare Email Routing → Gmail, and the headers came back clean:

dkim=pass header.i=@king-ppap.net header.s=resend
arc=pass (i=1 spf=pass spfdomain=rsend.king-ppap.net dkim=pass dkdomain=king-ppap.net)
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=king-ppap.net

The forwarding hop is where DKIM earns its keep. Cloudflare's forward rewrites the envelope, so SPF alignment is gone by the time Gmail sees the message. The DKIM signature survives the forward, and DMARC still passes.

The UPS Card That Couldn't Connect

The APC Network Management Card 2 was a different story. Pointed at Resend, it got as far as 220 Ready to start TLS and then failed. On port 465 with implicit TLS, it didn't even get a server response.

openssl showed why:

openssl s_client -starttls smtp -connect smtp.resend.com:587 -tls1_2 \
  -cipher ECDHE-RSA-AES128-GCM-SHA256
# ssl/tls alert handshake failure

openssl s_client -starttls smtp -connect smtp.resend.com:587 \
  | openssl x509 -noout -text | grep "Public Key Algorithm"
# Public Key Algorithm: id-ecPublicKey

Resend's SMTP endpoint only offers an ECDSA certificate, and every RSA cipher suite is refused. The UPS card's TLS stack evidently can't do ECDSA, while smtp.gmail.com still offers RSA, which is why it had worked with Gmail all along.

Firmware can't fix that from my side, so the card needed something in the middle.

A Local Relay — Locked Down

A Pi CM5 already runs Docker on the LAN, so it got a small Postfix container (boky/postfix). The UPS talks to the relay with an RSA certificate it understands, and the relay talks to Resend with modern TLS.

The obvious risk: an SMTP relay on the LAN holding a valid API key is exactly what a compromised device would love to find. So the relay is locked down in layers.

# compose.yaml
services:
  smtp-relay:
    image: boky/postfix:latest
    restart: unless-stopped
    ports:
      - "587:587"
    env_file: .env # RELAYHOST_PASSWORD, SMTPD_SASL_USERS
    environment:
      RELAYHOST: "[smtp.resend.com]:587"
      RELAYHOST_USERNAME: resend
      ALLOWED_SENDER_DOMAINS: king-ppap.net
      POSTFIX_smtp_tls_security_level: encrypt
    volumes:
      - ./certs:/certs:ro
      - ./init:/docker-init.d:ro

The image generates its own Postfix config at startup, and any script in /docker-init.d/ runs afterwards. That gives the lockdown script the final say:

#!/bin/sh
# init/10-lockdown.sh
postconf -e \
  "mynetworks = 127.0.0.0/8" \
  "smtpd_tls_cert_file = /certs/relay.crt" \
  "smtpd_tls_key_file = /certs/relay.key" \
  "smtpd_tls_security_level = encrypt" \
  "smtpd_tls_auth_only = yes" \
  "smtpd_tls_mandatory_protocols = >=TLSv1.2" \
  "smtpd_delay_reject = no" \
  "smtpd_client_restrictions = check_client_access inline:{ <ups-ip>=OK, 127.0.0.1=OK }, reject" \
  "smtpd_relay_restrictions = permit_sasl_authenticated, reject" \
  "smtpd_sender_login_maps = inline:{ ups-network@king-ppap.net=ups-network@king-ppap.net }" \
  "smtpd_sender_restrictions = reject_sender_login_mismatch, check_sender_access inline:{ ups-network@king-ppap.net=OK }, reject" \
  "smtpd_recipient_restrictions = check_recipient_access inline:{ <my-inbox>=OK }, reject" \
  "smtpd_client_message_rate_limit = 20" \
  "smtpd_client_auth_rate_limit = 10" \
  "anvil_rate_time_unit = 3600s" \
  "smtpd_client_event_limit_exceptions ="

Each line closes one path for abuse:

LayerEffect
Client allowlist + smtpd_delay_reject = noAny IP other than the UPS is rejected as soon as it connects
smtpd_tls_security_level = encrypt + smtpd_tls_auth_onlyLogin is never offered in cleartext
SASL auth required to relayA random 32-character password per device
reject_sender_login_mismatchA login can only send as its own address
Recipient allowlistMail can only go to my own inboxes, never to arbitrary targets
Rate limits20 messages and 10 auth attempts per hour

Two details are easy to miss:

  • The certificate is a self-signed RSA key, generated once with openssl req -x509 -newkey rsa:2048. The card has "Require CA Root Certificate" turned off, and RSA is the whole reason the relay exists.
  • smtpd_client_event_limit_exceptions defaults to $mynetworks, so rate limits silently don't apply to trusted clients. Setting it to empty makes them apply to everyone.

I tested every path before wiring up the UPS:

no TLS            → AUTH not offered
wrong password    → 535 authentication failed
wrong sender      → 553 Sender address rejected: not owned by user
wrong recipient   → 554 Recipient address rejected: Access denied
other LAN host    → 554 Client host rejected: Access denied (at connect)
UPS, correct auth → 250 Ok → relayed to Resend, status=sent

The worst case is now an attacker who has both spoofed the UPS's IP and stolen its password. Even then, they can only send a few alerts an hour to my own inbox.

The UPS settings ended up as <cm5-ip>:587, authentication on, SSL/TLS Always (STARTTLS), and a static DHCP lease on both boxes so the IP allowlist stays valid.

Where Everything Sends Now

SenderRouteStatus
BeszelDirectly to smtp.resend.com✅
APC UPS cardLocked-down CM5 relay → Resend✅
Gmail "Send mail as" (ponpawit@kpp.sh)smtp.resend.com, port 465 SSL, key scoped to kpp.sh✅

Takeaways

  • p=reject breaks Gmail "Send mail as" for your domain. Gmail's envelope and DKIM both say gmail.com, so nothing aligns.
  • DKIM is what survives forwarding. With Cloudflare Email Routing in the path, SPF alignment is gone by the time mail reaches Gmail. DKIM carries it.
  • Check the certificate type when an old device can't connect. An ECDSA-only endpoint looks like a generic "SMTP configuration error" on embedded firmware.
  • Treat a LAN relay as an internet-facing service. It holds a key that can send as your domain, so lock it down accordingly.