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 two–part 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.
Approach
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.
Implementation
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
1 |
gpg --version |
Create public/private gpg
keys for the Thunderbird user (or use existing, if you have them already):
1 2 |
gpg --gen-key //requires a password to protect priv key gpg --output revoke.asc --gen-revoke //in case password is forgotten |
Now let’s prepare the mailserver. Create an unpriveleged user for running our script:
1 |
adduser --shell /bin/false --home /var/opt/gpgit --disabled-password --disabled-login --gecos "" gpgit |
Set up gpg
for the this user:
1 2 3 4 5 6 7 8 |
mkdir /var/opt/gpgit/.gnupg chown gpgit:gpgit /var/opt/gpgit/.gnupg chmod 700 /var/opt/gpgit/.gnupg # ORDER OF OPTIONS IMPORTANT! sudo -u gpgit /usr/bin/gpg --homedir=/var/opt/gpgit/.gnupg --import support@kacangbawang.com.ascii.gpg sudo -u gpgit /usr/bin/gpg --homedir=/var/opt/gpgit/.gnupg --list-keys # give ultimate trust (5) to the imported key sudo -u gpgit /usr/bin/gpg --homedir=/var/opt/gpgit/.gnupg --edit-key support@kacangbawang.com trust quit |
Install (clone) gpgit
from here into /opt/gpgit
if needed, install Mail::GnuPG
like this:
1 2 3 4 5 6 7 |
# configure cpan by running it: $cpan, then follow prompts # install cpanminus cpan App::cpanminus # Now install module. cpanm Mail::GnuPG # or any others you may be missing # cpanm XXX::YYY |
To test the gpgit.pl script, call command below, then type, then Ctrl-D
1 2 3 4 |
# this should produce a text file with some b64 data sudo -u gpgit /opt/gpgit/gpgit.pl support@kacangbawang.com > /var/opt/gpgit/success # this should leave plaintext as is sudo -u gpgit /opt/gpgit/gpgit.pl doesnt@exit.com > /var/opt/gpgit/error |
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
:
1 2 |
#add '-o content_filter=gpgit-pipe' to every 'smtpd' command, for example: smtp inet n - - - - smtpd -o content_filter=gpgit-pipe |
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:
1 2 |
gpgit-pipe unix - n n - - pipe flags=Rq user=gpgit argv=/opt/gpgit/gpgit_postfix.sh -oi -f ${sender} ${recipient} |
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.
1 2 3 4 5 6 7 8 9 |
#!/bin/bash SENDMAIL=/usr/sbin/sendmail GPGIT=/opt/gpgit/gpgit.pl #encrypt and resend directly from stdin set -o pipefail ${GPGIT} "$4" | ${SENDMAIL} "$@" exit $? |
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.
PS.
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.
very good implementation. I want to try it with sendmail
Please let me know how it turns out. Between Mike Cardwell’s, mine and your posts that should have almost all popular MTAs covered 🙂
it works with sendmail too!!
anyway I am trying to see if I can find a recipe to auto-encrypt mail which goes to Sent folder
This is an awesome post I’m glad you documented this with Postfix. Any chance you could create one utilizing S/MIME? That way iPhones could decrypt in the built in mail app?
No problem, glad you found it useful. Haven’t really had a chance to look at S/MIME yet, as I’m currently in the middle of a massive Bacula setup…
When deploying this encryption, I guess you also encrypted your old emails. Could you please share a script that you have used?
Thanks
I initially set this up on a more or less empty mail server, so I did not do anything to the existing messages. You do raise a good point though. I will look into it.
Doesn’t
${GPGIT} “$4”
Just encrypt the recipient address and not the message?
Hi Stan, take a look at the gpgit pipe hook above. sender = $2, recepient = $3 and body follows as $4. Not sure whether -oi and -f get joined into a single argument when the hook is called, but this exact setup is how I have it working right now.
Sender and recipient email addresses are actually not encrypted with gpg, that is one of its weaknesses.
For S/Mime there are some similiar projects on github:
https://github.com/milte/smime-encrypt-milter
or
https://github.com/jobisoft/encrypt-smime
Hello! Before I implement that system on my mail server, let me ask a question, because I have a somewhat special set up on my mail server.
I have two mailboxes set up:
1) myname@mydomain.com
2) throwaway@mydomain.com
The first one is nothing special and works as you would expect. The second one, though, is a catch-all address. So everything that does not go to myname@mydomain.com goes to the second one. The advantage being that I am able to use different e-mail addresses for different services on the internet.
The problem now lies in the nature of PGP-keys; of course I have a key for myname@mydomain.com, but not for every single address that I use to pipe e-mail to throwaway@mydomain.com.
So, would it be possible to set it up in a way that links PGP-keys to inboxes rather than e-mail addresses?
Thanks!
Hi PK, the set-up that you desire is exactly how it will work. The key is to apply the PGP to the catch-all (eg. throwaway@mydomain.com), but not the throw-away instances (eg. fdsfsd01@mydomain.com). HTH
Great! Thank you very much for the quick reply.
Hi,
I created this small script to automate key imports and trusting.
I can thus generate a key on my local machine, scp it to the mail server and send a CLI with ssh and the name/path to the key.
#!/bin/bash
if [ -f $1 ]; then
printf “Importing key $1…\n”
sudo -u gpgit /usr/bin/gpg2 –homedir=/var/opt/gpgit/.gnupg –import $1
printf “Giving ultimate trust to key $1…\n”
sudo -u gpgit /usr/bin/gpg2 –homedir=/var/opt/gpgit/.gnupg –edit-key $1 trust quit <<< $'5\no\n'
else
printf "File $1 does not exist. Upload it using SCP."
fi
Thank you for sharing Sumi
Hi,
Thanks for this!
I got it working but only if I comment out my spamassassin config in /etc/postfix/master.cf
How would I incorporate it into this?
Here’s what I have:
smtp inet n – n – – smtpd
-o content_filter=spamassassin
spamassassin unix – n n – – pipe
user=spamd argv=/usr/bin/spamc -f -e
/usr/sbin/sendmail -oi -f ${sender} ${recipient}
Or rather here’s what I have to get it to work (spamassassin commented out):
smtp inet n – – – – smtpd
-o content_filter=gpgit-pipe
#smtp inet n – n – – smtpd
# -o content_filter=spamassassin
#spamassassin unix – n n – – pipe
# user=spamd argv=/usr/bin/spamc -f -e
# /usr/sbin/sendmail -oi -f ${sender} ${recipient}
I started to administer someone’s postfix server. Why some e-mail content is encrypted and some not ? The internal e-mails are not (between users) and content can be read while some e-mails arriving from large companies are stored in an encrypted manner. How does this happen ? How can I alter the settings in both cases ?
Indeed, local messages do not get encrypted. This is because they do not pass through the postfix queue where the encryption takes place. Or perhaps they exit the queue at an earlier stage. Unfortunately I do not have a ready solution for you at this time…