postfix-config: report addresses are relay recipients when the relay is on

Found by talking SMTP to port 25 of the test server once it answered
again (2026-10-10): RCPT TO:<dmarc-reports+bs.mixdep.ru@selfpost.mixfed.ru>
was refused with "550 5.1.1 User unknown in relay recipient table".

With INBOUND_RELAY_ENABLE and DMARC_REPORTS_ENABLE both true the server's
own name is a relay domain (for the reports) and relay_recipient_maps
lists the relay's recipients only. smtpd_reject_unlisted_recipient
applies after the restriction list, so the OK that
check_recipient_access gives a report address does not help: every
report was refused. The table of report addresses is added to
relay_recipient_maps in that combination.

Checked on the server by setting the parameter in the running container
and reloading Postfix: the domain's report address is answered 250, the
former server-wide mailbox and an unknown local part 550, a foreign
domain and an inbound domain without recipients 554. e2e: the combined
case requires relay_recipient_maps to name both tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-11 00:05:40 +03:00
co-authored by Claude Opus 5.5
parent 80748e6c94
commit 36db4223f2
3 changed files with 37 additions and 1 deletions
+6
View File
@@ -227,6 +227,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
value with whitespace, and the e2e check asks Postfix what it reads. value with whitespace, and the e2e check asks Postfix what it reads.
Present since DMARC ingest was fitted into the 1.9.1 fix of the same Present since DMARC ingest was fitted into the 1.9.1 fix of the same
stanza. stanza.
- **DMARC reports are accepted with the inbound relay on.** With both
features enabled every report address was refused as "User unknown in
relay recipient table": the relay's unlisted-recipient check runs after the
restriction list, and the report addresses - on the server's own name, a
relay domain - were not in `relay_recipient_maps`. Their table is a
recipient table now. Hidden until now by the port-25 failure above.
- Overview's Processes card counts the programs that are meant to stay up - Overview's Processes card counts the programs that are meant to stay up
("6 of 6 running"); the idle one-shot `postfix-reload` was counted as ("6 of 6 running"); the idle one-shot `postfix-reload` was counted as
running. The Milter sockets card no longer clips its second line with three running. The Milter sockets card no longer clips its second line with three
+12 -1
View File
@@ -306,8 +306,19 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
"transport_maps=${TRANSPORT_SETTING}" \ "transport_maps=${TRANSPORT_SETTING}" \
"smtpd_reject_unlisted_recipient=yes" "smtpd_reject_unlisted_recipient=yes"
if [ "${INBOUND_RELAY_ENABLE}" = "true" ]; then if [ "${INBOUND_RELAY_ENABLE}" = "true" ]; then
# With the relay on, Postfix refuses every recipient of a relay domain
# that relay_recipient_maps does not list (smtpd_reject_unlisted_recipient
# above) - and it does so after the restriction list, whatever
# check_recipient_access answered. The DMARC report addresses live on a
# relay domain too (the server's own name), so their table is a recipient
# table as well; without it they are "User unknown in relay recipient
# table" and no report is ever accepted while both features are on.
RELAY_RECIPIENT_SETTING="texthash:${RELAY_RECIPIENTS_MAP}"
if [ "${DMARC_REPORTS_ENABLE}" = "true" ]; then
RELAY_RECIPIENT_SETTING="${RELAY_RECIPIENT_SETTING} texthash:${DMARC_RECIPIENTS_MAP}"
fi
postconf -e \ postconf -e \
"relay_recipient_maps=texthash:${RELAY_RECIPIENTS_MAP}" \ "relay_recipient_maps=${RELAY_RECIPIENT_SETTING}" \
"smtp_tls_policy_maps=texthash:${TLS_POLICY_MAP}" "smtp_tls_policy_maps=texthash:${TLS_POLICY_MAP}"
else else
postconf -e \ postconf -e \
+19
View File
@@ -22,6 +22,9 @@ type port25Case struct {
name string name string
env []string env []string
check []optionCheck check []optionCheck
// main are main.cf parameters the combination must end up with, read with
// postconf -h.
main []optionCheck
} }
var port25Cases = []port25Case{ var port25Cases = []port25Case{
@@ -61,6 +64,13 @@ var port25Cases = []port25Case{
{param: "smtpd_recipient_restrictions", contains: "check_recipient_access,texthash:/data/postfix/dmarc_recipients,reject_unauth_destination,reject_unlisted_recipient", wantExact: true}, {param: "smtpd_recipient_restrictions", contains: "check_recipient_access,texthash:/data/postfix/dmarc_recipients,reject_unauth_destination,reject_unlisted_recipient", wantExact: true},
{param: "smtpd_milters", contains: "", wantExact: true}, {param: "smtpd_milters", contains: "", wantExact: true},
}, },
// Both on: the report addresses sit on a relay domain, and the relay's
// unlisted-recipient check runs after the restriction list. Unless their
// table is a recipient table too, every report is refused as "User
// unknown in relay recipient table".
main: []optionCheck{
{param: "relay_recipient_maps", contains: "texthash:/data/postfix/relay_recipients texthash:/data/postfix/dmarc_recipients", wantExact: true},
},
}, },
{ {
name: "inbound relay with antispam milter", name: "inbound relay with antispam milter",
@@ -129,6 +139,15 @@ func checkPort25ConfigCombinations(s *stack) error {
return fmt.Errorf("%s: master.cf has -o %s = %q but Postfix reads %q - the value contains whitespace", tc.name, c.param, got, strings.TrimSpace(seen)) return fmt.Errorf("%s: master.cf has -o %s = %q but Postfix reads %q - the value contains whitespace", tc.name, c.param, got, strings.TrimSpace(seen))
} }
} }
for _, c := range tc.main {
got, err := s.execIn("selfpost", "postconf", "-h", c.param)
if err != nil {
return fmt.Errorf("%s: postconf -h %s: %v\n%s", tc.name, c.param, err, got)
}
if got = strings.TrimSpace(got); got != c.contains {
return fmt.Errorf("%s: %s = %q, want %q", tc.name, c.param, got, c.contains)
}
}
} }
// Back to the stand's own configuration: flags off, no port-25 listener. // Back to the stand's own configuration: flags off, no port-25 listener.