feat: add SMTP submission listener and enhance stalwart configuration (#425)

- add SMTP submission listener on port 587 with TLS requirement
- configure HAProxy frontend/backend for submission with send-proxy-v2 support
- add send-proxy-v2 support to all listeners
- add dynamic HAProxy node discovery for proxy trusted networks
- use service hostname instead of node FQDN for autoconfig/autodiscover
- remove redundant IMAP/IMAPS/SMTP alt-names from TLS certificates
- update VRRP CNAME configuration to use mail.main.unkin.net

Reviewed-on: #425
This commit is contained in:
Ben Vincent 2025-11-09 18:48:06 +11:00
parent 35614060bd
commit 9eff241003
7 changed files with 112 additions and 9 deletions

View File

@ -13,8 +13,7 @@ profiles::haproxy::dns::vrrp_cnames:
- dashboard.ceph.unkin.net
- mail-webadmin.main.unkin.net
- mail-in.main.unkin.net
- imap.main.unkin.net
- imaps.main.unkin.net
- mail.main.unkin.net
- autoconfig.main.unkin.net
- autodiscover.main.unkin.net
@ -333,7 +332,7 @@ profiles::haproxy::backends:
stick-table: 'type ip size 200k expire 30m'
stick: 'on src'
tcp-check:
- connect port 143
- connect port 143 send-proxy
- expect string "* OK"
- send "A001 STARTTLS\r\n"
- expect rstring "A001 (OK|2.0.0)"
@ -349,7 +348,7 @@ profiles::haproxy::backends:
stick-table: 'type ip size 200k expire 30m'
stick: 'on src'
tcp-check:
- connect ssl
- connect ssl send-proxy
- expect string "* OK"
be_stalwart_smtp:
description: Backend for Stalwart SMTP
@ -363,7 +362,21 @@ profiles::haproxy::backends:
stick-table: 'type ip size 200k expire 30m'
stick: 'on src'
tcp-check:
- connect port 25
- connect port 25 send-proxy
- expect string "220 "
be_stalwart_submission:
description: Backend for Stalwart SMTP Submission
collect_exported: false
options:
mode: tcp
balance: roundrobin
option:
- tcp-check
- prefer-last-server
stick-table: 'type ip size 200k expire 30m'
stick: 'on src'
tcp-check:
- connect port 587 send-proxy
- expect string "220 "
profiles::haproxy::certlist::enabled: true

View File

@ -196,6 +196,17 @@ profiles::haproxy::frontends:
tcp-request:
- inspect-delay 5s
- content accept if { req_len 0 }
fe_submission:
description: 'Frontend for Stalwart SMTP Submission'
bind:
0.0.0.0:587: []
mode: 'tcp'
options:
log: global
default_backend: be_stalwart_submission
tcp-request:
- inspect-delay 5s
- content accept if { req_len 0 }
profiles::haproxy::backends:
be_letsencrypt:

View File

@ -8,9 +8,6 @@ hiera_include:
profiles::pki::vault::alt_names:
- mail.main.unkin.net
- mail-webadmin.main.unkin.net
- imap.main.unkin.net
- imaps.main.unkin.net
- smtp.main.unkin.net
- main-in.main.unkin.net
- autoconfig.main.unkin.net
- autodiscovery.main.unkin.net
@ -41,6 +38,7 @@ stalwart::s3_region: "%{facts.region}"
stalwart::domains:
- 'mail.unkin.net'
stalwart::postfix_relay_host: 'out-mta.main.unkin.net'
stalwart::service_hostname: 'mail.main.unkin.net'
stalwart::manage_dns_records: false
## With load balancer:

View File

@ -29,6 +29,7 @@ class stalwart::config {
content => epp('stalwart/config.toml.epp', {
'cluster_size' => $stalwart::cluster_size,
'other_cluster_members' => $stalwart::other_cluster_members,
'haproxy_ips' => $stalwart::haproxy_ips,
'effective_node_id' => $stalwart::effective_node_id,
'bind_address' => $stalwart::bind_address,
'advertise_address' => $stalwart::advertise_address,
@ -49,10 +50,12 @@ class stalwart::config {
'enable_imap' => $stalwart::enable_imap,
'enable_imap_tls' => $stalwart::enable_imap_tls,
'enable_http' => $stalwart::enable_http,
'enable_smtp_submission' => $stalwart::enable_smtp_submission,
'data_dir' => $stalwart::data_dir,
'tls_cert' => $stalwart::tls_cert,
'tls_key' => $stalwart::tls_key,
'log_level' => $stalwart::log_level,
'service_hostname' => $stalwart::service_hostname,
'fallback_admin_user' => $stalwart::fallback_admin_user,
'fallback_admin_password' => $stalwart::fallback_admin_password,
'webadmin_unpack_path' => $stalwart::webadmin_unpack_path,

View File

@ -86,6 +86,15 @@
# @param enable_smtp_relay
# Enable SMTP for postfix relay communication
#
# @param enable_smtp_submission
# Enable SMTP submission listener on port 587
#
# @param haproxy_role
# Role name for HAProxy nodes to include in proxy trusted networks
#
# @param service_hostname
# Service hostname used for autoconfig/autodiscover and SMTP greeting
#
# @param package_ensure
# Package version to install
#
@ -133,6 +142,9 @@ class stalwart (
Boolean $enable_imap_tls = true,
Boolean $enable_http = true,
Boolean $enable_smtp_relay = true,
Boolean $enable_smtp_submission = true,
String $haproxy_role = 'roles::infra::halb::haproxy2',
Stdlib::Fqdn $service_hostname = $facts['networking']['fqdn'],
String $package_ensure = 'present',
Stdlib::Absolutepath $config_dir = '/opt/stalwart/etc',
Stdlib::Absolutepath $data_dir = '/var/lib/stalwart',
@ -166,6 +178,14 @@ class stalwart (
$other_cluster_members = $sorted_cluster_members.filter |$member| { $member != $my_fqdn }
$cluster_size = length($sorted_cluster_members)
# Query HAProxy nodes for proxy trusted networks
$haproxy_query = "enc_role='${haproxy_role}' and country='${facts['country']}' and region='${facts['region']}'"
$haproxy_members_raw = query_nodes($haproxy_query, 'networking.ip')
$haproxy_ips = $haproxy_members_raw ? {
undef => [],
default => sort($haproxy_members_raw),
}
# Extract last 4 digits from hostname (e.g., ausyd1nxvm1234 -> 1234)
if $hostname =~ /^.*(\d{4})$/ {
$hostname_digits = $1

View File

@ -2,7 +2,7 @@
# Generated by Puppet - DO NOT EDIT MANUALLY
[server]
hostname = "<%= $node_facts['networking']['fqdn'] %>"
hostname = "<%= $service_hostname %>"
greeting = "Stalwart ESMTP"
[server.listener."smtp-relay"]
@ -10,10 +10,33 @@ bind = ["<%= $bind_address %>:25"]
protocol = "smtp"
greeting = "Stalwart SMTP Relay"
<% if !$haproxy_ips.empty { -%>
[server.listener."smtp-relay".proxy]
trusted-networks = ["127.0.0.0/8", "::1"<% $haproxy_ips.each |$ip| { %>, "<%= $ip %>"<% } %>]
<% } -%>
<% if $enable_smtp_submission { -%>
[server.listener."submission"]
bind = ["<%= $bind_address %>:587"]
protocol = "smtp"
greeting = "Stalwart SMTP Submission"
tls.require = true
<% if !$haproxy_ips.empty { -%>
[server.listener."submission".proxy]
trusted-networks = ["127.0.0.0/8", "::1"<% $haproxy_ips.each |$ip| { %>, "<%= $ip %>"<% } %>]
<% } -%>
<% } -%>
<% if $enable_imap { -%>
[server.listener."imap"]
bind = ["<%= $bind_address %>:143"]
protocol = "imap"
<% if !$haproxy_ips.empty { -%>
[server.listener."imap".proxy]
trusted-networks = ["127.0.0.0/8", "::1"<% $haproxy_ips.each |$ip| { %>, "<%= $ip %>"<% } %>]
<% } -%>
<% } -%>
<% if $enable_imap_tls { -%>
@ -21,6 +44,11 @@ protocol = "imap"
bind = ["<%= $bind_address %>:993"]
protocol = "imap"
tls.implicit = true
<% if !$haproxy_ips.empty { -%>
[server.listener."imaps".proxy]
trusted-networks = ["127.0.0.0/8", "::1"<% $haproxy_ips.each |$ip| { %>, "<%= $ip %>"<% } %>]
<% } -%>
<% } -%>
<% if $enable_http { -%>
@ -28,6 +56,11 @@ tls.implicit = true
bind = ["<%= $bind_address %>:443"]
protocol = "http"
tls.implicit = true
<% if !$haproxy_ips.empty { -%>
[server.listener."https".proxy]
trusted-networks = ["127.0.0.0/8", "::1"<% $haproxy_ips.each |$ip| { %>, "<%= $ip %>"<% } %>]
<% } -%>
<% } -%>
[server.tls]
@ -35,6 +68,7 @@ enable = true
implicit = false
certificate = "default"
[webadmin]
path = "<%= $webadmin_unpack_path %>"
auto-update = <%= $webadmin_auto_update %>
@ -167,6 +201,12 @@ directory = "internal"
[imap.protocol]
max-requests = 64
# Inbound rate limiting
[[queue.limiter.inbound]]
key = ["remote_ip"]
rate = "500/1s"
enable = true
# SMTP configuration for postfix relay
[session.data]
pipe.command = "sendmail"
@ -212,6 +252,7 @@ max-message-size = 52428800
[certificate."default"]
cert = "%{file:<%= $tls_cert %>}%"
private-key = "%{file:<%= $tls_key %>}%"
default = true
# Logging configuration
[tracer]

View File

@ -15,6 +15,7 @@ class profiles::stalwart::haproxy (
'inter 2s',
'rise 3',
'fall 2',
'send-proxy-v2',
]
}
@ -27,6 +28,7 @@ class profiles::stalwart::haproxy (
'inter 3s',
'rise 2',
'fall 3',
'send-proxy-v2',
]
}
@ -41,6 +43,7 @@ class profiles::stalwart::haproxy (
'inter 3s',
'rise 2',
'fall 3',
'send-proxy-v2',
]
}
@ -53,6 +56,20 @@ class profiles::stalwart::haproxy (
'inter 3s',
'rise 2',
'fall 3',
'send-proxy-v2',
]
}
# smtp submission
profiles::haproxy::balancemember { "${facts['networking']['fqdn']}_587":
service => 'be_stalwart_submission',
ports => [587],
options => [
'check',
'inter 3s',
'rise 2',
'fall 3',
'send-proxy-v2',
]
}