From 9484e8b77b21f7e3d105f8a52b0ff70050b9044e Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 13 Oct 2026 21:14:16 +1100 Subject: [PATCH 1/3] Add local rspamd and redis to the mail edge gateway (#553) Edge gateways need cheap deterministic in-session filtering behind the postfix milter, with no AI and no ClamAV on 1GB VMs. - add a signature-checked rspamd yum repo via artifactapi, enabled for the gateway role - add profiles::rspamd::edge managing the package and local.d files from hiera - fetch rule files without blocking the service: empty placeholder, swap only on a successful changed fetch, then reload - run a self-scanning milter proxy on 11332; reject at 13, header at 6, no greylisting, subject rewrite or gpt - add a local 128MB redis for bayes autolearn; load SpamAssassin rules from rspamd-rules - bypass rspamd for mail relayed out by the smarthost Stacked on #552; requires terraform-artifactapi #63 applied first. Reviewed-on: https://git.unkin.net/unkin/puppet-prod/pulls/553 Co-authored-by: unkin-agent Co-committed-by: unkin-agent --- hieradata/os/AlmaLinux/all_releases.yaml | 12 ++++ hieradata/roles/infra/mail/gateway.yaml | 54 +++++++++++++++++ site/profiles/manifests/rspamd/edge.pp | 68 ++++++++++++++++++++++ site/roles/manifests/infra/mail/gateway.pp | 1 + 4 files changed, 135 insertions(+) create mode 100644 site/profiles/manifests/rspamd/edge.pp diff --git a/hieradata/os/AlmaLinux/all_releases.yaml b/hieradata/os/AlmaLinux/all_releases.yaml index 3ff5da7..c09e0ee 100644 --- a/hieradata/os/AlmaLinux/all_releases.yaml +++ b/hieradata/os/AlmaLinux/all_releases.yaml @@ -147,6 +147,18 @@ profiles::yum::global::repos: baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/postgresql/common/redhat/rhel-%{facts.os.release.major}-%{facts.os.architecture} gpgkey: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/postgresql/keys/PGDG-RPM-GPG-KEY-RHEL + # Rspamd repositories + rspamd: + ensure: absent + name: rspamd + descr: rspamd repository + target: /etc/yum.repos.d/rspamd.repo + baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/rspamd/rpm-stable/centos-%{facts.os.release.major}/x86_64 + gpgkey: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/rspamd/rpm/rspamd.asc + gpgcheck: 1 + repo_gpgcheck: 1 + mirrorlist: absent + # Ceph repositories ceph: ensure: absent diff --git a/hieradata/roles/infra/mail/gateway.yaml b/hieradata/roles/infra/mail/gateway.yaml index 89e459b..0dcd981 100644 --- a/hieradata/roles/infra/mail/gateway.yaml +++ b/hieradata/roles/infra/mail/gateway.yaml @@ -1,4 +1,58 @@ --- +hiera_include: + - redis + +profiles::yum::global::repos: + rspamd: + ensure: present + +# local redis for rspamd bayes and history; bayes tokens carry a ttl so volatile-ttl can evict +redis::maxmemory: '128mb' +redis::maxmemory_policy: 'volatile-ttl' + +# local rspamd, deterministic checks only; module defaults cover spf/dkim/dmarc/arc/rbl/phishing/mime_types/fuzzy_check +profiles::rspamd::edge::rules: + edge.cf: 'https://git.unkin.net/unkin/rspamd-rules/raw/branch/main/edge.cf' +profiles::rspamd::edge::local_d: + worker-proxy.inc: | + bind_socket = "127.0.0.1:11332"; + milter = yes; + timeout = 120s; + count = 1; + upstream "local" { + default = yes; + self_scan = yes; + } + worker-normal.inc: | + enabled = false; + redis.conf: | + servers = "127.0.0.1:6379"; + actions.conf: | + reject = 13; + add_header = 6; + rewrite_subject = null; + greylist = null; + greylist.conf: | + enabled = false; + gpt.conf: | + enabled = false; + classifier-bayes.conf: | + backend = "redis"; + expire = 8640000; + autolearn { + spam_threshold = 6.0; + ham_threshold = -0.5; + check_balance = true; + } + spamassassin.conf: | + ruleset = "/etc/rspamd/rules/*.cf"; + alpha = 0.1; + settings.conf: | + smarthost_outbound { + priority = high; + ip = "103.216.191.185"; + want_spam = yes; + } # base postfix configuration (passed to postfix class) postfix::relayhost: 'direct' diff --git a/site/profiles/manifests/rspamd/edge.pp b/site/profiles/manifests/rspamd/edge.pp new file mode 100644 index 0000000..c3a248e --- /dev/null +++ b/site/profiles/manifests/rspamd/edge.pp @@ -0,0 +1,68 @@ +# local rspamd milter for the mail edge gateways +# - local_d: /etc/rspamd/local.d file name => content +# - rules: /etc/rspamd/rules file name => url, re-fetched every puppet run; fetch failures keep the last good copy +class profiles::rspamd::edge ( + Hash[String, String] $local_d = {}, + Hash[String, Stdlib::HTTPSUrl] $rules = {}, +) { + + package { 'rspamd': + ensure => installed, + } + + $local_d.each |$name, $content| { + file { "/etc/rspamd/local.d/${name}": + ensure => file, + owner => 'root', + group => 'root', + mode => '0644', + content => $content, + require => Package['rspamd'], + notify => Service['rspamd'], + } + } + + file { '/etc/rspamd/rules': + ensure => directory, + owner => 'root', + group => 'root', + mode => '0755', + require => Package['rspamd'], + } + + $rules.each |$name, $url| { + $path = "/etc/rspamd/rules/${name}" + + # empty placeholder so rspamd starts before the first successful fetch + file { $path: + ensure => file, + owner => 'root', + group => 'root', + mode => '0644', + content => '', + replace => false, + } + + # a failed or unchanged fetch skips the swap without failing the run + exec { "rspamd_rules_fetch_${name}": + command => "/usr/bin/mv -f ${path}.new ${path}", + onlyif => "/usr/bin/curl -fsS --max-time 30 -o ${path}.new ${url} && ! /usr/bin/cmp -s ${path}.new ${path}", + require => File[$path], + notify => Exec['rspamd_reload'], + } + } + + # reload keeps the milter answering while config changes apply + service { 'rspamd': + ensure => running, + enable => true, + restart => '/usr/bin/systemctl reload rspamd', + require => Package['rspamd'], + } + + exec { 'rspamd_reload': + command => '/usr/bin/systemctl reload rspamd', + refreshonly => true, + require => Service['rspamd'], + } +} diff --git a/site/roles/manifests/infra/mail/gateway.pp b/site/roles/manifests/infra/mail/gateway.pp index e046920..37ac4e4 100644 --- a/site/roles/manifests/infra/mail/gateway.pp +++ b/site/roles/manifests/infra/mail/gateway.pp @@ -6,6 +6,7 @@ class roles::infra::mail::gateway { }else{ include profiles::defaults include profiles::base + include profiles::rspamd::edge include profiles::postfix::gateway } } From 85a9f470253a358e40f979089278fc996299195b Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 13 Oct 2026 21:18:54 +1100 Subject: [PATCH 2/3] Add edge PTR names to the mail gateway vault certs --- hieradata/nodes/aumel1nxvm2132.main.unkin.net.yaml | 2 ++ hieradata/nodes/ausyd2nxvm2130.main.unkin.net.yaml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/hieradata/nodes/aumel1nxvm2132.main.unkin.net.yaml b/hieradata/nodes/aumel1nxvm2132.main.unkin.net.yaml index b6b8454..de887ee 100644 --- a/hieradata/nodes/aumel1nxvm2132.main.unkin.net.yaml +++ b/hieradata/nodes/aumel1nxvm2132.main.unkin.net.yaml @@ -1,2 +1,4 @@ --- profiles::postfix::gateway::myhostname: 'mta2.edge.unkin.net' +profiles::pki::vault::alt_names: + - mta2.edge.unkin.net diff --git a/hieradata/nodes/ausyd2nxvm2130.main.unkin.net.yaml b/hieradata/nodes/ausyd2nxvm2130.main.unkin.net.yaml index b12ce28..9203cff 100644 --- a/hieradata/nodes/ausyd2nxvm2130.main.unkin.net.yaml +++ b/hieradata/nodes/ausyd2nxvm2130.main.unkin.net.yaml @@ -1,2 +1,4 @@ --- profiles::postfix::gateway::myhostname: 'mta1.edge.unkin.net' +profiles::pki::vault::alt_names: + - mta1.edge.unkin.net From 89efe4f413404a5810bef874391f7e9968287603 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 13 Oct 2026 21:24:23 +1100 Subject: [PATCH 3/3] Relay main.unkin.net through the mail edge gateways --- hieradata/roles/infra/mail/gateway.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hieradata/roles/infra/mail/gateway.yaml b/hieradata/roles/infra/mail/gateway.yaml index 0dcd981..f9644cd 100644 --- a/hieradata/roles/infra/mail/gateway.yaml +++ b/hieradata/roles/infra/mail/gateway.yaml @@ -65,9 +65,11 @@ postfix::manage_aliases: true # postfix map content (templates) profiles::postfix::gateway::relay_recipients_maps: '@unkin.net': 'OK' + '@main.unkin.net': 'OK' profiles::postfix::gateway::relay_domains_maps: 'unkin.net': 'OK' + 'main.unkin.net': 'OK' profiles::postfix::gateway::postscreen_access_maps: '127.0.0.1/32': 'permit' @@ -88,6 +90,10 @@ postfix::transports: ensure: present destination: 'relay' nexthop: '[mail-in.main.unkin.net]:25' + 'main.unkin.net': + ensure: present + destination: 'relay' + nexthop: '[mail-in.main.unkin.net]:25' # postfix virtuals postfix::virtuals: