If you run a mail server under a dynamic ip, or an ip that should not be disclosed and you're proxiying with cloudflare, let's configure a 1€/month IONOS vps as mail relay server.
You will need a vanity MX subdomain mail.yourdomain.com  with SSL certificate. Each domain should use it as MX server, fix the SPF record and be advertised on SRV records if used.
The vps's IP should resolve to this subdomain. Ensure with the vps provider that the IP rDNS record points to your vanity subdomain, and in case of IONOS ask support to enable port 25, and allow on the external firewall.
With both servers, HESTIA and VPS the best option is to use wireward to interconnect them, this is highly encouraged and i will use the interface ips on this tutorial.
Generate a key and create the file /etc/wireguard/wg0.conf on both servers
# HESTIA (client)
[Interface]
PrivateKey = __A_PRIVATE_KEY__
Address = 10.0.0.2/24
[Peer]
PublicKey = __B_PUBLIC_KEY__
Endpoint = __VPS_FIXED_IP__:18632
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25# VPS (server)
[Interface]
PrivateKey = __B_PRIVATE_KEY__
Address = 10.0.0.1/24
ListenPort = 18632
[Peer]
PublicKey = __A_PUBLIC_KEY__
AllowedIPs = 10.0.0.2/32Use wg-quick to enable and start the VPN (ensure your VPS allow connections on ListenPort/udp).
HESTIA ip: 10.0.0.2   # if not using wireward, assume public external ip
VPS ip:    10.0.0.1   # if not using wireward, assume public external ipNow both servers should be able to ping between them. You can restrict smtp in hestia firewall from 0.0.0.0/0 -> 10.0.0.1 so hestia's exim isnt reachable from internet, but the VPS.
It's tempting to try to just use socat or ssh lateral port forwarding to receive inbound mail, but doing that, exim will see all traffic coming from 127.0.0.1 instead of the original IP, making all email local and trusted, converting your VPS into an open relay (useful for spammers and amplification attacks). and it dont fix the issue for outbound email, wich also shuld be relayed by the VPS.
The best option is to use an MTA like postfix, that will work like this:
- Inbound email for local domains (eg. recive email from gmail): Perform rDNS, RBL, SPF, DKIM, DMARC checks on IP/Sender, then relay to exim
- Inbound email for remote domains (eg. outlook/mail app) and outbound email (eg. send to gmail): require to be a trusted ip (127.0.0.1, 10.0.0.2) or AUTH*, ensure DKIM signature and relay to the remote host.
We need a way to authorize connections on postfix. exim uses dovecot SASL authenticator, that can be exposed to the VPS via network port, edit /etc/dovecot/conf.d/10-master.conf:
service auth {
  inet_listener auth {
    port = 12345
  }
  ...Add to hestia firewall a tcp allow for port 12345 to the VPS (10.0.0.1) check with telnet 10.0.0.2 12345 on the VPS that SASL authenticator is available.
Modern SMTP servers use TLS forward secrecy, wich just means that AUTH is only available while using a TLS encrypted connection, either by using SSL (465)/STARTTLS (587) port or negotating STARTTLS on 25. As the user and password travel in plain base64 MITM attacks could exfiltrate those credentials easily without this limitation.
Start by installing the required packages:
apt install postfix postfix-policyd-spf-python opendmarc opendkim fail2ban
Transfer your ssl certificate of your vanity domain (hostname) to
/usr/local/hestia/ssl/certificate.crt -> /etc/ssl/certs/fullchain.pem
/usr/local/hestia/ssl/certificate.key ->/etc/ssl/private/privkey.pem
Write to /etc/postfix/main.cf
# Postfix SMTP 
myhostname 						= mail.yourdomain.com
myorigin 						= mail.yourdomain.com
mydestination 					= 
mynetworks 						= 127.0.0.0/8 10.0.0.0/24
smtpd_tls_cert_file 			= /etc/ssl/certs/fullchain.pem
smtpd_tls_key_file 				= /etc/ssl/private/privkey.pem
smtpd_tls_security_level 		= may
smtpd_tls_auth_only 			= yes
smtpd_tls_mandatory_protocols 	= !SSLv2, !SSLv3
smtpd_tls_mandatory_ciphers 	= high
smtpd_tls_dh1024_param_file 	= /etc/ssl/certs/dhparam.pem
smtp_tls_security_level 		= may
smtp_tls_note_starttls_offer 	= yes
smtp_tls_CAfile 				= /etc/ssl/certs/ca-certificates.crt
relay_domains 					= hash:/etc/postfix/relay_domains
relay_recipient_maps 			= 
transport_maps 					= hash:/etc/postfix/transport
relayhost 						= 
smtpd_sasl_auth_enable 			= yes
smtpd_sasl_type 				= dovecot
smtpd_sasl_path 				= inet:10.0.0.2:12345
smtpd_sasl_security_options 	= noanonymous
smtpd_sasl_tls_security_options = noanonymous
smtpd_helo_required 			= yes
disable_vrfy_command 			= yes
smtpd_forbid_unauth_pipelining  = yes
smtpd_delay_reject 				= yes
smtpd_helo_restrictions 		= permit_mynetworks, reject_invalid_helo_hostname, permit
smtpd_client_restrictions 		= permit_mynetworks, permit_sasl_authenticated, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net, reject_rbl_client dnsbl.sorbs.net, check_policy_service unix:private/spf, permit
smtpd_relay_restrictions 		= permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
smtpd_recipient_restrictions 	= permit_mynetworks, permit_sasl_authenticated, reject_unknown_reverse_client_hostname, check_recipient_access hash:/etc/postfix/relay_domains, reject_non_fqdn_sender, reject_non_fqdn_recipient, reject_unknown_sender_domain, reject_unknown_recipient_domain, reject_unauth_pipelining, reject_invalid_hostname, reject_non_fqdn_hostname, reject_unverified_recipient, reject_unauth_destination
smtpd_data_restrictions 		= permit_mynetworks, permit_sasl_authenticated, reject_unauth_pipelining
smtpd_etrn_restrictions 		= permit_mynetworks, reject
milter_default_action 			= accept
milter_protocol 				= 6
smtpd_milters 					= inet:localhost:8891, inet:localhost:8893
non_smtpd_milters 				= $smtpd_milters
milter_macro_daemon_name 		= ORIGINATING
alias_maps 						= hash:/etc/aliases
alias_database 					= $alias_maps
smtp_connection_cache_on_demand	= no
compatibility_level 			= 3.8
smtpd_relay_before_recipient_restrictions = yes(you might need to generate a dhparam.pem or copy from hestia server)
Write to /etc/postfix/master.cf
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (no)    (never) (100)
# ==========================================================================
smtp      inet  n       -       y       -       -       smtpd
smtps     inet  n       -       y       -       -       smtpd 
  -o smtpd_tls_wrappermode=yes 
  -o smtpd_sasl_auth_enable=yes
submission inet n       -       y       -       -       smtpd
  -o smtpd_tls_security_level=encrypt 
  -o smtpd_sasl_auth_enable=yes
#smtp     inet  n       -       y       -       1       postscreen
#smtpd    pass  -       -       y       -       -       smtpd
#dnsblog  unix  -       -       y       -       0       dnsblog
#tlsproxy unix  -       -       y       -       0       tlsproxy
#628      inet  n       -       y       -       -       qmqpd
pickup    unix  n       -       y       60      1       pickup
cleanup   unix  n       -       y       -       0       cleanup
qmgr      unix  n       -       n       300     1       qmgr
#qmgr     unix  n       -       n       300     1       oqmgr
tlsmgr    unix  -       -       y       1000?   1       tlsmgr
rewrite   unix  -       -       y       -       -       trivial-rewrite
bounce    unix  -       -       y       -       0       bounce
defer     unix  -       -       y       -       0       bounce
trace     unix  -       -       y       -       0       bounce
verify    unix  -       -       y       -       1       verify
flush     unix  n       -       y       1000?   0       flush
proxymap  unix  -       -       n       -       -       proxymap
proxywrite unix -       -       n       -       1       proxymap
smtp      unix  -       -       y       -       -       smtp
relay     unix  -       -       y       -       -       smtp
  -o syslog_name=postfix/$service_name 
  -o smtp_helo_timeout=5 
  -o smtp_connect_timeout=5
showq     unix  n       -       y       -       -       showq
error     unix  -       -       y       -       -       error
retry     unix  -       -       y       -       -       error
discard   unix  -       -       y       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       y       -       -       lmtp
anvil     unix  -       -       y       -       1       anvil
scache    unix  -       -       y       -       1       scache
postlog   unix-dgram n  -       n       -       1       postlogd
uucp      unix  -       n       n       -       -       pipe
  flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
spf       unix  -       n       n       -       0       spawn
  user=nobody argv=/usr/bin/python3 /usr/bin/policyd-spf /etc/postfix-policyd-spf-python/policyd-spf.confEdit or create this configuration files:
# /etc/postfix-policyd-spf-python/policyd-spf.conf
debugLevel = 1
TestOnly = 0
HELO_reject = Fail
Mail_From_reject = Fail
PermError_reject = True
TempError_Defer = True
skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1,10.0.0.0/24
# /etc/opendkim.conf
AuthservID              mail.yourdomain.com
Socket                  inet:8891@localhost
Mode                    sv
ExternalIgnoreList      refile:/etc/opendkim/TrustedHosts
InternalHosts           refile:/etc/opendkim/TrustedHosts
KeyTable                refile:/etc/opendkim/KeyTable
SigningTable            refile:/etc/opendkim/SigningTable
Canonicalization        relaxed/relaxed
OversignHeaders References,In-Reply-To,Sender,Reply-To,Cc,Content-ID,Content-Description,Resent-Date,Resent-From,Resent-Sender,Resent-To,Resent-Cc,Resent-Message-ID,List-Id,List-Help,List-Unsubscribe,List-Subscribe,List-Post,List-Owner,List-Archive
# /etc/opendkim/TrustedHosts
127.0.0.1
localhost
10.0.0.1
10.0.0.2
# /etc/opendkim/KeyTable
mail._domainkey.yourdomain.com yourdomain.com:mail:/etc/opendkim/keys/yourdomain.com.pem
# /etc/opendkim/SigningTable
*@yourdomain.com mail._domainkey.yourdomain.com
# /etc/opendmarc.conf
AuthservID                 mail.yourdomain.com
TrustedAuthservIDs         mail.yourdomain.com
Socket                     inet:8893@localhost
RejectFailures             true
RequiredHeaders            true
SPFIgnoreResults           true
SPFSelfValidate            true
IgnoreAuthenticatedClients true
IgnoreMailFrom yourdomain.com,mail.yourdomain.comTransfer the DKIM certificate from /home/user/conf/mail/yourdomain.com/dkim.pem to /etc/opendkim/keys/yourdomain.com.pem automatic transfer will be covered later.
Postfix needs to know what domains should be relayed to exim, for now lets build a static list (later in this tutorial this will be addressed***)
List the domains on /etc/postfix/transport like in the example
yourdomain.com smtp:[10.0.0.2]:587
otherdomain.com smtp:[10.0.0.2]:587also /etc/postfix/relay_domains
yourdomain.com OK
otherdomain.com OKThis routes verified emails to exim. hash the files with postmap /etc/postfix/relay_domains and postmap /etc/postfix/transport and then restart everything service opendkim restart; service opendmarc restart; service postfix restart
The VPS is ready, now its needed to configure exim to properly handle mail.
Edit /etc/exim4/exim4.conf.template  first we want to force an internal hostname that is not the same as the vanity subdomain, and add the VPS ip as relay allowed (local mail) and add VPS ip as "local" host (authorized by default)
Because of how the setup works, its easier to delegate DKIM signing to postfix, so emails sent by outside clients are signed and there is no need to complex setup routing mails to exim and back to postfix to be relayed
smtp_active_hostname = server1.yourdomain.com
smtp_banner = $smtp_active_hostname
...
hostlist relay_from_hosts = 127.0.0.1 10.0.0.1 10.0.0.2
...
DKIM_FILE =After this change, inbound email should work, but lets also use the VPS as exim's "smarthost" and using it as relay. Hestia provides functionality creating this file /etc/exim4/smtp_relay.conf
host:10.0.0.1
port:25
user:
pass:This configuration ensures outbound messages are relayed to destination via postfix on the VPS, removing the internal IP component (using wireward, or removing the extra header)
After a small time you will notice a lot of inbound connections try brute-forcing accounts on the server, log will show something like this on /var/log/mail.log:
postfix/smtpd[257723]: connect from unknown[81.30.107.5]
postfix/smtpd[257723]: disconnect from unknown[81.30.107.5] ehlo=1 auth=0/1 rset=0/1 quit=1 commands=2/4
postfix/smtpd[257470]: connect from unknown[81.30.107.6]
postfix/smtpd[257470]: disconnect from unknown[81.30.107.6] ehlo=1 auth=0/1 rset=0/1 quit=1 commands=2/4
postfix/smtpd[257723]: connect from unknown[81.30.107.5]
postfix/smtpd[257723]: disconnect from unknown[81.30.107.5] ehlo=1 auth=0/1 rset=0/1 quit=1 commands=2/4
postfix/smtpd[257470]: connect from unknown[81.30.107.6]
postfix/smtpd[257470]: disconnect from unknown[81.30.107.6] ehlo=1 auth=0/1 rset=0/1 quit=1 commands=2/4
Since dovecot does not run on the VPS the logs are not available, so lets just make a simple fail2ban rule to block out spammers using the mail.log disconnections from IPs with auth=0/N, where N = failed attempts. Also crawlers and random non smtp traffic is tracked.
/etc/fail2ban/jail.d/postfix-authfail.local
[postfix-authfail]
enabled = true
port = smtp,ssmtp,submission
filter = postfix-authfail
logpath = /var/log/mail.log
maxretry = 3
findtime = 600
bantime = 3600
bantime.increment = true
action = iptables-multiport[name=PostfixAuth, port="smtp,ssmtp,submission", protocol=tcp]
ignoreip = 127.0.0.1/8 10.0.0.2 10.0.0.1/etc/fail2ban/filter.d/postfix-authfail.conf
[Definition]
failregex = ^.*postfix/smtpd\[\d+\]: disconnect from \S+\[<HOST>\] .*auth=0/(?P<attempts>\d+).*
            ^.*postfix/smtpd\[\d+\]: disconnect from \S+\[<HOST>\] .*commands=0/\d+.*
            ^.*postfix/smtpd\[\d+\]: disconnect from \S+\[<HOST>\] .*unknown=0/\d+.*
            ^.*postfix/smtpd\[\d+\]: SSL_accept error from \S+\[<HOST>\].*
ignoreregex =Then apply and check
# service fail2ban restart
# fail2ban-client status postfix-authfail
Status for the jail: postfix-authfail
|- Filter
|  |- Currently failed: 0
|  `- Total failed:     6
`- Actions
   |- Currently banned: 2
   |- Total banned:     4
   `- Banned IP list:   81.30.107.5 81.30.107.6The server works but requires us to update the domain (ls /etc/exim4/domains) list and SSL certificate of the vanity subdomain after renewal, wich we have on hestia.
Lets crete a shell script that updates the VPS and we can call it on a cronjob or a hook:
#!/bin/bash
### CONFIGURATION ###
VPS_USER="root"
VPS_HOST="10.0.0.1"
VPS_PASS="your_ssh_password_here"
CRT_SRC="/usr/local/hestia/ssl/certificate.crt"
KEY_SRC="/usr/local/hestia/ssl/certificate.key"
CRT_DEST="/etc/ssl/certs/fullchain.pem"
KEY_DEST="/etc/ssl/private/privkey.pem"
RELAY_DOMAINS_FILE="/tmp/relay_domains"
TRANSPORT_FILE="/tmp/transport"
SIGNING_FILE="/tmp/SigningTable"
KEY_FILE="/tmp/KeyTable"
### STEP 1: Generate relay_domains, transport, SigningTable, and KeyTable files ###
echo "Generating config files from /etc/exim4/domains..."
> "$RELAY_DOMAINS_FILE"
> "$TRANSPORT_FILE"
> "$SIGNING_FILE"
> "$KEY_FILE"
domains_comma=$(ls /etc/exim4/domains | tr '\n' ',' | sed 's/,$//')
for domain in $(ls /etc/exim4/domains); do
    echo "$domain OK" >> "$RELAY_DOMAINS_FILE"
    echo "$domain smtp:[10.0.0.2]:587" >> "$TRANSPORT_FILE"
    
    if [ -f "/etc/exim4/domains/$domain/dkim.pem" ]; then
        echo "*@$domain mail._domainkey.$domain" >> "$SIGNING_FILE"
        echo "mail._domainkey.$domain $domain:mail:/etc/opendkim/keys/$domain.pem" >> "$KEY_FILE"
    fi
done
### STEP 2: Push certs and files to VPS ###
echo "Transferring certificate, key, domain configs, and DKIM files to VPS..."
sshpass -p "$VPS_PASS" scp "$CRT_SRC" "$VPS_USER@$VPS_HOST:$CRT_DEST"
sshpass -p "$VPS_PASS" scp "$KEY_SRC" "$VPS_USER@$VPS_HOST:$KEY_DEST"
sshpass -p "$VPS_PASS" scp "$RELAY_DOMAINS_FILE" "$VPS_USER@$VPS_HOST:/etc/postfix/relay_domains"
sshpass -p "$VPS_PASS" scp "$TRANSPORT_FILE" "$VPS_USER@$VPS_HOST:/etc/postfix/transport"
sshpass -p "$VPS_PASS" scp "$SIGNING_FILE" "$VPS_USER@$VPS_HOST:/etc/opendkim/SigningTable"
sshpass -p "$VPS_PASS" scp "$KEY_FILE" "$VPS_USER@$VPS_HOST:/etc/opendkim/KeyTable"
# Transfer per-domain DKIM PEM files and set permissions
for domain in $(ls /etc/exim4/domains); do
    if [ -f "/etc/exim4/domains/$domain/dkim.pem" ]; then
        sshpass -p "$VPS_PASS" scp "/etc/exim4/domains/$domain/dkim.pem" "$VPS_USER@$VPS_HOST:/etc/opendkim/keys/$domain.pem"
        sshpass -p "$VPS_PASS" ssh "$VPS_USER@$VPS_HOST" "chown opendkim:opendkim /etc/opendkim/keys/$domain.pem && chmod 0600 /etc/opendkim/keys/$domain.pem"
    fi
done
### STEP 3: Update configs and reload services on VPS ###
echo "Updating configs and reloading services on VPS..."
sshpass -p "$VPS_PASS" ssh "$VPS_USER@$VPS_HOST" bash <<EOF
postmap /etc/postfix/transport
postmap /etc/postfix/relay_domains
chmod 600 $CRT_DEST $KEY_DEST
sed -i "s/^IgnoreMailFrom .*/IgnoreMailFrom $domains_comma/" /etc/opendmarc.conf
service opendkim reload
service opendmarc reload
postfix reload
EOF
### CLEANUP ###
rm "$RELAY_DOMAINS_FILE" "$TRANSPORT_FILE" "$SIGNING_FILE" "$KEY_FILE"
echo "✅ All done. Certificates, domain configs, and DKIM settings updated on VPS."Now the configuration can be synchronized quickly calling this script.
This means a SPF-alike verification will be done over HTTPS. To enable MTA-STS, you will need to add this records per mail domain:
_mta-sts.<domain>.com. 3600 IN TXT "v=STSv1; id=2025080000"
<domain>.com. 3600 IN MX 10 mail.yourdomain.com.
mta-sts.<domain>.com. 3600 IN CNAME mail.yourdomain.com.
Then each domain should have a nginx configuration /home/admin/conf/web/mta-sts.<domain>.com/nginx.ssl.conf_mtasts and make sure the subdomain has a valid SSL certificate
location = /.well-known/mta-sts.txt {
  default_type text/plain;
  return 200 "version: STSv1\nmode: enforce\nmx: mail.yourdomain.com\nmax_age: 604800\n";
}DANE and other would require DNSSEC