Compare commits

...

4 Commits

Author SHA1 Message Date
unkin-agent 8c722c36db Adapt base classes for the mail edge gateway
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
2026-10-12 22:38:45 +11:00
unkin-agent 05c1176cdc Run dnsmasq as the mail edge local resolver
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
2026-10-12 22:38:09 +11:00
unkin-agent 7878d35587 Add local rspamd and redis to the mail edge gateway
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
2026-10-12 22:37:36 +11:00
unkin-agent bca6dce407 Fix edge postfix gateway config for unkin.net
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
2026-10-12 22:36:08 +11:00
8 changed files with 260 additions and 110 deletions
@@ -0,0 +1,2 @@
---
profiles::postfix::gateway::myhostname: 'mta2.edge.unkin.net'
@@ -0,0 +1,2 @@
---
profiles::postfix::gateway::myhostname: 'mta1.edge.unkin.net'
+10
View File
@@ -147,6 +147,16 @@ 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} 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 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}/%{facts.os.architecture}
gpgkey: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/rspamd/rpm/rspamd.asc
mirrorlist: absent
# Ceph repositories # Ceph repositories
ceph: ceph:
ensure: absent ensure: absent
+86 -15
View File
@@ -1,26 +1,94 @@
--- ---
lookup_options:
ssh::server::options:
merge:
strategy: deep
# additional altnames hiera_include:
profiles::pki::vault::alt_names: - redis
- in-mta.main.unkin.net
# no consul datacentre at the edge, so nothing to register or scrape via consul
profiles::consul::client::enable: false
exporters::node_exporter::enable: false
# ssh only from the vpc and the lab over the vpn route
ssh::server::options:
AllowUsers: '*@10.0.0.0/8 *@172.16.0.0/12 *@192.168.0.0/16 *@198.18.0.0/16'
# local caching resolver for postfix and rspamd, forwarding to the lab resolver over the vpn route
dnsmasq::no_resolv: true
dnsmasq::cache_size: 10000
dnsmasq::servers:
- 198.18.200.7
profiles::dns::base::nameservers:
- 127.0.0.1
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) # base postfix configuration (passed to postfix class)
postfix::relayhost: 'direct' postfix::relayhost: 'direct'
postfix::myorigin: 'main.unkin.net' postfix::myorigin: 'unkin.net'
postfix::mydestination: 'blank' postfix::mydestination: 'blank'
postfix::mynetworks: '127.0.0.0/8 [::1]/128' postfix::mynetworks: '127.0.0.0/8 [::1]/128 103.216.191.185/32'
postfix::mta: true postfix::mta: true
postfix::manage_aliases: true postfix::manage_aliases: true
# profile parameters for customization
profiles::postfix::gateway::myhostname: 'in-mta.main.unkin.net'
# postfix map content (templates) # postfix map content (templates)
profiles::postfix::gateway::relay_recipients_maps: profiles::postfix::gateway::relay_recipients_maps:
'@main.unkin.net': 'OK' '@unkin.net': 'OK'
profiles::postfix::gateway::relay_domains_maps: profiles::postfix::gateway::relay_domains_maps:
'main.unkin.net': 'OK' 'unkin.net': 'OK'
profiles::postfix::gateway::postscreen_access_maps: profiles::postfix::gateway::postscreen_access_maps:
'127.0.0.1/32': 'permit' '127.0.0.1/32': 'permit'
@@ -32,21 +100,24 @@ profiles::postfix::gateway::helo_access_maps:
'unknown': 'REJECT' 'unknown': 'REJECT'
'localhost': 'REJECT You are not localhost' 'localhost': 'REJECT You are not localhost'
profiles::postfix::gateway::smtp_tls_policy_maps:
'[mail-in.main.unkin.net]:25': 'secure match=hostname'
# postfix transports # postfix transports
postfix::transports: postfix::transports:
'main.unkin.net': 'unkin.net':
ensure: present ensure: present
destination: 'relay' destination: 'relay'
nexthop: 'mail-in.main.unkin.net:25' nexthop: '[mail-in.main.unkin.net]:25'
# postfix virtuals # postfix virtuals
postfix::virtuals: postfix::virtuals:
'root': 'root':
ensure: present ensure: present
destination: 'ben@main.unkin.net' destination: 'ben@unkin.net'
'postmaster': 'postmaster':
ensure: present ensure: present
destination: 'ben@main.unkin.net' destination: 'ben@unkin.net'
'abuse': 'abuse':
ensure: present ensure: present
destination: 'ben@main.unkin.net' destination: 'ben@unkin.net'
+74 -71
View File
@@ -12,91 +12,94 @@ class profiles::consul::client (
Array[Hash] $node_rules = [], Array[Hash] $node_rules = [],
Hash $ports = {}, Hash $ports = {},
Stdlib::IP::Address $host_addr = $facts['networking']['ip'], Stdlib::IP::Address $host_addr = $facts['networking']['ip'],
Boolean $enable = true,
) { ) {
if $facts['enc_role'] != $members_role { if $enable {
if $facts['enc_role'] != $members_role {
# set a datacentre/cluster name # set a datacentre/cluster name
$consul_cluster = "${::facts['country']}-${::facts['region']}" $consul_cluster = "${::facts['country']}-${::facts['region']}"
# if lookup is enabled, find all the hosts in the specified role and create the servers_array # if lookup is enabled, find all the hosts in the specified role and create the servers_array
if $members_lookup { if $members_lookup {
# check that the role is also set # check that the role is also set
unless !($members_role == undef) { unless !($members_role == undef) {
fail("members_role must be provided for ${title} when members_lookup is True") fail("members_role must be provided for ${title} when members_lookup is True")
}
# if it is, find hosts, sort them so they dont cause changes every run
$servers_array = sort(puppetdb_query(
"facts[certname] {
name = 'enc_role' and value = '${members_role}' and
certname in facts[certname] { name = 'region' and value = '${::facts['region']}' }
}"
).map |$fact| { $fact['certname'] })
# else use provided array from params
}else{
$servers_array = $consul_servers
} }
# if it is, find hosts, sort them so they dont cause changes every run # deploy the consul agent
$servers_array = sort(puppetdb_query( class { 'consul':
"facts[certname] { config_hash => {
name = 'enc_role' and value = '${members_role}' and 'data_dir' => $data_dir,
certname in facts[certname] { name = 'region' and value = '${::facts['region']}' } 'datacenter' => $consul_cluster,
}" 'log_level' => 'INFO',
).map |$fact| { $fact['certname'] }) 'node_name' => $facts['networking']['fqdn'],
'retry_join' => $servers_array,
# else use provided array from params 'bind_addr' => $host_addr,
}else{ 'advertise_addr' => $host_addr,
$servers_array = $consul_servers 'enable_script_checks' => true,
} 'ports' => $ports,
'acl' => {
# deploy the consul agent tokens => {
class { 'consul': default => fqdn_uuid("${facts['networking']['fqdn']}-${secret_id_salt}")
config_hash => { }
'data_dir' => $data_dir,
'datacenter' => $consul_cluster,
'log_level' => 'INFO',
'node_name' => $facts['networking']['fqdn'],
'retry_join' => $servers_array,
'bind_addr' => $host_addr,
'advertise_addr' => $host_addr,
'enable_script_checks' => true,
'ports' => $ports,
'acl' => {
tokens => {
default => fqdn_uuid("${facts['networking']['fqdn']}-${secret_id_salt}")
} }
} },
}, }
} }
}
# Create ACL policy that allows nodes to update themselves and read others # Create ACL policy that allows nodes to update themselves and read others
consul_policy { $facts['networking']['hostname']: consul_policy { $facts['networking']['hostname']:
description => "${facts['networking']['fqdn']} puppet-generated-policy", description => "${facts['networking']['fqdn']} puppet-generated-policy",
rules => $node_rules, rules => $node_rules,
acl_api_token => $consul_api_token, acl_api_token => $consul_api_token,
hostname => $consul_hostname, hostname => $consul_hostname,
protocol => $consul_protocol, protocol => $consul_protocol,
port => $consul_port, port => $consul_port,
} }
consul_token { $facts['networking']['hostname']: consul_token { $facts['networking']['hostname']:
accessor_id => fqdn_uuid($facts['networking']['fqdn']), accessor_id => fqdn_uuid($facts['networking']['fqdn']),
description => "${facts['networking']['fqdn']} puppet-generated-token", description => "${facts['networking']['fqdn']} puppet-generated-token",
policies_by_name => [$facts['networking']['hostname']], policies_by_name => [$facts['networking']['hostname']],
acl_api_token => $consul_api_token, acl_api_token => $consul_api_token,
secret_id => fqdn_uuid("${facts['networking']['fqdn']}-${secret_id_salt}"), secret_id => fqdn_uuid("${facts['networking']['fqdn']}-${secret_id_salt}"),
hostname => $consul_hostname, hostname => $consul_hostname,
protocol => $consul_protocol, protocol => $consul_protocol,
port => $consul_port, port => $consul_port,
} }
# ensure the consul token is saved for the root user # ensure the consul token is saved for the root user
file {'/root/.config/consul_node_token': file {'/root/.config/consul_node_token':
ensure => file, ensure => file,
owner => 'root', owner => 'root',
group => 'root', group => 'root',
mode => '0600', mode => '0600',
content => Sensitive(fqdn_uuid("${facts['networking']['fqdn']}-${secret_id_salt}")), content => Sensitive(fqdn_uuid("${facts['networking']['fqdn']}-${secret_id_salt}")),
require => File['/root/.config'], require => File['/root/.config'],
} }
# cleanup /usr/local/bin/consul which was created by url install method # cleanup /usr/local/bin/consul which was created by url install method
if $facts['os']['family'] == 'RedHat' { if $facts['os']['family'] == 'RedHat' {
file {'/usr/local/bin/consul': file {'/usr/local/bin/consul':
ensure => absent, ensure => absent,
}
} }
} }
} }
+30 -24
View File
@@ -3,8 +3,7 @@ class profiles::postfix::gateway (
Stdlib::Absolutepath $tls_key_file = '/etc/pki/tls/vault/certificate.pem', Stdlib::Absolutepath $tls_key_file = '/etc/pki/tls/vault/certificate.pem',
Stdlib::Absolutepath $tls_ca_file = '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem', Stdlib::Absolutepath $tls_ca_file = '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
String $myhostname = $trusted['certname'], String $myhostname = $trusted['certname'],
String $message_size_limit = '133169152', String $message_size_limit = '52428800',
String $mailbox_size_limit = '133169152',
String $local_transport = 'error:No local mail delivery', String $local_transport = 'error:No local mail delivery',
Boolean $enable_postscreen = true, Boolean $enable_postscreen = true,
Array[String] $alias_maps = [ Array[String] $alias_maps = [
@@ -12,12 +11,12 @@ class profiles::postfix::gateway (
'hash:/etc/postfix/aliases', 'hash:/etc/postfix/aliases',
], ],
Array[String] $postscreen_dnsbl_sites = [ Array[String] $postscreen_dnsbl_sites = [
'zen.spamhaus.org*3', 'zen.spamhaus.org=127.0.0.[2..11]*3',
'b.barracudacentral.org=127.0.0.[2..11]*2', 'b.barracudacentral.org=127.0.0.2*2',
'bl.spameatingmonkey.net*2', 'bl.mailspike.net=127.0.0.2*1',
'bl.spamcop.net', 'psbl.surriel.com=127.0.0.2*1',
'dnsbl.sorbs.net', 'bl.spameatingmonkey.net=127.0.0.2*2',
'swl.spamhaus.org*-4', 'bl.spamcop.net=127.0.0.2',
'list.dnswl.org=127.[0..255].[0..255].0*-2', 'list.dnswl.org=127.[0..255].[0..255].0*-2',
'list.dnswl.org=127.[0..255].[0..255].1*-4', 'list.dnswl.org=127.[0..255].[0..255].1*-4',
'list.dnswl.org=127.[0..255].[0..255].[2..3]*-6', 'list.dnswl.org=127.[0..255].[0..255].[2..3]*-6',
@@ -25,7 +24,6 @@ class profiles::postfix::gateway (
Array[String] $smtpd_client_restrictions = [ Array[String] $smtpd_client_restrictions = [
'permit_sasl_authenticated', 'permit_sasl_authenticated',
'permit_mynetworks', 'permit_mynetworks',
'reject_rbl_client zen.spamhaus.org',
], ],
Array[String] $smtpd_sender_restrictions = [ Array[String] $smtpd_sender_restrictions = [
'permit_sasl_authenticated', 'permit_sasl_authenticated',
@@ -47,6 +45,7 @@ class profiles::postfix::gateway (
'permit_mynetworks', 'permit_mynetworks',
'reject_unauth_destination', 'reject_unauth_destination',
], ],
String $smtpd_milters = 'inet:127.0.0.1:11332',
Hash[String, String] $smtp_tls_policy_maps = {}, Hash[String, String] $smtp_tls_policy_maps = {},
Hash[String, String] $sender_canonical_maps = {}, Hash[String, String] $sender_canonical_maps = {},
Hash[String, String] $sender_access_maps = {}, Hash[String, String] $sender_access_maps = {},
@@ -73,7 +72,7 @@ class profiles::postfix::gateway (
'value' => 'permit_mynetworks, cidr:/etc/postfix/postscreen_access' 'value' => 'permit_mynetworks, cidr:/etc/postfix/postscreen_access'
}, },
'postscreen_blacklist_action' => { 'postscreen_blacklist_action' => {
'value' => 'enforce' 'value' => 'drop'
}, },
'postscreen_cache_map' => { 'postscreen_cache_map' => {
'value' => 'btree:$data_directory/postscreen_cache' 'value' => 'btree:$data_directory/postscreen_cache'
@@ -87,6 +86,10 @@ class profiles::postfix::gateway (
'postscreen_dnsbl_threshold' => { 'postscreen_dnsbl_threshold' => {
'value' => '2' 'value' => '2'
}, },
# el9 ships postfix 3.5, before the allowlist rename
'postscreen_dnsbl_whitelist_threshold' => {
'value' => '-1'
},
'postscreen_greet_action' => { 'postscreen_greet_action' => {
'value' => 'enforce' 'value' => 'enforce'
}, },
@@ -108,9 +111,6 @@ class profiles::postfix::gateway (
'alias_database' => { 'alias_database' => {
'value' => $alias_maps_string 'value' => $alias_maps_string
}, },
'default_destination_recipient_limit' => {
'value' => '1'
},
'disable_vrfy_command' => { 'disable_vrfy_command' => {
'value' => 'yes' 'value' => 'yes'
}, },
@@ -129,9 +129,6 @@ class profiles::postfix::gateway (
'local_transport' => { 'local_transport' => {
'value' => $local_transport 'value' => $local_transport
}, },
'mailbox_size_limit' => {
'value' => $mailbox_size_limit
},
'message_size_limit' => { 'message_size_limit' => {
'value' => $message_size_limit 'value' => $message_size_limit
}, },
@@ -141,8 +138,20 @@ class profiles::postfix::gateway (
'non_smtpd_milters' => { 'non_smtpd_milters' => {
'ensure' => 'blank' 'ensure' => 'blank'
}, },
'qmqpd_authorized_clients' => { 'milter_connect_timeout' => {
'value' => '127.0.0.1 [::1]' 'value' => '10s'
},
'milter_content_timeout' => {
'value' => '60s'
},
'milter_default_action' => {
'value' => 'tempfail'
},
'milter_mail_macros' => {
'value' => 'i {mail_addr} {client_addr} {client_name} {auth_authen}'
},
'milter_protocol' => {
'value' => '6'
}, },
'recipient_canonical_maps' => { 'recipient_canonical_maps' => {
'value' => 'hash:/etc/postfix/recipient_canonical' 'value' => 'hash:/etc/postfix/recipient_canonical'
@@ -177,7 +186,7 @@ class profiles::postfix::gateway (
'smtp_tls_session_cache_database' => { 'smtp_tls_session_cache_database' => {
'value' => 'btree:/var/lib/postfix/smtp_tls_session_cache' 'value' => 'btree:/var/lib/postfix/smtp_tls_session_cache'
}, },
'smtp_use_tls' => { 'smtp_send_xforward_command' => {
'value' => 'yes' 'value' => 'yes'
}, },
'smtpd_banner' => { 'smtpd_banner' => {
@@ -211,7 +220,7 @@ class profiles::postfix::gateway (
'value' => 'check_helo_access hash:/etc/postfix/helo_access, reject_invalid_hostname' 'value' => 'check_helo_access hash:/etc/postfix/helo_access, reject_invalid_hostname'
}, },
'smtpd_milters' => { 'smtpd_milters' => {
'value' => 'inet:127.0.0.1:33333' 'value' => $smtpd_milters
}, },
'smtpd_recipient_restrictions' => { 'smtpd_recipient_restrictions' => {
'value' => join($smtpd_recipient_restrictions, ', ') 'value' => join($smtpd_recipient_restrictions, ', ')
@@ -255,13 +264,10 @@ class profiles::postfix::gateway (
'smtpd_tls_session_cache_timeout' => { 'smtpd_tls_session_cache_timeout' => {
'value' => '3600s' 'value' => '3600s'
}, },
'smtpd_use_tls' => {
'value' => 'yes'
},
'tls_medium_cipherlist' => { 'tls_medium_cipherlist' => {
'value' => join([ 'value' => join([
'ECDSA+AESGCM:ECDH+AESGCM:DH+AESGCM:ECDSA+AES:ECDH+AES:DH+AES', 'ECDSA+AESGCM:ECDH+AESGCM:DH+AESGCM:ECDSA+AES:ECDH+AES:DH+AES',
'ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS' 'RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS:!3DES'
], ':') ], ':')
}, },
'tls_preempt_cipherlist' => { 'tls_preempt_cipherlist' => {
+51
View File
@@ -0,0 +1,51 @@
# 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
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| {
file { "/etc/rspamd/rules/${name}":
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
source => $url,
notify => Service['rspamd'],
}
}
# reload keeps the milter answering while config changes apply
service { 'rspamd':
ensure => running,
enable => true,
restart => '/usr/bin/systemctl reload rspamd',
require => Package['rspamd'],
}
}
@@ -6,6 +6,11 @@ class roles::infra::mail::gateway {
}else{ }else{
include profiles::defaults include profiles::defaults
include profiles::base include profiles::base
include dnsmasq
include profiles::rspamd::edge
include profiles::postfix::gateway include profiles::postfix::gateway
# resolv.conf points at dnsmasq, so it must be running first
Class['dnsmasq'] -> Class['profiles::dns::resolvconf']
} }
} }