Last active
August 16, 2025 21:25
-
-
Save pabloko/073afecfc64886b44851145a8eb16e29 to your computer and use it in GitHub Desktop.
Revisions
-
pabloko revised this gist
Aug 16, 2025 . 1 changed file with 5 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -111,7 +111,7 @@ 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 @@ -190,6 +190,8 @@ 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 @@ -222,8 +224,8 @@ Postfix needs to know what domains should be relayed to exim, for now lets build List the domains on `/etc/postfix/transport` like in the example ```ini yourdomain.com smtp:[10.0.0.2]:587 otherdomain.com smtp:[10.0.0.2]:587 ``` also `/etc/postfix/relay_domains` ```ini -
pabloko revised this gist
Aug 15, 2025 . 1 changed file with 11 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -102,7 +102,7 @@ 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 @@ -173,6 +173,15 @@ spf unix - n n - 0 spawn ``` Edit or create this configuration files: ```ini # /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 @@ -333,7 +342,7 @@ 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" -
pabloko revised this gist
Aug 14, 2025 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -74,8 +74,8 @@ Transfer your ssl certificate of your vanity domain (hostname) to Write to `/etc/postfix/main.cf` ```ini # 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 -
pabloko revised this gist
Aug 14, 2025 . 1 changed file with 141 additions and 115 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -10,25 +10,26 @@ The vps's IP should resolve to this subdomain. Ensure with the vps provider that 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](https://wg.orz.tools/) and create the file `/etc/wireguard/wg0.conf` on both servers ```ini # 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 ``` ``` ini # 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/32 ``` Use wg-quick to enable and start the VPN (ensure your VPS allow connections on ListenPort/udp). @@ -45,7 +46,7 @@ It's tempting to try to just use socat or ssh lateral port forwarding to receive 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. ### Auth (Dovecot SASL) 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`: @@ -64,75 +65,58 @@ Modern SMTP servers use TLS forward secrecy, wich just means that AUTH is only a 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` ```ini # Postfix SMTP myhostname = mail.almeriahost.com myorigin = mail.almeriahost.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, check_policy_service unix:private/spf, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net, reject_rbl_client dnsbl.sorbs.net, 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 = $myhostname 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) @@ -143,6 +127,12 @@ Write to `/etc/postfix/master.cf` # (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 @@ -163,8 +153,9 @@ 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 @@ -177,39 +168,44 @@ 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.conf ``` Edit or create this configuration files: ```ini # /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 # /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.com ``` Transfer 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. ### Local domains @@ -227,16 +223,20 @@ 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` ### Exim inbound and outbound 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 ```ini 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` ```ini @@ -245,7 +245,7 @@ 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) ### Brute force @@ -260,7 +260,7 @@ postfix/smtpd[257723]: disconnect from unknown[81.30.107.5] ehlo=1 auth=0/1 rset 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` ```ini [postfix-authfail] @@ -271,18 +271,20 @@ 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` ```ini [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 ```txt # service fail2ban restart # fail2ban-client status postfix-authfail @@ -316,40 +318,64 @@ 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]:25" >> "$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. -
pabloko revised this gist
Aug 11, 2025 . 1 changed file with 7 additions and 93 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -39,6 +39,7 @@ 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. ### Postfix MTA 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: @@ -237,34 +238,12 @@ 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` ```ini 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) @@ -389,69 +368,4 @@ location = /.well-known/mta-sts.txt { return 200 "version: STSv1\nmode: enforce\nmx: mail.yourdomain.com\nmax_age: 604800\n"; } ``` DANE and other would require DNSSEC -
pabloko revised this gist
Aug 7, 2025 . 1 changed file with 34 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -142,9 +142,15 @@ Write to `/etc/postfix/master.cf` # (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 @@ -156,8 +162,8 @@ 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 @@ -170,13 +176,23 @@ 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 ``` @@ -358,6 +374,22 @@ echo "✅ All done. Certificates and domain configs updated on VPS." ``` Now the configuration can be synchronized quickly calling this script. ### MTA-STS 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: ```dns _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 ```nginx location = /.well-known/mta-sts.txt { default_type text/plain; return 200 "version: STSv1\nmode: enforce\nmx: mail.yourdomain.com\nmax_age: 604800\n"; } ``` ### Results (read the headers) This is a low maintenance and cheap option to have an external SMTP box. -
pabloko created this gist
Aug 6, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,425 @@ # Hestia on dynamic/undisclosed IP: mail stack via VPS 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. ### Server internal connection (optional) 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](https://wg.orz.tools/) and create the file `/etc/wireguard/wg0.conf` on both servers HESTIA ```ini [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 ``` ini [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). ```bash 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. ### Postfix MTA 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. ### Auth (Dovecot SASL) 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`: ```ini 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. ### VPS Configuration 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` ```ini # 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` ```ini # ========================================================================== # service type private unpriv chroot wakeup maxproc command + args # (yes) (yes) (no) (never) (100) # ========================================================================== smtp inet n - y - - smtpd pickup unix n - y 60 1 pickup cleanup unix n - y - 0 cleanup qmgr unix n - n 300 1 qmgr 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 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: ```ini Mode v ... #Socket local:/run/opendkim/opendkim.sock Socket inet:8891@localhost ``` Do the same for `/etc/opendmarc.conf` ```ini #Socket local:/run/opendmarc/opendmarc.sock Socket inet:8893@localhost TrustedAuthservIDs mail.yourdomain.com ``` ### Local domains 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 ```ini yourdomain.com smtp:[10.0.0.2]:25 otherdomain.com smtp:[10.0.0.2]:25 ``` also `/etc/postfix/relay_domains` ```ini 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` ### Exim inbound and outbound 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) ```ini 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 to do this under configuration, but ill leave here the config i've done: ```ini # routes section: ... begin routers send_via_vps: driver = manualroute address_data = 10.0.0.1:587 domains = !+local_domains transport = smarthost_smtp route_list = * ${extract{1}{:}{$address_data}}::${extract{2}{:}{$address_data}} no_more no_verify ... # transports section ... begin transports smarthost_smtp: driver = smtp helo_data = ${lookup dnsdb{>: defer_never,ptr=$sending_ip_address}{${listextract{1}{$value}}}{$primary_hostname}} hosts_require_auth = hosts_require_tls = * dkim_domain = DKIM_DOMAIN dkim_selector = mail dkim_private_key = DKIM_PRIVATE_KEY dkim_canon = relaxed dkim_strict = 0 ... ``` 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) ### Brute force 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`: ```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` ```ini [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` ```ini [Definition] failregex = ^.*postfix/smtpd\[\d+\]: disconnect from \S+\[<HOST>\] .*auth=0/(?P<attempts>\d+).* ignoreregex = ``` Then apply and check ```txt # 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 ``` ### Automatic maintenance 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: ```bash #!/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. ### Results (read the headers) This is a low maintenance and cheap option to have an external SMTP box. Lets debug this by sending a mail from gmail to our box. I've extracted the important headers to watch: ```txt Return-path: <[email protected]> Envelope-to: [email protected] Delivery-date: Wed, 06 Aug 2025 21:14:29 +0200 Received: from [10.0.0.1] (helo=mail.yourdomain.com) by server1.yourdomain.com with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.95) (envelope-from <[email protected]>) id 1ujjan-0081iB-Eh for [email protected]; Wed, 06 Aug 2025 21:14:29 +0200 Authentication-Results: mail.yourdomain.com; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: mail.yourdomain.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com [email protected] header.a=rsa-sha256 header.s=20230601 header.b=cfZj1r; dkim-atps=neutral Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=209.85.128.45; helo=mail-wm1-f45.google.com; [email protected]; receiver=yourdomain.com Received: from mail-wm1-f45.google.com (mail-wm1-f45.google.com [209.85.128.45]) by mail.yourdomain.com (Postfix) with ESMTPS id 852B8F for <[email protected]>; Wed, 6 Aug 2025 19:14:28 +0000 (UTC) Received: by mail-wm1-f45.google.com with SMTP id 5b1f17b804b1-4563cac2dso2299455e9.3 for <[email protected]>; Wed, 06 Aug 2025 12:14:28 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; ..... TRUNCATED ...... ``` We can track the server relay chain from `mail-wm1-f45.google.com (gsmtp)` -> `mail.yourdomain.com (Postfix)` -> `server1.yourdomain.com (Exim 4.95)` Postfix has checked rDNS, RBL, SPF, DKIM, DMARC before relaying. Lets reply to the email from the box back to gmail and watch the headers: ```txt Delivered-To: [email protected] Received: by 2002:a05:6000:18a3:b0:3b7:90b9:7d0b with SMTP id b3csp286773wri; Wed, 6 Aug 2025 12:19:45 -0700 (PDT) ..... TRUNCATED ...... ARC-Authentication-Results: i=1; mx.google.com; dkim=pass [email protected] header.s=mail header.b="JfDwbt"; spf=pass (google.com: domain of [email protected] designates XX.XX.XX.XX as permitted sender) [email protected]; dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=yourdomain.com Return-Path: <[email protected]> Received: from mail.yourdomain.com (mail.yourdomain.com. [XX.XX.XX.XX]) by mx.google.com with ESMTPS id facd0b8a97d-3b79c6d4ea5si90616f8f.839.2025.08.06.12.19.45 for <[email protected]> (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 06 Aug 2025 12:19:45 -0700 (PDT) Received-SPF: pass (google.com: domain of [email protected] designates XX.XX.XX.XX as permitted sender) client-ip=XX.XX.XX.XX; Authentication-Results: mx.google.com; dkim=pass [email protected] header.s=mail header.b="J/jfDwbt"; spf=pass (google.com: domain of [email protected] designates XX.XX.XX.XX as permitted sender) [email protected]; dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=yourdomain.com Authentication-Results: mail.yourdomain.com; dmarc=pass (p=quarantine dis=none) header.from=yourdomain.com Authentication-Results: mail.yourdomain.com; dkim=pass (2048-bit key; unprotected) header.d=yourdomain.com [email protected] header.a=rsa-sha256 header.s=mail header.b=J/jfDwbt; dkim-atps=neutral Received: from server1.yourdomain.com (unknown [10.0.0.2]) by mail.yourdomain.com (Postfix) with ESMTPS id D386F; Wed, 6 Aug 2025 19:19:44 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=yourdomain.com; s=mail; h=References:In-Reply-To:Cc:To:Subject:Message-ID: From:Content-Transfer-Encoding:Content-Type:Date:MIME- ..... TRUNCATED ...... Received: from [127.0.0.1] (helo=mail.yourdomain.com) by server1.yourdomain.com with esmtpa (Exim 4.95) (envelope-from <[email protected]>) id 1ujjfs-0082hO-6k; Wed, 06 Aug 2025 21:19:44 +0200 MIME-Version: 1.0 ..... TRUNCATED ...... ``` Using webmail we see `[127.0.0.1] server1.yourdomain.com (Exim 4.95)` -> `mail.yourdomain.com (Postfix)` -> `mx.google.com (gsmtp)` -> `2002:a05:6000:18a3:b0:3b7:90b9:7d0b with SMTP` The email was DKIM signed on origin and recived with TLS connection, wich is required by gmail. I've rushed this with AI, so if you have questions or fixes please comment below.