HOW TO KEEP EMAIL HISTORY OFF YOUR PROVIDER: FETCHMAIL, DOVECOT AND A VPN
Most small and mid-sized companies keep their mail where their provider puts it: in mailboxes on internet-facing servers, often going back many years. Anyone who compromises those servers therefore gains access to the complete correspondence history of the company, and a single stolen mailbox password exposes the full history of that mailbox. End-to-end encryption with OpenPGP or S/MIME would protect the content, but only for correspondents who use it as well, and in practice most mail arrives unencrypted. For a security company, whose mail contains findings, customer data and contract negotiations, we consider a long history on the provider’s servers an unacceptable concentration of risk.
We therefore want our mail stored on systems we control, inside our VPN. At the same time, we do not want to run mail servers on the public internet: keeping them secure and off spam blocklists is, in my experience, close to a full-time job, and we already have one. This led us to a middle path with three goals:
- No history at the provider. An attacker who controls the provider sees only mail that passes through it while the compromise lasts.
- No mail service of our own on the internet. Mail clients reach our server only through the VPN.
- No lost mail. When our server is offline, incoming mail waits until it is back.
In short, the provider keeps doing what it does well, receiving and delivering mail on the internet, but it no longer stores anything. The rest of this post describes how we set this up with fetchmail, Dovecot and Postfix, including the mistakes we made along the way.
Implementation
The domain has a single catch-all mailbox at the provider that receives mail for every address. An internal mail server inside our VPN holds a permanent connection to that mailbox, downloads new messages within seconds, and deletes them at the provider afterwards. Outgoing mail takes the opposite route: staff submit it to the internal server, which relays it through the provider’s submission service. The following sketch shows the path of an incoming message.
Internet ──▶ provider (catch-all mailbox)
│ fetchmail, IMAP IDLE, deletes after download
▼
internal mail server (inside the VPN)
│ Dovecot delivery and Sieve rules
▼
staff mailboxes, reachable only through the VPN
External recipients see mail from the provider’s servers, with its established reputation and its SPF and DKIM setup. Mail between colleagues never leaves our network at all. Access to the internal server goes through our VPN, which requires a client certificate, a user account and a one-time password, as described in our post on hardening OpenVPN.
Fetching mail from the provider
fetchmail logs in to the catch-all mailbox and hands every message to Dovecot. The provider records the original recipient of each message in a header, so fetchmail can deliver it to the right local mailbox even though all mail arrives in one place. The following listing shows our /etc/fetchmailrc.
poll mail.provider.example with proto IMAP
envelope "Envelope-To" # header in which the provider records the recipient
localdomains example.com
username "catchall@example.com"
password "..."
smtphost "/var/run/dovecot/lmtp" # deliver directly to Dovecot via LMTP
is * # local recipient = address from the header
no keep # delete at the provider after delivery
ssl
sslcertck
idle # wake up as soon as new mail arrives
Two lines deserve a closer look. The envelope line names the header in which the provider records the original recipient. Its name differs between providers: besides Envelope-To, X-Original-To and Delivered-To are common, and the headers of any received message show which one yours uses. The idle line keeps the connection open, so that the provider notifies fetchmail of new mail immediately. In our tests, a message from an external account reached the internal mailbox about one second after the provider accepted it.
On Debian and Ubuntu, running fetchmail as a system service requires three further steps outside the configuration file. The daemon is enabled with START_DAEMON=yes in /etc/default/fetchmail. Because the configuration file contains the password, only the fetchmail user should be able to read it (chown fetchmail /etc/fetchmailrc and chmod 600 /etc/fetchmailrc). Finally, fetchmail needs access to Dovecot’s LMTP socket, which belongs to the vmail group (see the Dovecot configuration below), so the fetchmail user must be a member of that group (usermod -aG vmail fetchmail).
Delivering mail with Dovecot
Dovecot stores the mail and serves it to clients over IMAP. Two features make it fit this design: it accepts deliveries over LMTP from both fetchmail and Postfix, and it can give every address without an account a shared mailbox, whose Sieve script implements forwards for shared addresses. The following listing shows the relevant part of our configuration.
mail_location = maildir:/srv/mail/%d/%n
mail_uid = vmail
mail_gid = vmail
first_valid_uid = 5000
last_valid_uid = 5000
ssl_cert = </etc/dovecot/private/dovecot.pem
ssl_key = </etc/dovecot/private/dovecot.key
# Staff accounts, one per line: user@example.com:<hash from doveadm pw>:5000:5000:::
passdb {
driver = passwd-file
args = scheme=CRYPT username_format=%u /etc/dovecot/users
}
userdb {
driver = passwd-file
args = username_format=%u /etc/dovecot/users
default_fields = uid=vmail gid=vmail mail=maildir:/srv/mail/%d/%n home=/srv/mail/%d/%n
skip = notfound
}
# Every other address shares one mailbox and one Sieve script
userdb {
driver = static
args = uid=vmail gid=vmail mail=maildir:/srv/mail/%d/shared allow_all_users=yes home=/srv/mail/%d/shared
}
protocols = imap lmtp
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
plugin {
sieve = ~/.dovecot.sieve
}
# IMAP only with TLS on port 993
service imap-login {
inet_listener imap {
port = 0
}
}
service lmtp {
# for Postfix
unix_listener /var/spool/postfix/private/dovecot-lmtp {
user = postfix
group = postfix
mode = 0660
}
# for fetchmail
unix_listener lmtp {
group = vmail
mode = 0660
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
user = postfix
group = postfix
mode = 0660
}
}
Because the static user database accepts every address, no mail for the domain is rejected. Whatever has no account of its own ends up in the shared mailbox, where the Sieve script decides what happens to it.
Forwarding shared addresses
Shared addresses, such as a contact address, have no account. Their mail is processed by the Sieve script of the shared mailbox, which redirects it to the responsible people. The following listing shows two such rules.
require ["envelope"];
if envelope :is "to" "contact@example.com" {
redirect "alice@example.com";
redirect "bob@example.com";
stop;
}
if envelope :is "to" "invoices@example.com" {
redirect "alice@example.com";
stop;
}
# Mail that matches no rule stays in the shared mailbox.
The rules test the envelope recipient, not the provider’s recipient header, and that is the most important lesson of this setup. My first version matched on the header. It worked perfectly for external mail and silently failed for mail between colleagues: Postfix delivers such mail directly to Dovecot, so it never carries the provider’s header, and it quietly stayed in the shared mailbox. The envelope recipient is set on both paths. It also removes a second problem: a header can list several recipients and thereby trigger several rules, whereas Dovecot processes each recipient separately. Before switching, Pigeonhole’s sieve-test is useful for running the old and new script against the same test message without delivering anything.
Sending mail through the provider
Postfix accepts mail from staff and relays external mail through the provider’s submission service, logged in with the catch-all account. Mail to the company’s own domain is delivered locally instead and never leaves the network. The following listing shows the relevant settings in main.cf.
# The whole domain is delivered locally via Dovecot.
# mydestination must not list the domain itself.
mydestination = $myhostname, localhost
virtual_mailbox_domains = example.com
virtual_transport = lmtp:unix:private/dovecot-lmtp
# External mail goes through the provider's submission service
relayhost = [mail.provider.example]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
smtp_header_checks = regexp:/etc/postfix/header_checks
# Staff log in with their Dovecot account over TLS; nobody else may send
smtpd_tls_cert_file = /etc/dovecot/private/dovecot.pem
smtpd_tls_key_file = /etc/dovecot/private/dovecot.key
smtpd_tls_security_level = encrypt
smtpd_tls_auth_only = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_recipient_restrictions = permit_sasl_authenticated, reject
smtpd_sender_login_maps = pcre:/etc/postfix/sender_login_maps.pcre
smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch, permit_sasl_authenticated, reject
Three small files complete the setup. The password map holds the credentials for the provider. It must be readable only by root and needs a postmap /etc/postfix/sasl_passwd after each change. The header check removes Received headers that would reveal internal addresses to recipients. The sender map ensures that each user can send only from their own address, while the catch-all account may use every address of the domain. The following listing shows all three.
# /etc/postfix/sasl_passwd
[mail.provider.example]:587 catchall@example.com:PASSWORD
# /etc/postfix/header_checks
/^Received:.*(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.).*/ IGNORE
# /etc/postfix/sender_login_maps.pcre
/^(.*@example\.com)$/ $1, catchall@example.com
Staff submit mail on port 465 with implicit TLS, which the smtps service in master.cf provides with smtpd_tls_wrappermode=yes and smtpd_sasl_auth_enable=yes. We disabled the standard smtp service on port 25 entirely, since the internal server never receives mail from other mail servers.
Limitations
The provider still sees every message in transit, in both directions. An attacker who controls it, or who obtains the credentials of the catch-all mailbox, can read and modify mail for as long as the compromise goes unnoticed. Those credentials are therefore the most valuable secret in the setup: they give access to all incoming mail at the provider, and because the sender map lets the catch-all account use every address of the domain, they also allow sending mail under any company address. Unlike mailbox passwords, however, they are under the control of IT rather than individual users. They can therefore be long, random and changed regularly, and no one at the provider depends on staff choosing good passwords.
The provider’s own backups may also retain recent messages for a period we do not control. The buffering has limits, too: the catch-all mailbox has a storage quota, and after a long enough outage the provider will start rejecting mail. Sending requires the internal server, although Postfix retries queued mail automatically.
The internal server, in turn, now holds the complete history and becomes the asset to protect, which makes its backups as important as the server itself. Encrypting mail at rest, for example with the OpenPGP keys of individual staff members at delivery time, would reduce the damage of a compromise of the internal server.
Conclusion
This setup keeps the parts of mail hosting that a small or mid-sized company should not run itself, deliverability and the exposed internet-facing service, at the provider. Our mail history now lives where our other sensitive data lives: inside a network that is reachable only with a certificate, an account and a one-time password. None of the building blocks is new; fetchmail in particular has been around for decades. What makes the combination worthwhile is that it limits what a compromise of the provider yields: instead of years of correspondence, an attacker obtains only the mail that passes through while the compromise lasts.
Questions like this one, what an attacker gains when a single component is compromised, are part of our security assessments. If you would like to know how your mail, VPN or other internal infrastructure would hold up, we review the design and test the running configuration.