From 061f3819c4fdfba3616f3190478e22823647da5a Mon Sep 17 00:00:00 2001 From: Dan Mahoney Date: Sat, 4 Apr 2026 13:18:28 -0700 Subject: [PATCH 1/4] Fix SSL socket binding bugs and update SSL documentation - --ssl / --ssl-ca-file / --ssl-verify no longer force all -i sockets to SSL when any socket spec uses an explicit ssl: prefix; absence of the prefix now reliably means plain TCP in that case - --ssl-port with no -i args now correctly creates both a plain-TCP listener on --port and an SSL listener on --ssl-port, as the man page has always documented but the code never implemented - Document default paths for --server-key and --server-cert (LOCAL_RULES_DIR/certs/server-{key,cert}.pem) - Clarify --ssl-port implies --ssl - Note that --ssl-verify only checks CA signing; CN matching and CRL checking are not performed and have no options to enable them - Update --ssl-ca-path to reference c_rehash(1) instead of deferring to the IO::Socket::SSL man page Co-Authored-By: Claude Sonnet 4.6 --- spamd/spamd.raw | 84 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/spamd/spamd.raw b/spamd/spamd.raw index 7215d4dbdd..6848ec0c80 100755 --- a/spamd/spamd.raw +++ b/spamd/spamd.raw @@ -810,7 +810,27 @@ my @listen_socket_specs = @{$opt{'listen-sockets'}}; } # supply a default socket (loopback IP address) if none specified -push(@listen_socket_specs, 'localhost') if !@listen_socket_specs; +if (!@listen_socket_specs) { + if (defined $opt{'ssl-port'}) { + # --ssl-port with no -i: implement the documented behaviour of creating + # both a plain-TCP listener on --port and an SSL listener on --ssl-port. + # Previously the code only created a single SSL socket (on --ssl-port), + # silently discarding the plain-TCP port that the man page promised. + push(@listen_socket_specs, 'localhost'); # plain TCP on --port + push(@listen_socket_specs, 'ssl:localhost'); # SSL on --ssl-port + } else { + push(@listen_socket_specs, 'localhost'); + } +} + +# Detect whether any socket spec uses an explicit "ssl:" prefix. When the +# user is mixing SSL and plain-TCP sockets via per-socket prefixes they are +# exercising explicit per-socket SSL control. In that mode the global --ssl +# flag (which may have been implied by --ssl-verify / --ssl-ca-file) must not +# forcibly upgrade non-prefixed sockets to SSL; the absence of a prefix +# unambiguously means "plain TCP". The legacy single-socket --ssl behaviour +# is preserved when no spec carries an explicit "ssl:" prefix. +my $has_explicit_ssl_spec = grep { m{^ssl:}i } @listen_socket_specs; for (@listen_socket_specs) { my $socket_specs = $_; @@ -839,7 +859,15 @@ for (@listen_socket_specs) { (?: : ( [a-z0-9-]* ) )? \z }xsi) { my($proto,$addr,$port) = ($1, $2||$3||$4||$5, $6); $addr = 'localhost' if !defined $addr; - $proto = 'ssl' if defined $opt{'ssl'} || defined $opt{'ssl-port'}; + # Apply the global --ssl / --ssl-port default only when the user has NOT + # used explicit "ssl:" prefixes anywhere in their -i specs. If any spec + # carries an explicit "ssl:" prefix the user is doing per-socket SSL + # control: a spec without the prefix deliberately means plain TCP and + # must not be overridden here (even when --ssl was implied by, e.g., + # --ssl-ca-file). When no spec uses the prefix the legacy behaviour is + # preserved: --ssl (or anything that implies it) makes all sockets SSL. + $proto = 'ssl' if !$has_explicit_ssl_spec && !defined $proto + && (defined $opt{'ssl'} || defined $opt{'ssl-port'}); $proto = !defined($proto) ? '' : lc($proto); $port = $opt{'ssl-port'} if !defined $port && $proto eq 'ssl'; $port = $opt{'port'} if !defined $port || $port eq ''; @@ -3501,11 +3529,12 @@ IPv6 address may be omitted if a port number specification is also omitted. Optionally specifies the port number for the server to listen on (default: 783). -If the B<--ssl> switch is used, and B<--ssl-port> is not supplied, then this -port will be used to accept SSL connections instead of unencrypted connections. -If the B<--ssl> switch is used, and B<--ssl-port> is set, then unencrypted -connections will be accepted on the B<--port> at the same time as encrypted -connections are accepted at B<--ssl-port>. +If the B<--ssl> switch is used without B<--ssl-port>, this port accepts SSL +connections instead of unencrypted connections. If B<--ssl-port> is also set +(which itself implies B<--ssl>), then unencrypted connections are accepted on +B<--port> and encrypted connections are accepted on B<--ssl-port> +simultaneously. Use B<-i> / B<--listen> with an C prefix for finer +control over which addresses and ports carry SSL vs plain-TCP traffic. =item B<-q>, B<--sql-config> @@ -3850,20 +3879,25 @@ home directory instead. =item B<--ssl> -Accept only SSL connections on the associated port. -The B perl module must be installed. +Accept SSL connections. The B perl module must be installed. -If the B<--ssl> switch is used, and B<--ssl-port> is not supplied, then -B<--port> port will be used to accept SSL connections instead of unencrypted -connections. If the B<--ssl> switch is used, and B<--ssl-port> is set, then -unencrypted connections will be accepted on the B<--port>, at the same time as -encrypted connections are accepted at B<--ssl-port>. +When used without B<-i> / B<--listen> and without B<--ssl-port>, the single +default listener (B<--port>) is converted to SSL. When used together with +B<--ssl-port> (which also implies B<--ssl>), two default listeners are +created: a plain-TCP listener on B<--port> and an SSL listener on +B<--ssl-port>. When B<-i> / B<--listen> options are present, B<--ssl> has no +effect on individual sockets — use the C prefix on B<-i> arguments +instead (e.g. C<-i ssl:*:784 -i *:783>). =item B<--ssl-verify> -Implies B<--ssl>. Request a client certificate and verify the certificate. +Implies B<--ssl>. Request a client certificate and verify the certificate. Requires B<--ssl-ca-file> or B<--ssl-ca-path>. +Note that verification is limited to confirming the certificate is signed by +the specified CA. Hostname (CN) matching and CRL checking are not performed, +as spamd has no options to configure either. + =item B<--ssl-ca-file>=I Implies B<--ssl-verify>. Use the specified Certificate Authority @@ -3873,23 +3907,27 @@ be signed by this certificate. =item B<--ssl-ca-path>=I Implies B<--ssl-verify>. Use the Certificate Authority certificate files in -the specified set of directories to verify the client certificate. The -client certificate must be signed by one of these Certificate Authorities. -See the man page for B for additional details. +the specified directory to verify the client certificate. The client +certificate must be signed by one of these Certificate Authorities. The +directory must be hashed in the usual manner (see B(1)). =item B<--ssl-port>=I -Optionally specifies the port number for the server to listen on for -SSL connections (default: whatever --port uses). See B<--ssl> for -more details. +Specifies the port number for SSL connections. Implies B<--ssl>. When no +B<-i> / B<--listen> options are given, spamd listens for plain-TCP +connections on B<--port> and for SSL connections on B<--ssl-port> +simultaneously. When B<-i> options are present, B<--ssl-port> acts only as a +default port for C-prefixed B<-i> specs that omit a port number. =item B<--server-key> I -Specify the SSL key file to use for SSL connections. +Specify the SSL key file to use for SSL connections. Defaults to +F<@@LOCAL_RULES_DIR@@/certs/server-key.pem>. =item B<--server-cert> I -Specify the SSL certificate file to use for SSL connections. +Specify the SSL certificate file to use for SSL connections. Defaults to +F<@@LOCAL_RULES_DIR@@/certs/server-cert.pem>. =item B<--socketpath> I From 5f0f1f173f6d148d9adcbea683093cecab0c2f93 Mon Sep 17 00:00:00 2001 From: Dan Mahoney Date: Sat, 4 Apr 2026 16:41:02 -0700 Subject: [PATCH 2/4] Fix --ssl-port dual-listener expansion to apply to -i addresses too When --ssl-port is set without explicit ssl: prefixes, expand every address spec (whether from -i or the localhost default) into a plain-TCP listener on --port and an SSL listener on --ssl-port. Previously the expansion only ran when no -i options were given; specifying -i without a value (all interfaces) would silently produce only an SSL socket. Update --ssl-port man page entry to document this behaviour and add examples showing explicit ssl:/plain address binding with -i. Co-Authored-By: Claude Sonnet 4.6 --- spamd/spamd.raw | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/spamd/spamd.raw b/spamd/spamd.raw index 6848ec0c80..eed3e3c57a 100755 --- a/spamd/spamd.raw +++ b/spamd/spamd.raw @@ -810,17 +810,17 @@ my @listen_socket_specs = @{$opt{'listen-sockets'}}; } # supply a default socket (loopback IP address) if none specified -if (!@listen_socket_specs) { - if (defined $opt{'ssl-port'}) { - # --ssl-port with no -i: implement the documented behaviour of creating - # both a plain-TCP listener on --port and an SSL listener on --ssl-port. - # Previously the code only created a single SSL socket (on --ssl-port), - # silently discarding the plain-TCP port that the man page promised. - push(@listen_socket_specs, 'localhost'); # plain TCP on --port - push(@listen_socket_specs, 'ssl:localhost'); # SSL on --ssl-port - } else { - push(@listen_socket_specs, 'localhost'); - } +push(@listen_socket_specs, 'localhost') if !@listen_socket_specs; + +# When --ssl-port is set and no spec uses an explicit ssl: prefix, the user +# has explicitly requested both a plain-TCP port and an SSL port. Expand +# each address spec into a plain/SSL pair so that both listeners are created +# on every requested address. The port assignment logic below will use +# --port for the plain socket and --ssl-port for the SSL one. +# This applies whether the addresses came from -i or from the localhost +# default above. +if (defined $opt{'ssl-port'} && !grep { m{^ssl:}i } @listen_socket_specs) { + @listen_socket_specs = map { ($_, "ssl:$_") } @listen_socket_specs; } # Detect whether any socket spec uses an explicit "ssl:" prefix. When the @@ -3913,11 +3913,19 @@ directory must be hashed in the usual manner (see B(1)). =item B<--ssl-port>=I -Specifies the port number for SSL connections. Implies B<--ssl>. When no -B<-i> / B<--listen> options are given, spamd listens for plain-TCP -connections on B<--port> and for SSL connections on B<--ssl-port> -simultaneously. When B<-i> options are present, B<--ssl-port> acts only as a -default port for C-prefixed B<-i> specs that omit a port number. +Specifies the port number for SSL connections. Implies B<--ssl>. When +B<-i> / B<--listen> options are given without explicit C prefixes, +each address is expanded into a plain-TCP listener on B<--port> and an SSL +listener on B<--ssl-port>, matching the no-B<-i> behaviour. + +If you intend to accept SSL connections from non-local clients you should +think carefully about which addresses each listener is bound to. Use +explicit C prefixes on B<-i> arguments if you need independent control +over the address bindings for the plain-TCP and SSL sockets. For example, +C<-i ssl:*:11784 -i *:11783> listens on all interfaces for both SSL and +plain-TCP, while C<-i ssl:*:11784 -i localhost:11783> accepts SSL connections +from any host but restricts plain-TCP to local connections only, avoiding SSL +overhead for processes on the same machine. =item B<--server-key> I From cd2aad7b050049afd3e2f54f8a13c2af2079c7ae Mon Sep 17 00:00:00 2001 From: Dan Mahoney Date: Sat, 4 Apr 2026 16:59:58 -0700 Subject: [PATCH 3/4] Expand --allowed-ips documentation Document that -A replaces the localhost default rather than extending it, so external addresses must be added alongside explicit 127.0.0.1/::1 entries to preserve local access. Note that the allowed-IP list is global across all sockets with no per-socket access control, and suggest OS-level firewall rules as a complement. Document -A 0.0.0.0/0 / ::/0 as the way to allow all addresses, with an appropriate warning. Co-Authored-By: Claude Sonnet 4.6 --- spamd/spamd.raw | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spamd/spamd.raw b/spamd/spamd.raw index eed3e3c57a..1cd14f3432 100755 --- a/spamd/spamd.raw +++ b/spamd/spamd.raw @@ -3762,6 +3762,20 @@ connections from specified test networks and from localhost. In absence of the B<-A> option, connections are only accepted from IP address 127.0.0.1 or ::1, i.e. from localhost on a loopback interface. +To allow connections from any address, use B<-A 0.0.0.0/0> for IPv4 and +B<-A ::/0> for IPv6. Do not do this unless spamd is on a private network +and/or protected by an OS-level firewall. + +If you add external addresses to B<-A> in order to listen on non-loopback +interfaces, you must also explicitly include C<127.0.0.1> and/or C<::1> in +the list if you still want to allow local connections; the default is +replaced, not extended. + +Note that the allowed-IP list is global across all listening sockets — there +is no per-socket access control. A source address permitted by B<-A> may +connect to any socket spamd is listening on, regardless of which interface +that socket is bound to. For tighter control, firewall rules at the OS level +can be used alongside B<-A> as a belt-and-suspenders measure. =item B<-D> [I], B<--debug> [I] From 1e38acd9ea0e9ea706cb5bd30a291e9f8d8f3299 Mon Sep 17 00:00:00 2001 From: Dan Mahoney Date: Sat, 4 Apr 2026 17:32:49 -0700 Subject: [PATCH 4/4] Cross-reference -A from -i documentation Listening on non-loopback addresses is a common source of confusion when connections are still refused due to the default localhost-only -A filter. Add a note to the -i entry pointing users at -A and explaining that the two options are independent. Co-Authored-By: Claude Sonnet 4.6 --- spamd/spamd.raw | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spamd/spamd.raw b/spamd/spamd.raw index 1cd14f3432..278b56433d 100755 --- a/spamd/spamd.raw +++ b/spamd/spamd.raw @@ -3525,6 +3525,12 @@ global --port (and --ssl-port) setting. An IPv6 addresses should be enclosed in square brackets, e.g. [::1]:783. For compatibility square brackets on an IPv6 address may be omitted if a port number specification is also omitted. +Note that listening on non-loopback addresses does not automatically permit +connections from those addresses. The B<-A> / B<--allowed-ips> option +controls which source addresses may connect, and defaults to localhost only +(127.0.0.1 and ::1). If you listen on external interfaces you must add the +appropriate addresses to B<-A> explicitly. + =item B<-p> I, B<--port>=I Optionally specifies the port number for the server to listen on (default: 783).