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"
|
||||
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.
|
||||
- **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
|
||||
("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
|
||||
|
||||
+19
-4
@@ -13,12 +13,25 @@
|
||||
# a fixed literal or a container environment variable.
|
||||
set -eu
|
||||
|
||||
# postconf -P refuses master.cf -o values that contain whitespace; inbound port
|
||||
# 25 needs restriction lists and milter specs that include spaces. Patch the
|
||||
# smtp/inet service stanza directly instead.
|
||||
# Sets one -o option of the smtp/inet (port 25) service by patching its stanza.
|
||||
#
|
||||
# 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() {
|
||||
param="$1"
|
||||
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" '
|
||||
/^smtp[ \t]+inet[ \t]/ && !done {
|
||||
print
|
||||
@@ -284,7 +297,9 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
||||
RELAY_DOMAINS_SETTING="texthash:${DMARC_RELAY_DOMAINS_MAP}"
|
||||
TRANSPORT_SETTING="texthash:${DMARC_TRANSPORT_MAP}"
|
||||
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
|
||||
postconf -e \
|
||||
"relay_domains=${RELAY_DOMAINS_SETTING}" \
|
||||
|
||||
@@ -47,7 +47,7 @@ var port25Cases = []port25Case{
|
||||
"DMARC_REPORTS_ENABLE=true",
|
||||
},
|
||||
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},
|
||||
},
|
||||
},
|
||||
@@ -58,7 +58,7 @@ var port25Cases = []port25Case{
|
||||
"DMARC_REPORTS_ENABLE=true",
|
||||
},
|
||||
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},
|
||||
},
|
||||
},
|
||||
@@ -116,6 +116,18 @@ func checkPort25ConfigCombinations(s *stack) error {
|
||||
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)
|
||||
}
|
||||
// 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