Skip to content

Instantly share code, notes, and snippets.

@pabloko
Last active August 16, 2025 21:25
Show Gist options
  • Save pabloko/073afecfc64886b44851145a8eb16e29 to your computer and use it in GitHub Desktop.
Save pabloko/073afecfc64886b44851145a8eb16e29 to your computer and use it in GitHub Desktop.

Revisions

  1. pabloko revised this gist Aug 16, 2025. 1 changed file with 5 additions and 3 deletions.
    8 changes: 5 additions & 3 deletions VPS Email Relay.md
    Original 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 = $myhostname
    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]:25
    otherdomain.com smtp:[10.0.0.2]:25
    yourdomain.com smtp:[10.0.0.2]:587
    otherdomain.com smtp:[10.0.0.2]:587
    ```
    also `/etc/postfix/relay_domains`
    ```ini
  2. pabloko revised this gist Aug 15, 2025. 1 changed file with 11 additions and 2 deletions.
    13 changes: 11 additions & 2 deletions VPS Email Relay.md
    Original 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, 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_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]:25" >> "$TRANSPORT_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"
  3. pabloko revised this gist Aug 14, 2025. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions VPS Email Relay.md
    Original 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.almeriahost.com
    myorigin = mail.almeriahost.com
    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
  4. pabloko revised this gist Aug 14, 2025. 1 changed file with 141 additions and 115 deletions.
    256 changes: 141 additions & 115 deletions VPS Email Relay.md
    Original 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
    HESTIA

    ```ini
    # HESTIA (client)
    [Interface]
    PrivateKey = A_PRIVATE_KEY
    PrivateKey = __A_PRIVATE_KEY__
    Address = 10.0.0.2/24
    [Peer]
    PublicKey = B_PUBLIC_KEY
    Endpoint = VPS_FIXED_IP:18632
    PublicKey = __B_PUBLIC_KEY__
    Endpoint = __VPS_FIXED_IP__:18632
    AllowedIPs = 10.0.0.1/32
    PersistentKeepalive = 25
    ```
    VPS
    ``` ini
    # VPS (server)
    [Interface]
    PrivateKey = B_PRIVATE_KEY
    PrivateKey = __B_PRIVATE_KEY__
    Address = 10.0.0.1/24
    ListenPort = 18632
    [Peer]
    PublicKey = A_PUBLIC_KEY
    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*, then relay to the remote.
    - 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 `/etc/ssl/certs/fullchain.pem - /etc/ssl/private/privkey.pem`
    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
    # 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
    # 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
    # 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_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
    compatibility_level = 3.6
    ```
    (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
    -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)

    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
    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:
    Edit or create this configuration files:
    ```ini
    Mode v
    ...
    #Socket local:/run/opendkim/opendkim.sock
    # /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
    ```
    Do the same for `/etc/opendmarc.conf`
    ```ini
    #Socket local:/run/opendmarc/opendmarc.sock
    Socket inet:8893@localhost
    TrustedAuthservIDs 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_banner = server1.yourdomain.com
    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 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)
    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.
    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 and transport files ###
    echo "Generating relay_domains and transport from /etc/exim4/domains..."
    ### 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, and domain config 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: Rebuild DB and reload Postfix ###
    echo "Reloading Postfix on VPS..."
    ### 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"
    rm "$RELAY_DOMAINS_FILE" "$TRANSPORT_FILE" "$SIGNING_FILE" "$KEY_FILE"

    echo "✅ All done. Certificates and domain configs updated on VPS."
    echo "✅ All done. Certificates, domain configs, and DKIM settings updated on VPS."
    ```
    Now the configuration can be synchronized quickly calling this script.

  5. pabloko revised this gist Aug 11, 2025. 1 changed file with 7 additions and 93 deletions.
    100 changes: 7 additions & 93 deletions VPS Email Relay.md
    Original 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 to do this under configuration, but ill leave here the config i've done:
    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
    # 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
    ...
    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";
    }
    ```

    ### 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.
    DANE and other would require DNSSEC
  6. pabloko revised this gist Aug 7, 2025. 1 changed file with 34 additions and 2 deletions.
    36 changes: 34 additions & 2 deletions VPS Email Relay.md
    Original 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
    -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.
  7. pabloko created this gist Aug 6, 2025.
    425 changes: 425 additions & 0 deletions VPS Email Relay.md
    Original 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.