This release includes 2 security fixes for security teams reviewing exposed deployments.
Topics
+3 more
Affected surfaces
Summary
AI summaryUpdates Breaking, Security & supply chain, and Development across a mixed release.
Full changelog
A security-hardening and operational-maturity release, from a full audit of the shipped v1.0. One breaking change (below) that closes an open-relay footgun; everything else is additive or a fix.
Breaking
auth_required = "none"on a public bind is now refused at config-parse time (#41). An SMTP listener withauth_required = "none"whoselistenaddress is not verifiably loopback/private (e.g.:2525,0.0.0.0:2525, a public IP, or an unresolvable hostname) is a startup error. Settrusted_network = trueto assert the listener is reachable only from a trusted network — a Docker bridge with noports:exposure, a VPN, or a firewall. Migration: deployments built from the pre-existing Ghost / Gitea / internal-relay recipes usedauth_required = "none"withlisten = ":2525"and must addtrusted_network = true(the updated recipes include it). This closes the open-relay footgun where the sender allowlist was the only gate on an unauthenticated public listener.
Added
- SMTP listener brute-force and resource hardening (#50). Per-remote-IP AUTH-failure budget (10 failures/minute; further attempts get
421and the connection closes; successful auths never consume budget). Global (max_connections, default 100) and per-IP (max_connections_per_ip, default 16) concurrent-connection caps; over-cap connections get a421before any protocol work. - CSRF rejections now increment
posthorn_spam_blocked_total{kind="csrf"}— previously they were log-only and invisible on/metrics. spam_blocked(honeypot and origin) andcsrf_rejectedlog lines now carryclient_ipfor operator forensics, matchingrate_limited; omitted whenstrip_client_ip = true(FR59).
Fixed
- An
[smtp_listener]-only config (no[[endpoints]]) is now accepted — the config validator required at least one HTTP endpoint, breaking the documented Ghost and Gitea recipes and every SMTP-listener-only deployment.posthorn validateoutput now names the listener so a listener-only config doesn't report a bare "0 endpoint(s)". (#37) - SMTP-listener sends now honor the FR19-22 retry policy — a transient provider error is retried once (as the HTTP ingress already did) instead of returning an immediate
451. Previously a provider blip could drop a transactional email on the SMTP path that the HTTP path would have retried. The retry policy is now shared between both ingresses. (#59) - The HTTP server now sets
ReadTimeout(15s) andWriteTimeout(30s, chosen to exceedReadTimeout+ the handler's 10s request bound so a legitimate slow upload followed by a provider retry isn't truncated after the mail was sent) plus an explicit 64 KBMaxHeaderBytes, so slow-body senders and slow-reading clients can't hold connections indefinitely. (#42) - Raw stdlib parse errors are no longer echoed to clients on malformed bodies; both parse paths return a generic 400 and log the detail server-side as
body_parse_failed. Deliberate API-shape messages (e.g. "nested objects are not supported") remain client-visible. (#42)
Security & supply chain
- CI now runs a
gofmtgate,govulncheck, andgolangci-linton every push and pull request. (#38) - Docker base images are pinned by digest, and Dependabot keeps the base images, Go modules, and GitHub Actions current. (#39)
- Release images now ship SLSA provenance and SBOM attestations, are signed with cosign (keyless via GitHub OIDC), and are gated on a Trivy vulnerability scan. (#56)
Development
- New hermetic provider-testing harness (
core/providertest): ingress→egress end-to-end tests for all five transports and the SMTP listener, run on every PR with no credentials. Amake test-liverunner and a scheduled, protected workflow validate against real provider APIs (against non-delivering targets; SES via OIDC, no stored key). See #76. Posthorn deliberately does not test deliverability/DKIM — the provider owns signing and reputation.
Image: ghcr.io/craigmccaskill/posthorn:1.1.0 (multi-arch, cosign-signed, SBOM + provenance attested). Validated via v1.1.0-rc.1 before GA.
Breaking Changes
- `auth_required = "none"` on a public bind (listen not loopback/private) is rejected at config parse time; add `trusted_network = true` to assert trusted‑network reachability.
Security Fixes
- CI pipeline added `govulncheck`, Docker base images pinned by digest, release images signed with cosign and ship SLSA provenance + SBOM; these harden the supply chain.
- SMTP listener now respects shared retry policy (FR19‑22), retrying transient provider errors once instead of immediate 451 failure.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Posthorn
All releases →Beta — feedback welcome: [email protected]