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:
mix
2026-10-10 23:57:36 +03:00
co-authored by Claude Opus 5.5
parent c5086510f0
commit 80748e6c94
3 changed files with 44 additions and 6 deletions
+11
View File
@@ -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
View File
@@ -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}" \
+14 -2
View File
@@ -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))
}
} }
} }