576f4ca88b
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.1 KiB
4.1 KiB
felhom.eu — task reports
Overwrite this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in hub/CHANGELOG.md.
App-email passthrough — hub leg (POST /api/v1/mail → Resend SMTP), hub v0.18.0
Task: SMTP app-relay (apps → on-box shim → hub → Resend). Implements
documentation/audits/SPIKE-smtp-app-relay-2026-06-28.md (verdict READY).
Baseline (verified live)
- Hub
main@4b97855, version v0.17.0 (the Resend-key rotation already shipped earlier today 2026-06-29) → target v0.18.0. (The prompt assumed v0.16.0→v0.17.0; the rotation took v0.17.0 first, so this leg is v0.18.0.) Prerequisite satisfied: Resend key is out-of-band inSecret/resend-api, read viaRESEND_API_KEY.
Files
- Created
internal/mailrelay/relay.go—ResendSMTP(Sender): STARTTLS tosmtp.resend.com:587,AUTH LOGIN resend/<key>(small stdlibnet/smtp.AuthLOGIN impl), rawMAIL/RCPT/DATApassthrough.FromDomain(From-header parser). No new external dependency. - Created
internal/api/mail.go—handleMail:checkAuthCustomer→ From-domain allowlist (403 backstop) → per-customer token-bucket rate limit (429) → passthrough to Resend (200 / 502).SetMailRelaywiring +mailRateLimiter. - Modified
internal/api/handler.go— sender/limiter/allowlist fields +POST /api/v1/mailroute. - Modified
cmd/hub/main.go—MailConfig(per_customer_per_minute,from_domains) + wireResendSMTPwhen a key is present (else 503). Thenotify/dispatcher.goHTTP-API alert path is untouched.
Green gate (local)
go build ./... && go vet ./... && go test ./... — PASS (6 packages ok, 0 failures).
Tests & §10 companion red-proofs
- Passthrough byte-equality (§7 A / §10):
TestMail_HappyPath_PassthroughRawBytes— theSenderreceives the raw bytes unchanged (not a parsed payload). PASS. - From-reject + companion (§7 B / §10):
TestMail_FromOutsideAllowlist_Rejected_NoSend(403, sender never called) +TestMail_FromReject_CompanionProof(allowing the domain reaches the sender). PASS. - Per-box rate limit + isolation + companion (§7 C / §10):
TestMail_RateLimit_PerCustomer(429 on the 2nd at 1/min; a different customer unaffected) +TestMail_RateLimit_CompanionProof(generous limit lets N+1 through). PASS. - Send-failure→502, 401/503/400 paths, token-bucket unit (injected clock), LOGIN auth + From-domain parse. PASS.
Deployment & live validation — DONE (2026-06-29)
- Deployed
felhom-hub:0.18.0: built on 180 → bumpedmanifests/hub.yaml→ ArgoCDfelhomapp hard-refresh- sync (auto-sync off). Rollout OK; live image
…felhom-hub:0.18.0; startup log:[INFO] App-email relay enabled (limit 30/min/customer, From domains [felhom.eu]). The Resend key is injected fromSecret/resend-api(RESEND_API_KEY); no key in any committed file.
- sync (auto-sync off). Rollout OK; live image
- End-to-end (app → on-box shim → hub → Resend): a raw MIME message with an inline CID image, From
vaultwarden@felhom.eu, was relayed from guest 9201's shim through this hub to the live Resend account. Hub log:/api/v1/mail: relayed for demo-felhom (from=vaultwarden@felhom.eu rcpts=1 bytes=789); the box's shim gothub=200and returnedDATA 250 "OK: queued"to the sender (Resend accepted/queued for the verifiedfelhom.eudomain). Operator inbox arrival is the final confirmation (sent to the operator's own address). - Security backstop (live): a send From
evil@notfelhom.examplewas rejected at the box's shim (550) before ever reaching/api/v1/mail— the hub From-allowlist + rate-limit are the documented second backstop.
Observations
- App-relay is a separate code path from the hub's own structured alerts (which keep using the Resend HTTP API) — raw passthrough is required because the API path silently drops inline CID images (spike §4).
- v1: single-shot, no spool, no idempotency key. v2 would add accept-and-spool +
Resend-Idempotency-Key. - Fleet free-tier ceiling is 100 emails/day. No secrets in any committed file.