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; mockups b2dcefc, 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:
mix
2026-10-16 23:51:42 +03:00
co-authored by Claude Opus 5.5
parent 0feaa552d1
commit a8fd96c752
22 changed files with 364 additions and 14 deletions
@@ -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)
}
}