postfix-config: no whitespace in the port-25 service's -o values
Seen in the System log of the test server (2026-10-10): on every
connection to port 25
postfix/smtpd: fatal: unexpected command-line argument:
texthash:/data/postfix/dmarc_recipients,reject_unauth_destination,...
postfix/master: warning: .../smtpd: bad command startup -- throttling
With DMARC_REPORTS_ENABLE=true the script wrote
-o smtpd_recipient_restrictions=check_recipient_access texthash:...,...
into master.cf. Master splits a service line on whitespace, so smtpd got
the option cut at the space plus a stray argument and died at start:
port 25 greeted nobody - no inbound relay, no DMARC reports. postconf -P
smtp/inet/smtpd_recipient_restrictions showed the cut value,
"check_recipient_access". The server's logs have it under the previous
image too (2026-10-09), so it is not from the redesign; postfix check
does not notice, and the e2e check compared the text of master.cf with
an expectation that had the space in it.
- The restriction list is written with commas only (Postfix splits it
on commas as well as whitespace).
- master_cf_set_smtp_inet_o stops the container on a value with
whitespace instead of writing it.
- e2e: for every option it checks, the port-25 case also asks
postconf -P and requires Postfix to read the same value.
Not fixed here: Postfix's start-time warnings "not owned by root" for
the queue directory and its pid/ - the entrypoint's recursive chown
again; harmless, logged at every start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -216,6 +216,17 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
showq service: Permission denied"), the Queue card said "Could not be read"
|
showq service: Permission denied"), the Queue card said "Could not be read"
|
||||||
and the queue page had no listing. Present since the persistent queue of
|
and the queue page had no listing. Present since the persistent queue of
|
||||||
1.3.0; mail flow was not affected. The e2e suite opens the queue page now.
|
1.3.0; mail flow was not affected. The e2e suite opens the queue page now.
|
||||||
|
- **Port 25 answers again with DMARC report ingest on.** The generated
|
||||||
|
`master.cf` gave the port-25 service `-o smtpd_recipient_restrictions=
|
||||||
|
check_recipient_access texthash:…,…` with a space in the value; Postfix
|
||||||
|
cuts a service line at whitespace, so `smtpd` was started with a stray
|
||||||
|
argument and died with "fatal: unexpected command-line argument" on every
|
||||||
|
connection - no inbound relay and no DMARC reports were accepted while
|
||||||
|
`DMARC_REPORTS_ENABLE=true`, although `postfix check` and the suite were
|
||||||
|
silent. The list is written with commas only, the generator refuses a
|
||||||
|
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
|
||||||
|
stanza.
|
||||||
- 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
|
||||||
|
|||||||
+19
-4
@@ -13,12 +13,25 @@
|
|||||||
# a fixed literal or a container environment variable.
|
# a fixed literal or a container environment variable.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
# postconf -P refuses master.cf -o values that contain whitespace; inbound port
|
# Sets one -o option of the smtp/inet (port 25) service by patching its stanza.
|
||||||
# 25 needs restriction lists and milter specs that include spaces. Patch the
|
#
|
||||||
# smtp/inet service stanza directly instead.
|
# A master.cf -o value must not contain whitespace: master splits the line on
|
||||||
|
# it, so "-o name=check_recipient_access texthash:/x,reject" reaches smtpd as
|
||||||
|
# the option "name=check_recipient_access" plus a stray argument, and smtpd dies
|
||||||
|
# at start with "fatal: unexpected command-line argument" - port 25 then
|
||||||
|
# answers nobody. (postconf -P refuses such values for the same reason.) Postfix
|
||||||
|
# splits a restriction list on commas as well as on whitespace, so every value
|
||||||
|
# passed here is written with commas only; a value with whitespace is a bug in
|
||||||
|
# this script and stops the container.
|
||||||
master_cf_set_smtp_inet_o() {
|
master_cf_set_smtp_inet_o() {
|
||||||
param="$1"
|
param="$1"
|
||||||
value="$2"
|
value="$2"
|
||||||
|
case "$value" in
|
||||||
|
*[[:space:]]*)
|
||||||
|
echo "FATAL: master.cf -o ${param} would contain whitespace: ${value}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
awk -v p="$param" -v v="$value" '
|
awk -v p="$param" -v v="$value" '
|
||||||
/^smtp[ \t]+inet[ \t]/ && !done {
|
/^smtp[ \t]+inet[ \t]/ && !done {
|
||||||
print
|
print
|
||||||
@@ -284,7 +297,9 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
|||||||
RELAY_DOMAINS_SETTING="texthash:${DMARC_RELAY_DOMAINS_MAP}"
|
RELAY_DOMAINS_SETTING="texthash:${DMARC_RELAY_DOMAINS_MAP}"
|
||||||
TRANSPORT_SETTING="texthash:${DMARC_TRANSPORT_MAP}"
|
TRANSPORT_SETTING="texthash:${DMARC_TRANSPORT_MAP}"
|
||||||
fi
|
fi
|
||||||
RECIPIENT_RESTRICTIONS="check_recipient_access texthash:${DMARC_RECIPIENTS_MAP},${RECIPIENT_RESTRICTIONS}"
|
# Comma, not a space, between check_recipient_access and its table: see
|
||||||
|
# master_cf_set_smtp_inet_o.
|
||||||
|
RECIPIENT_RESTRICTIONS="check_recipient_access,texthash:${DMARC_RECIPIENTS_MAP},${RECIPIENT_RESTRICTIONS}"
|
||||||
fi
|
fi
|
||||||
postconf -e \
|
postconf -e \
|
||||||
"relay_domains=${RELAY_DOMAINS_SETTING}" \
|
"relay_domains=${RELAY_DOMAINS_SETTING}" \
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ var port25Cases = []port25Case{
|
|||||||
"DMARC_REPORTS_ENABLE=true",
|
"DMARC_REPORTS_ENABLE=true",
|
||||||
},
|
},
|
||||||
check: []optionCheck{
|
check: []optionCheck{
|
||||||
{param: "smtpd_recipient_restrictions", contains: "check_recipient_access texthash:/data/postfix/dmarc_recipients,reject", wantExact: true},
|
{param: "smtpd_recipient_restrictions", contains: "check_recipient_access,texthash:/data/postfix/dmarc_recipients,reject", wantExact: true},
|
||||||
{param: "smtpd_milters", contains: "", wantExact: true},
|
{param: "smtpd_milters", contains: "", wantExact: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -58,7 +58,7 @@ var port25Cases = []port25Case{
|
|||||||
"DMARC_REPORTS_ENABLE=true",
|
"DMARC_REPORTS_ENABLE=true",
|
||||||
},
|
},
|
||||||
check: []optionCheck{
|
check: []optionCheck{
|
||||||
{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},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -116,6 +116,18 @@ func checkPort25ConfigCombinations(s *stack) error {
|
|||||||
case !c.wantExact && !strings.Contains(got, c.contains):
|
case !c.wantExact && !strings.Contains(got, c.contains):
|
||||||
return fmt.Errorf("%s: -o %s = %q, want it to contain %q", tc.name, c.param, got, c.contains)
|
return fmt.Errorf("%s: -o %s = %q, want it to contain %q", tc.name, c.param, got, c.contains)
|
||||||
}
|
}
|
||||||
|
// What the file says is not yet what Postfix reads: master splits
|
||||||
|
// a service line on whitespace, so a value with a space in it is
|
||||||
|
// cut at the space and the rest becomes a stray smtpd argument
|
||||||
|
// ("fatal: unexpected command-line argument", port 25 dead while
|
||||||
|
// `postfix check` stays silent). Ask Postfix itself.
|
||||||
|
seen, err := s.execIn("selfpost", "postconf", "-Ph", "smtp/inet/"+c.param)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: postconf -P smtp/inet/%s: %v\n%s", tc.name, c.param, err, seen)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(seen) != got {
|
||||||
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user