A personal project: running and managing a self-hosted Microsoft Exchange server from a home office: reliably sending and receiving mail from behind a residential internet connection, using a small cloud VPS as a mail gateway.
This write-up documents a personal home-lab / home-office project: keeping a full on-premises Microsoft Exchange server usable for real email while it lives on a residential internet line. Residential connections make self-hosted mail hard for three reasons, and this project solves all three with a small cloud VPS acting as a mail relay gateway, connected to the home server over a private WireGuard tunnel.
The key idea is to separate the public-facing role from the mailbox role. A cloud VPS with a clean static IP and a proper reverse-DNS (PTR) record becomes the only machine exposed to the internet. The home Exchange server never talks to the internet directly. It only speaks to the VPS through an encrypted tunnel it dials out itself.
192.168.1.10 / EXCHANGE.homelab.local.example.com in this article).example.com, the VPS public IP is 203.0.113.10, tunnel IPs are
10.8.0.1 (VPS) and 10.8.0.2 (Exchange), and the DDNS name is
myexchange.duckdns.org. Replace them with your own. Every token, password and
private key below is a placeholder. Never publish the real ones.Exchange can't send on port 25 directly, so it authenticates to the VPS over submission (port 587) and the VPS relays the message to the internet from its clean IP. The VPS also DKIM-signs the mail for our domain.
Create (or reuse) a mailbox on the VPS that Exchange will authenticate as, e.g.
relay@example.com, and give it a strong password. Exchange will send
From your real address while authenticating as this account, which is fine.
Generate a key and add the domain to OpenDKIM's key/signing tables:
# On the VPS
mkdir -p /etc/opendkim/keys/example.com
cd /etc/opendkim/keys/example.com
opendkim-genkey -b 2048 -d example.com -s default
chown opendkim:opendkim default.private && chmod 600 default.private
# /etc/opendkim/KeyTable
default._domainkey.example.com example.com:default:/etc/opendkim/keys/example.com/default.private
# /etc/opendkim/SigningTable (refile: allows the *@ wildcard)
*@example.com default._domainkey.example.com
# /etc/opendkim.conf (multi-domain mode)
KeyTable /etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
opendkim -n && systemctl restart opendkim # -n validates config before restart
default.txt holds the public key you'll publish in DNS (see Part 5).
Point Exchange's outbound at the VPS as a smarthost, with authentication over TLS:
# Exchange Management Shell
$cred = Get-Credential # relay@example.com + YOUR_RELAY_PASSWORD
New-SendConnector -Name "VPS Relay" -Custom -AddressSpaces "*" `
-SmartHosts "mail.myvps.net" -Port 587 `
-SmartHostAuthMechanism BasicAuth -AuthenticationCredential $cred `
-DNSRoutingEnabled $false -SourceTransportServers EXCHANGE
sasl_username=relay@example.com and status=sent (250 ...) to the
destination. If the VPS presents a self-signed certificate, use BasicAuth
(opportunistic TLS). The submission service still requires STARTTLS before accepting the login,
so credentials stay encrypted.A private tunnel lets the VPS reach the Exchange server without exposing any port at home. The home side dials out to the VPS, so a dynamic IP and NAT are non-issues.
apt-get install -y wireguard-tools
cd /etc/wireguard
wg genkey | tee server_private.key | wg pubkey > server_public.key
# /etc/wireguard/wg0.conf
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <VPS_PRIVATE_KEY>
[Peer] # the home Exchange
PublicKey = <EXCHANGE_PUBLIC_KEY>
AllowedIPs = 10.8.0.2/32
systemctl enable --now wg-quick@wg0
Allow inbound UDP 51820 on the VPS firewall.
Install WireGuard for Windows, then Import tunnel from file so it shows in the app and runs as an auto-starting service:
[Interface]
PrivateKey = <EXCHANGE_PRIVATE_KEY>
Address = 10.8.0.2/24
[Peer] # the VPS
PublicKey = <VPS_PUBLIC_KEY>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.8.0.1/32
PersistentKeepalive = 25
AllowedIPs = 10.8.0.1/32 keeps only tunnel traffic in the tunnel, so normal
networking is untouched. Verify from the VPS that it can reach Exchange's SMTP over the tunnel:
nc -zv 10.8.0.2 25 should return the Exchange banner.
Now make the VPS accept mail for the domain and relay it to Exchange over the tunnel. Because Postfix queues and retries automatically, if the home server is temporarily offline the VPS holds the mail and delivers it when the tunnel comes back: a real store-and-forward buffer.
If the domain was previously a local mail domain on the VPS (e.g. under a control panel), first disable local mail for it so it isn't delivered locally, then configure the relay:
# /etc/postfix/transport -> route the domain to Exchange over the tunnel
echo "example.com smtp:[10.8.0.2]:25" > /etc/postfix/relay_transport
postmap /etc/postfix/relay_transport
postconf -e "relay_domains = example.com"
postconf -e "transport_maps = hash:/etc/postfix/relay_transport, \$transport_maps"
postfix check && postfix reload
On the Exchange side, its default anonymous receive connector on port 25 already accepts mail
for an authoritative accepted domain, so mail arriving over the tunnel is delivered straight to
the mailbox. Test by sending to the VPS's port 25 and watching for
relay=10.8.0.2[10.8.0.2]:25 ... status=sent (250 ...), then confirm the message
lands in the mailbox.
relay_domains / transport_maps may be reverted by a
"repair mail" operation. Keep a backup of main.cf and re-apply if needed, and always
run postfix check before reloading on a server that hosts other domains.Mail no longer depends on the home IP (the MX points to the VPS). But if you want to reach the mailbox from outside (OWA / mobile ActiveSync) directly, you still need a name that tracks the changing home IP. A tiny DuckDNS updater on the Exchange box handles it.
REM C:\DuckDNS\duck.bat
@echo off
C:\Windows\System32\curl.exe -k -s -o C:\DuckDNS\duck.log ^
"https://www.duckdns.org/update?domains=myexchange&token=YOUR_DUCKDNS_TOKEN&ip="
REM Run it every 5 minutes as SYSTEM (PowerShell, one time)
$a = New-ScheduledTaskAction -Execute "C:\DuckDNS\duck.bat"
$t = New-ScheduledTaskTrigger -Once -At (Get-Date)
$t.Repetition = (New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Minutes 5)).Repetition
$p = New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName "DuckDNS Update" -Action $a -Trigger $t -Principal $p
C:\DuckDNS\duck.log shows OK on success, so it's easy to check any time.
The mail entry point is the VPS's static IP, so every record is static. Client-access names point to the DuckDNS host so OWA/mobile still reach home.
| Type | Host | Value | Purpose |
|---|---|---|---|
| A | mx | 203.0.113.10 | MX target = VPS |
| MX | @ | 10 mx.example.com | mail enters at the VPS |
| CNAME | mail, autodiscover | myexchange.duckdns.org | client access โ home |
| TXT | @ | v=spf1 ip4:203.0.113.10 -all | SPF (authorize the VPS) |
| TXT | default._domainkey | v=DKIM1; h=sha256; k=rsa; p=YOUR_PUBLIC_KEY | DKIM public key |
| TXT | _dmarc | v=DMARC1; p=none; rua=mailto:you@example.com | DMARC (monitor) |
| SRV | _autodiscover._tcp | 10 10 443 autodiscover.example.com | Outlook autodiscover |
mx.example.com),
never a CNAME. Keep the DKIM public key on one line. Start DMARC at p=none and only
move to quarantine/reject once you've confirmed all legitimate mail
aligns on SPF and DKIM.
Confirm the live zone from the authoritative nameservers and from public resolvers:
# MX + SPF + DKIM as the world sees them
dig +short MX example.com
dig +short TXT example.com
dig +short TXT default._domainkey.example.com
# From the VPS: cryptographically confirm DKIM public == private
opendkim-testkey -d example.com -s default -vvv # -> "key OK"
A deliverability checker (e.g. MXToolbox) should show the MX published and SPF/DKIM/DMARC present. Finally, send a real message to a strict provider (Gmail/Outlook) and confirm it lands in the Inbox with SPF and DKIM passing. For a brand-new sending IP, marking the first message "not spam" trains the recipient's filter quickly.
postfix check.