health-setup-checks: the certificate's name, Postfix's greeting, the proxy
Decided by the owner on 2026-10-13 (in place of preflight), taken up on 2026-10-16; mockupsb2dcefc,cc33890,0feaa55(his: the greeting on a row of its own, the name check a tag beside Names). - The certificate: health.Certificate keeps its names (DNS names, or the CN without any) and ForHost holds it to SELFPOST_HOSTNAME as a TLS client does - VerifyHostname over the DNS names, the CN only without any, as OpenSSL's X509_check_host and so a sending Postfix still take it (src/tls/tls_client.c:439, 513). A certificate for another name fails; the tag beside Names says matches / not for the hostname. - The greeting: postconf -x -h smtpd_banner, read once at the panel's start like the served certificate's path (main.cf is not rewritten after), is what smtpd writes after "220 " (src/smtpd/smtpd.c:5979). A first word other than the hostname warns, with what to set. A fact may now take the whole row (Fact.Wide, sp-wide) and carry a tag beside its label (Fact.LabelTag); both on the kit page. - The proxy: auth.Module.Seen says the address the panel counts sign-ins against, the peer, and whether X-Forwarded-For was read or left unread for an untrusted peer; the Configuration card shows "Your address" and, in the second case, warns that every visitor shares one sign-in limit and names the /32 (or /128) to put in TRUSTED_PROXY_CIDR. Tests: TestCertificateForHost (CN-only, DNS names, a wildcard, the CN ignored beside DNS names), TestSeenAddress, TestHealthSaysWhereItSeesTheViewer, TestHealthGreetingAndCertificateName; e2e reads the stand's right state on Health (matches, "220 mail.e2e.test ESMTP", a direct client). go vet + go test (26 packages) and make e2e on the test server for this tree. Docs: help (word for word with the mockup), guide § Overview and Health, CHANGELOG [Unreleased] § Added, roadmap row done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ type Config struct {
|
||||
TLSCertFile string
|
||||
TLSServedCertFile string
|
||||
TLSReloadInterval time.Duration
|
||||
SMTPBanner string
|
||||
SignSocket string
|
||||
SignLocalSocket string
|
||||
JournalSocket string
|
||||
|
||||
@@ -143,6 +143,8 @@ func (h *Handlers) HandleHealthRecheck(w http.ResponseWriter, r *http.Request) {
|
||||
// of Overview and the Notification mail card, which only Health shows.
|
||||
func (h *Handlers) healthView(r *http.Request, flash string) *view.Health {
|
||||
page := healthPage(h.shellMeta(r), h.healthChecks(), flash)
|
||||
seen := h.auth.Seen(r)
|
||||
page.WithClientAddress(seen.Client, seen.Peer, seen.FromHeader, seen.HeaderIgnored)
|
||||
mail := h.serverMail(false)
|
||||
page.WithNotificationMail(notify.From(h.cfg.Hostname), mail.rows, mail.status)
|
||||
stats, err := h.store.SenderCheckStats(time.Now())
|
||||
@@ -205,8 +207,15 @@ func healthPage(m view.Meta, c healthReadings, flash string) *view.Health {
|
||||
if !cert.NotAfter.IsZero() {
|
||||
page.CertFacts = append(page.CertFacts, view.Fact{Label: "Expires", Value: view.Plain(cert.NotAfter.UTC().Format("2006-01-02 15:04 UTC")), Mono: true})
|
||||
}
|
||||
if cert.Subject != "" {
|
||||
page.CertFacts = append(page.CertFacts, view.Fact{Label: "Names", Value: view.Plain(cert.Subject), Mono: true})
|
||||
if len(cert.Names) > 0 {
|
||||
names := view.Fact{Label: "Names", Value: view.Plain(strings.Join(cert.Names, ", ")), Mono: true}
|
||||
if m := cert.MatchesHost; m != nil {
|
||||
names.LabelTag = &view.Tag{Status: "ok", Label: "matches"}
|
||||
if !*m {
|
||||
names.LabelTag = &view.Tag{Status: "fail", Label: "not for the hostname"}
|
||||
}
|
||||
}
|
||||
page.CertFacts = append(page.CertFacts, names)
|
||||
}
|
||||
if r := cert.Renewal; r != nil {
|
||||
page.CertFacts = append(page.CertFacts,
|
||||
@@ -225,7 +234,7 @@ func healthPage(m view.Meta, c healthReadings, flash string) *view.Health {
|
||||
}
|
||||
|
||||
ptr := c.PTR
|
||||
page.Hostname.End = view.Verdict(string(ptr.Status))
|
||||
hostStatus := ptr.Status
|
||||
host := c.Hostname
|
||||
if host == "" {
|
||||
host = "(SELFPOST_HOSTNAME is not set)"
|
||||
@@ -241,6 +250,18 @@ func healthPage(m view.Meta, c healthReadings, flash string) *view.Health {
|
||||
}
|
||||
page.HostFacts = append(page.HostFacts, view.Fact{Label: "Lookup", Value: lookup, Mono: true})
|
||||
}
|
||||
// The greeting must name the hostname: a receiver compares it with the
|
||||
// name it looked up, and a container id or localhost there reads as a
|
||||
// misconfigured host. A banner that could not be read says nothing.
|
||||
if c.Banner != "" {
|
||||
fact := view.Fact{Label: "Greeting", Value: view.Plain("220 " + c.Banner), Mono: true, Wide: true}
|
||||
if f := strings.Fields(c.Banner); c.Hostname != "" && (len(f) == 0 || !strings.EqualFold(f[0], c.Hostname)) {
|
||||
hostStatus = health.Worst(hostStatus, health.StatusWarn)
|
||||
fact.Note = view.Plain("Postfix greets with another name than " + c.Hostname + ": set myhostname to it (build/postfix-config.sh does, from SELFPOST_HOSTNAME).")
|
||||
}
|
||||
page.HostFacts = append(page.HostFacts, fact)
|
||||
}
|
||||
page.Hostname.End = view.Verdict(string(hostStatus))
|
||||
return page
|
||||
}
|
||||
|
||||
@@ -439,6 +460,8 @@ type healthReadings struct {
|
||||
SocketStatus health.Status
|
||||
Hostname string
|
||||
PTR dnscheck.Result
|
||||
// Banner is Postfix's smtpd_banner, expanded: the greeting after "220".
|
||||
Banner string
|
||||
}
|
||||
|
||||
// healthChecks runs every server check once.
|
||||
@@ -459,7 +482,7 @@ func (h *Handlers) healthChecks() healthReadings {
|
||||
queueStatus = health.StatusWarn
|
||||
}
|
||||
|
||||
cert := health.CheckServedCertificate(h.cfg.TLSServedCertFile, h.cfg.TLSCertFile, h.cfg.TLSReloadInterval, time.Now())
|
||||
cert := health.CheckServedCertificate(h.cfg.TLSServedCertFile, h.cfg.TLSCertFile, h.cfg.TLSReloadInterval, time.Now()).ForHost(h.cfg.Hostname)
|
||||
sockets := []health.Socket{
|
||||
signerSocket(h.cfg.SignSocket, "applications"),
|
||||
signerSocket(h.cfg.SignLocalSocket, "notifications"),
|
||||
@@ -490,6 +513,7 @@ func (h *Handlers) healthChecks() healthReadings {
|
||||
SocketStatus: socketStatus,
|
||||
Hostname: h.cfg.Hostname,
|
||||
PTR: srv.PTR,
|
||||
Banner: h.cfg.SMTPBanner,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/mixeme/selfpost/internal/dnscheck"
|
||||
"github.com/mixeme/selfpost/internal/health"
|
||||
"github.com/mixeme/selfpost/internal/web/view"
|
||||
)
|
||||
|
||||
// checksFixture is one reading of every server check, shaped as healthChecks
|
||||
@@ -127,3 +128,44 @@ func TestProcessRowOfAnIdleOneShotSaysOnDemand(t *testing.T) {
|
||||
t.Errorf("stopped daemon = %+v", daemon)
|
||||
}
|
||||
}
|
||||
|
||||
// Health's Hostname card shows Postfix's greeting on a row of its own, and
|
||||
// warns when it names another host; the certificate's names carry whether
|
||||
// they are the hostname's.
|
||||
func TestHealthGreetingAndCertificateName(t *testing.T) {
|
||||
c := checksFixture()
|
||||
c.Banner = "mail.example.org ESMTP"
|
||||
matches := true
|
||||
c.Cert.Names, c.Cert.MatchesHost = []string{"mail.example.org"}, &matches
|
||||
page := healthPage(view.Meta{}, c, "")
|
||||
var greeting *view.Fact
|
||||
for i := range page.HostFacts {
|
||||
if page.HostFacts[i].Label == "Greeting" {
|
||||
greeting = &page.HostFacts[i]
|
||||
}
|
||||
}
|
||||
if greeting == nil || greeting.Value[0].Text != "220 mail.example.org ESMTP" || !greeting.Wide || greeting.Note != nil {
|
||||
t.Fatalf("greeting = %+v", greeting)
|
||||
}
|
||||
names := func(p *view.Health) *view.Tag {
|
||||
for _, f := range p.CertFacts {
|
||||
if f.Label == "Names" {
|
||||
return f.LabelTag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if tag := names(page); tag == nil || tag.Label != "matches" {
|
||||
t.Errorf("names = %+v", page.CertFacts)
|
||||
}
|
||||
|
||||
c.Banner = "3f2a9c1d0e7b ESMTP"
|
||||
matches = false
|
||||
page = healthPage(view.Meta{}, c, "")
|
||||
if end := page.Hostname.End; len(end) == 0 || end[0].Tag == nil || end[0].Tag.Status != "warn" {
|
||||
t.Errorf("a greeting with a container id: card %+v, want warn", page.Hostname.End)
|
||||
}
|
||||
if tag := names(page); tag == nil || tag.Status != "fail" || tag.Label != "not for the hostname" {
|
||||
t.Errorf("a certificate for another name: %+v", tag)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user