postfix: ETRN refused and not offered (audit S6)
Security audit 2026-10-17, finding S6 (low), stage 0 - proven from outside: `ETRN <an inbound domain>` on port 25 answered "250 Queuing started". smtpd_etrn_restrictions was empty (Postfix allows every client) and fast_flush_domains = $relay_domains, so a stranger could flush the deferred queue toward an inbound domain's upstream at will. Now smtpd_etrn_restrictions=reject (etrn_cmd -> smtpd_check_etrn -> reject_code 554, Postfix 3.10.13 src/smtpd/smtpd.c) and smtpd_discard_ehlo_keywords=etrn. e2e: testInboundRelay sends EHLO + ETRN for the inbound domain on port 25 and expects 554 and no ETRN in the EHLO reply (etrnPort25); checkPostfixHardening reads both settings back. To be run on the server. Docs: architecture.md § Postfix; CHANGELOG; plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,37 @@ func waitForPort25Ready(timeout time.Duration) error {
|
||||
})
|
||||
}
|
||||
|
||||
// etrnPort25 sends EHLO and then ETRN domain on port 25 and returns ETRN's
|
||||
// reply code and what EHLO offered.
|
||||
func etrnPort25(domain string) (int, string, error) {
|
||||
conn, err := net.DialTimeout("tcp", port25Addr, 10*time.Second)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("dial %s: %w", port25Addr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
tp := textproto.NewConn(conn)
|
||||
if _, _, err := tp.ReadResponse(220); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if err := tp.PrintfLine("EHLO mx.stranger.e2e.test"); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
_, ehlo, err := tp.ReadResponse(250)
|
||||
if err != nil {
|
||||
return 0, ehlo, err
|
||||
}
|
||||
if err := tp.PrintfLine("ETRN %s", domain); err != nil {
|
||||
return 0, ehlo, err
|
||||
}
|
||||
code, _, err := tp.ReadResponse(0)
|
||||
_ = tp.PrintfLine("QUIT")
|
||||
if code != 0 {
|
||||
err = nil // any code is the answer asked for
|
||||
}
|
||||
return code, ehlo, err
|
||||
}
|
||||
|
||||
// deliverPort25Raw is deliverPort25 with the DATA bytes written as they
|
||||
// stand. net/smtp's data writer would turn a bare LF into CRLF and stuff a
|
||||
// lone "." (net/textproto, DotWriter), which is the very input this is for.
|
||||
|
||||
Reference in New Issue
Block a user