diff --git a/hieradata/country/au/region/syd1/infra/halb/haproxy2.yaml b/hieradata/country/au/region/syd1/infra/halb/haproxy2.yaml index eaff76b..66e37a6 100644 --- a/hieradata/country/au/region/syd1/infra/halb/haproxy2.yaml +++ b/hieradata/country/au/region/syd1/infra/halb/haproxy2.yaml @@ -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 diff --git a/hieradata/roles/infra/halb/haproxy2.yaml b/hieradata/roles/infra/halb/haproxy2.yaml index f58be10..10d19c6 100644 --- a/hieradata/roles/infra/halb/haproxy2.yaml +++ b/hieradata/roles/infra/halb/haproxy2.yaml @@ -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: diff --git a/hieradata/roles/infra/mail/backend.yaml b/hieradata/roles/infra/mail/backend.yaml index ae937b0..c26c200 100644 --- a/hieradata/roles/infra/mail/backend.yaml +++ b/hieradata/roles/infra/mail/backend.yaml @@ -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: diff --git a/modules/stalwart/manifests/config.pp b/modules/stalwart/manifests/config.pp index 2b43ea5..95aa763 100644 --- a/modules/stalwart/manifests/config.pp +++ b/modules/stalwart/manifests/config.pp @@ -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, diff --git a/modules/stalwart/manifests/init.pp b/modules/stalwart/manifests/init.pp index 27b2671..093e960 100644 --- a/modules/stalwart/manifests/init.pp +++ b/modules/stalwart/manifests/init.pp @@ -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 diff --git a/modules/stalwart/templates/config.toml.epp b/modules/stalwart/templates/config.toml.epp index 83c2c14..b432dea 100644 --- a/modules/stalwart/templates/config.toml.epp +++ b/modules/stalwart/templates/config.toml.epp @@ -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] diff --git a/site/profiles/manifests/stalwart/haproxy.pp b/site/profiles/manifests/stalwart/haproxy.pp index e8375e9..82a3734 100644 --- a/site/profiles/manifests/stalwart/haproxy.pp +++ b/site/profiles/manifests/stalwart/haproxy.pp @@ -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', ] }