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
+21 -29
View File
@@ -88,42 +88,34 @@ func verifyDKIM(raw []byte, domain string) error {
return nil
}
// checkBothSigners is the oracle of docs/plans/panel-dkim-signing.md, stage
// 1: the panel's signer and OpenDKIM both sign (OpenDKIM after the panel in
// the chain, so its signature is the top one - build/postfix-config.sh);
// the two must agree on what they sign - the same h= and bh= - and each
// must verify with dkimpy (python3-dkim in the sink image), an
// implementation that is neither go-msgauth, the panel's library, nor
// OpenDKIM. name is the file in the sink's /mail.
func checkBothSigners(s *stack, name string, raw []byte, domain string) error {
// checkPanelSignature is the oracle of docs/plans/panel-dkim-signing.md: the
// message carries one DKIM signature, the panel's, for domain - signed as
// OpenDKIM signed until 2.8.0 (relaxed/simple, rsa-sha256, From over-signed)
// - and dkimpy (python3-dkim in the sink image), an implementation that is
// not go-msgauth, the panel's library, verifies it. Stage 1 compared it with
// OpenDKIM's on every message before OpenDKIM left. name is the file in the
// sink's /mail.
func checkPanelSignature(s *stack, name string, raw []byte, domain string) error {
msg, err := mail.ReadMessage(bytes.NewReader(raw))
if err != nil {
return fmt.Errorf("parse the delivered message: %w", err)
}
var sigs []map[string]string
for _, v := range msg.Header["Dkim-Signature"] {
tags := dkimTags(v)
if tags["d"] == domain {
sigs = append(sigs, tags)
sigs := msg.Header["Dkim-Signature"]
if len(sigs) != 1 {
return fmt.Errorf("%d DKIM signatures, want 1, the panel's", len(sigs))
}
tags := dkimTags(sigs[0])
for tag, want := range map[string]string{"d": domain, "c": "relaxed/simple", "a": "rsa-sha256"} {
if tags[tag] != want {
return fmt.Errorf("the signature has %s=%q, want %q", tag, tags[tag], want)
}
}
if len(sigs) != 2 {
return fmt.Errorf("%d DKIM signatures for %s, want 2 (the panel's and OpenDKIM's)", len(sigs), domain)
if h := strings.ToLower(tags["h"]); !strings.HasPrefix(h, "from:") || !strings.HasSuffix(h, ":from") {
return fmt.Errorf("the signature's h=%q does not sign From and over-sign it", tags["h"])
}
for _, tag := range []string{"bh", "h", "c", "s", "a"} {
a, b := sigs[0][tag], sigs[1][tag]
if tag == "h" {
a, b = strings.ToLower(a), strings.ToLower(b)
}
if a != b {
return fmt.Errorf("the signatures differ in %s=: OpenDKIM %q, the panel %q", tag, a, b)
}
}
for i := range sigs {
out, err := s.execIn("sink", "sh", "-c", fmt.Sprintf("dkimverify --index %d < '/mail/%s'", i, name))
if err != nil || !strings.Contains(out, "signature ok") {
return fmt.Errorf("dkimpy on signature %d (%s): %v\n%s", i, []string{"OpenDKIM's", "the panel's"}[i], err, out)
}
out, err := s.execIn("sink", "sh", "-c", fmt.Sprintf("dkimverify --index 0 < '/mail/%s'", name))
if err != nil || !strings.Contains(out, "signature ok") {
return fmt.Errorf("dkimpy: %v\n%s", err, out)
}
return nil
}