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
+33
View File
@@ -2,6 +2,7 @@ package view
import (
"strconv"
"strings"
"time"
)
@@ -53,6 +54,38 @@ type Health struct {
Configuration Box
Reload Action
// ConfigFacts is the address the panel sees the viewer at
// (WithClientAddress); ConfigProblem says what to do when it is the
// reverse proxy's own.
ConfigFacts []Fact
ConfigProblem Text
}
// WithClientAddress says where the panel sees the viewer's request from:
// client is the address it counts sign-ins against, peer the connection's,
// fromHeader that client came from X-Forwarded-For of a trusted proxy, and
// ignored that the header was there but peer is no trusted proxy - then
// every visitor has the proxy's address and shares one sign-in limit, which
// nothing else shows (auth.Module.Seen; guide § TRUSTED_PROXY_CIDR).
func (h *Health) WithClientAddress(client, peer string, fromHeader, ignored bool) *Health {
f := Fact{Label: "Your address", Value: Plain(client), Mono: true}
switch {
case fromHeader:
f.Note = Plain("From X-Forwarded-For, which the trusted proxy " + peer + " set")
case ignored:
f.Note = Plain("The proxy's own: its X-Forwarded-For is not read")
mask := "/32"
if strings.Contains(peer, ":") {
mask = "/128"
}
h.ConfigProblem = Rich("Every visitor reaches the panel as ", Mono(peer),
", the reverse proxy, and they all share one sign-in limit. Set ", Code("TRUSTED_PROXY_CIDR"),
" to ", Code(peer+mask), " - or the proxy's network - and restart the container.")
default:
f.Note = Plain("The connection's own; no proxy in between said otherwise")
}
h.ConfigFacts = []Fact{f}
return h
}
// SenderCheckRow is one period of the Sender check table.