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
[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
[Interface]
PrivateKey = B_PRIVATE_KEY
Address = 10.0.0.1/24
ListenPort = 18632
[Peer]
PublicKey = A_PUBLIC_KEY
AllowedIPs = 10.0.0.2/32
Use 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 ip
Now 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*, then relay to the remote.
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 /etc/ssl/certs/fullchain.pem - /etc/ssl/private/privkey.pem
Write to /etc/postfix/main.cf
# Host Identity
myhostname = mail.yourdomain.com
myorigin = mail.yourdomain.com
mydestination =
mynetworks = 127.0.0.1 10.0.0.1 10.0.0.2
# TLS
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
# PSF
smtp_tls_security_level = may
#smtp_tls_security_level = encrypt
smtp_tls_note_starttls_offer = yes
smtp_tls_loglevel = 1
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
# Relay and Routing
relay_domains = hash:/etc/postfix/relay_domains
relay_recipient_maps =
transport_maps = hash:/etc/postfix/transport
relayhost =
# Authenticated SMTP
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
# HELO verification
smtpd_helo_required = yes
disable_vrfy_command = yes
strict_rfc821_envelopes = yes
smtpd_delay_reject = yes
smtpd_helo_restrictions =
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
permit
# Message filtering
smtpd_client_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
check_client_access cidr:/etc/postfix/rbl_exempt.cidr,
check_policy_service unix:private/policyd-spf,
reject_rbl_client zen.spamhaus.org,
reject_rbl_client bl.spamcop.net,
permit
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
check_recipient_access hash:/etc/postfix/relay_domains,
reject_unauth_destination
# DKIM/DMARC/SPF milters
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8891, inet:localhost:8893
non_smtpd_milters = inet:localhost:8891, inet:localhost:8893
milter_macro_daemon_name = $myhostname
# NIS warn skip
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
# misc
smtpd_relay_before_recipient_restrictions = yes
compatibility_level = 3.6
(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
#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)
submission inet n - y - - smtpd
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=inet:10.0.0.2:12345
-o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
smtps inet n - y - - smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=inet:10.0.0.2:12345
-o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
policyd-spf unix - n n - 0 spawn
user=nobody argv=/usr/bin/python3 /usr/bin/policyd-spf /etc/postfix-policyd-spf-python/policyd-spf.conf
Edit /etc/opendkim.conf
to use a network port and only verify:
Mode v
...
#Socket local:/run/opendkim/opendkim.sock
Socket inet:8891@localhost
Do the same for /etc/opendmarc.conf
#Socket local:/run/opendmarc/opendmarc.sock
Socket inet:8893@localhost
TrustedAuthservIDs mail.yourdomain.com
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]:25
otherdomain.com smtp:[10.0.0.2]:25
also /etc/postfix/relay_domains
yourdomain.com OK
otherdomain.com OK
This 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)
smtp_banner = server1.yourdomain.com
smtp_active_hostname = server1.yourdomain.com
...
hostlist relay_from_hosts = 127.0.0.1 10.0.0.1 10.0.0.2
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 properly DKIM-signed and relayed to destination via postfix on the VPS, removing the internal IP component (using wireward, or removing the extra Recived 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.
/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
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+).*
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.6
The 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"
### STEP 1: Generate relay_domains and transport files ###
echo "Generating relay_domains and transport from /etc/exim4/domains..."
> "$RELAY_DOMAINS_FILE"
> "$TRANSPORT_FILE"
for domain in $(ls /etc/exim4/domains); do
echo "$domain OK" >> "$RELAY_DOMAINS_FILE"
echo "$domain smtp:[10.0.0.2]:25" >> "$TRANSPORT_FILE"
done
### STEP 2: Push certs and files to VPS ###
echo "Transferring certificate, key, and domain config 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"
### STEP 3: Rebuild DB and reload Postfix ###
echo "Reloading Postfix 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
postfix reload
EOF
### CLEANUP ###
rm "$RELAY_DOMAINS_FILE" "$TRANSPORT_FILE"
echo "✅ All done. Certificates and domain configs 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