Encrypting Stored Email with Postfix

by kacang bawang

This post is about something that has been on my mind for a while, but somehow always seemed too complex, too far from a practical implementation. Until now, bwa-ha-ha. Encrypted email. Before you get too excited, let me first say that we’re not talking about a mathematically perfect system, and it is certainly not NSA-proof. But! It is about as practical as it gets. We get a system where all email is stored on disk in encrypted form. This is a Big Deal. If your VPS falls to a 0-day for a few hours, the evil hacker won’t be able to grab a dump of your naked pictures email. Really, in any situation where an adversary gains temporary control over the server, this scheme will keep the contents of your emails secret. Just remember: IMAP messages tend to build up and stay on the server indefinitely… I mean, when did you last clean your inbox 😉

Before proceeding further, I must give credit for the idea behind this approach to Mike Cardwell. In a twopart post he describes the method and provides scripts for GnuPG encrypting your emails as they arrive on the server. My contribution will be in the form of the use of a different MTA – Postfix, a different approach to encrypting sent messages and a slightly more detailed description of the steps.


The idea is briefly the following: trigger every message arriving/leaving from the server to be fed through an encryption function. The encryption function encrypts the body of the email if it finds a public key that corresponds to its recipient in its local store. These are public keys, so there’s nothing wrong with storing them on the server. If no key is found corresponding to the recipient email address, the message is left plain-text. Additionally, if the message is already PGP encrypted it is not encrypted again.

For dealing with the ‘sent’ folder, we will rely exclusively on Thunderbolt rather than trying to process sent messages as they leave the server. The achieved effect is virtually identical, but is a lot simpler to set up.


We will refer to two machines – one local, running Thunderbird/Enigmail combo and one remote which is our mail server. I will assume that gpg is already setup on both – check by doing

Create public/private gpg keys for the Thunderbird user (or use existing, if you have them already):

Now let’s prepare the mailserver. Create an unpriveleged user for running our script:

Set up gpg for the this user:

Install (clone) gpgit from here into /opt/gpgit

if needed, install Mail::GnuPG like this:

To test the gpgit.pl script, call command below, then type, then Ctrl-D

Encryption of Arriving Mail with Postfix

Ok, now we’re finally to the Postfix-specific part. What we’re essentially trying to do is a content filtering operation on the message. We want to yank it out of the queue, and, based on the content produce a modified content and return the updated message to the mail queue for delivery. It is not that different from what a spam filter does. There are 3 places to tap into in order to achieve the desired result (manual). We will choose the after queue method in order to relieve pressure on RAM and CPU, and provide the most reliable service. In other words, we will add -o content_filter=my_filter option to smtp commands, where my_filter is another config element that specifies an external shell script to which the message contents will be passed.

Here’s where it gets a somewhat difficult. You may already have a content filter setup (if you have a spam filter, for example), in which case you will have to combine the two filters together. Also, this content filter will be applied to all message, incoming and outgoing. That’s just how postfix works. The reason that our script only affects the incoming messages is because so far, we have only installed our own gpg key. Thus the only messages able to be encrypted are those that have us as the recepient, which pretty much means the message is incoming. If we added a friend’s key into the keystore on the server then all messages sent to the friend would be encrypted, even if we originally sent them plain-text (from Thunderbird).

Only the message body is fed to the script for changing, thus headers will not get encrypted no matter what. This reveals “from”, “time” and other metadata. Not ideal, but at least the content is safe. So, like I said, not NSA-proof, but nevertheless useful.

modify postfix configs, master.cf:

Note: if you run several services like smtp, smtps, submission, you will need to do the above for each. Each port (25/smtp, 465/smtps, 587/submission) is treated separately by postfix.

At the end of master.cf, add the code for the gpgit-pipe hook:

We do get some useful parameters to pass into our script above, like ${sender} and ${recepient}, so you can base some logic around those to make the scripts more robust.

create gpgit_postfix.sh (don’t forget permissions 755) in /opt/gpgit/ with the following contents.

In postfix, the command called from content_filter will receive the body of the message on stdin after all other parameters. The result of the command will be returned to the caller (smtpd) with the exit code. If code is non-zero, a bounce reply will be sent and the message will count as bounced. In our code above the pipe construct xxx | yyy will swallow the return code from xxx. To fix this, we set -o pipefail, which will not overwrite non-zero return codes in $? unless all return codes in the script were 0. This preserves the result of ${GPGIT} $4, which would otherwise have been stomped by the result of ${SENDMAIL} $@. Even if the return code of our filter script is 0 (success), the message given to it is out of the queue and will not re-enter it. It was basically consumed by the content_filter. Thus, we must re-send it ourselves, from within the filter. That is what the call to $SENDMAIL does. The beauty of this piped construct is in that the message body is never saved as a file on the mail server disk. If it was, it could potentially be recovered via forensic disk analysis, which is undesirable. Note that messages given to sendmail must not be content filtered, otherwise we will get a loop. Some more advanced filtering techniques are described here, but I will not go into those in this post.

At this point we should be able to send some messages and receive them in the second account somewhere as plain text. Upon clicking on the same message in our email reader we should get prompted for the gpg private-key password. You can also find the message file on the server (if you use files in your setup) and the contents should be gpg encrypted if you open it in a text editor.

Some drawbacks: the ‘sent’ folder is still plain text (we’ll get to that shortly) and any mails that do not leave the system (like Logwatch or Fail2Ban messages) will not get encrypted, since they go only through sendmail and not smtpd. Bounced reply messages (which contain copy of original message) are not encrypted also, since they never reach the smtp step. Try not to send any.

Encrypting the Sent Folder

In Mike Cardwell’s second post he sets up a system where the outgoing mail on his Exim mail server is processed in a script (Exim can treat incoming/outgoing separately). The script then uses the IMAP API to add the message to the user’s ‘Sent’ folder. After initially trying to replicate his setup, I ultimately abandoned the idea for the following reasons.

1. Postfix does not differentiate incoming mail from outgoing mail, so everthing had to be handled in the same hook, namely -o content_filter. This forced me to have to save to the filesystem the contents of stdin, temporarily. This is of course vulnerable to forensic recovery. There are some crutches around the problem such as ram-drives and encrypted folders/volumes, but there is a bigger problem.

2. In order to make changes to the IMAP mailbox one needs the IMAP username/password. There is no way to set-up key-based, or even non-interactive logins without having to store the plaintext credentials somewhere. This is true for both courier and dovecot. We would need each user’s credentials, or else a “master” account which would be able to write (and read) any mailbox. This is highly counter-productive to what we’re trying to achieve here.

So, here is what I did instead. I turned off save sent copy in t-bird, and set an auto bcc to self in each t-bird account. Also added a mail filter that if a message is exactly from myself then move it to sent folder. This allows me to keep a copy of the sent message, in the sent folder, in encrypted form, without ghost files on disk. It’s not as elegant as I had hoped for, but it works surprisingly well.

If you’re like me, then there is a couple of more things you will probably want, that are beyond the scope of this post and are easily googlable. First, scrubbing the outgoing “Received: from” headers. They can be surprisingly revealing. Second is removal of UserAgent reporting in Thunderbird.