panel-dkim-signing stage 2: OpenDKIM removed
test / test (push) Canceled after 0s

The panel's signer is the only one. Gone: the opendkim package and its
configuration, the syslog-relay and opendkim programs, /dev/log and
/run/opendkim, the reload by USR1 through supervisord
(internal/supervisor/supervisor.go), internal/syslogrelay, the port of
OpenDKIM's From: parser (internal/milter/mailparse.go and its probe,
harness and test/upstream scripts), OPENDKIM_SOCKET and OPENDKIM_DIR.

Keys: internal/domain/keys.go (Keys, NewKeys, DKIM_DIR=/data/dkim) keeps
the keys and writes a selector table, /data/dkim/selectors ("<name>
<selector>"), which the signer reads in place of the KeyTable - it reads
no database, as the sender check reads none. The table is written before
the signer serves (EnsureServerSigned moved there from the notifier).
Keys and table 0600, directories 0700, the panel's. The entrypoint copies
/data/opendkim/keys once, as the panel user, into /data/dkim.new renamed
when whole, and stops the start if the copy fails; /data/opendkim is left
for a rollback to 2.7.x.

Sender check: one RFC 5322 address as a mail program reads it, held
against the bindings; the 256-byte refusal and the texts naming OpenDKIM
go. Allowed() folded the domain through strings.ToLower, which makes the
Kelvin sign a "k" - the comparison with OpenDKIM's reading had hidden it;
it folds ASCII only now.

Health: two dkim-signer rows (applications, notifications) in Milter
sockets, opendkim out of the liveness set; the card no longer takes any
row with a note for the spam filter. Texts as in the design commit.

KILL stays in deploy/docker-compose.yml: measured on the test server,
root without it cannot signal a process of another uid - supervisord
stopping the panel, Postfix's master its children. The plan's "only use
was the reload" was wrong; the comment now gives the real uses.

e2e: one signature, the panel's, verified by dkimpy; the /data/dkim modes;
the OpenDKIM log and process checks, testNeverSignedForAnother and the
cited opendkim, libopendkim11 and libmilter1.0.1 go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-16 01:05:39 +03:00
co-authored by Claude Opus 5.5
parent 005922bc7d
commit faa3c11ae7
96 changed files with 971 additions and 2247 deletions
+27 -8
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"regexp"
"slices"
"strconv"
"strings"
"sync"
@@ -346,19 +347,24 @@ func healthCards(c healthReadings) []view.HealthCard {
}
// The line under the count has to fit a card: the sockets that do not
// answer when some do not; otherwise their names, or - with the inbound spam
// filter among them, when three names no longer fit - that it is included.
// answer when some do not; otherwise their names - the signer's two once -
// or, with the inbound spam filter among them, when three names no longer
// fit, that it is included.
answering, socketNames, silent, withFilter := 0, make([]string, 0, len(sockets)), []string{}, false
for _, s := range sockets {
if s.Present {
answering++
} else if s.Note != "" && s.Name != filterName {
silent = append(silent, s.Name+" · "+s.Note)
} else {
silent = append(silent, s.Name)
}
if s.Note != "" {
if s.Name == filterName {
withFilter = true
}
socketNames = append(socketNames, s.Name)
if !slices.Contains(socketNames, s.Name) {
socketNames = append(socketNames, s.Name)
}
}
socketSub := strings.Join(socketNames, ", ")
switch {
@@ -455,7 +461,8 @@ func (h *Handlers) healthChecks() healthReadings {
cert := health.CheckServedCertificate(h.cfg.TLSServedCertFile, h.cfg.TLSCertFile, h.cfg.TLSReloadInterval, time.Now())
sockets := []health.Socket{
health.CheckSocket("OpenDKIM", h.cfg.OpenDKIMSocket, true),
signerSocket(h.cfg.SignSocket, "applications"),
signerSocket(h.cfg.SignLocalSocket, "notifications"),
health.CheckSocket("sender-check", h.cfg.SenderSocket, true),
health.CheckSocket("send-log", h.cfg.JournalSocket, false),
}
@@ -486,15 +493,27 @@ func (h *Handlers) healthChecks() healthReadings {
}
}
// filterName is the inbound spam filter's row among the milter sockets.
const filterName = "Spam filter"
// signerSocket is a row of the DKIM signer's: one socket for what
// applications send, one for the panel's own notifications. Postfix attaches
// both strictly, so either one down defers the mail that goes through it.
func signerSocket(path, note string) health.Socket {
s := health.CheckSocket("dkim-signer", path, true)
s.Note = note
return s
}
// filterSocket is the inbound spam filter's row among the milter sockets. A
// filter that is down costs what the server's setting says: with the action
// tempfail Postfix defers inbound mail (an error, like OpenDKIM), otherwise it
// tempfail Postfix defers inbound mail (an error, like the signer), otherwise it
// accepts the mail unfiltered (a warning, like the send log). The address is the
// one in INBOUND_ANTISPAM_MILTER as configured; Health and Overview, the only
// pages that show it, are for the global administrator.
func filterSocket(milter, action string) health.Socket {
tempfail := action == "tempfail"
s := health.CheckMilter("Spam filter", milter, tempfail)
s := health.CheckMilter(filterName, milter, tempfail)
s.Note = "inbound"
switch {
case s.Status != health.StatusError && s.Status != health.StatusWarn:
@@ -519,7 +538,7 @@ func queueSummary(out string) string {
func statusFlash(r *http.Request) string {
switch {
case r.URL.Query().Get("reloaded") != "":
return "Configuration regenerated from the database - OpenDKIM's tables, every map the panel writes for Postfix, the filter lists - and OpenDKIM and Postfix have re-read it."
return "Configuration regenerated from the database - the DKIM selector table, every map the panel writes for Postfix, the filter lists - and Postfix has re-read it."
case r.URL.Query().Get("rechecked") != "":
return "DNS re-checked."
default: