From 03095f0d7ccc873a607199497c373b063d7b84bf Mon Sep 17 00:00:00 2001 From: revsuine Date: Sat, 14 Dec 2024 00:04:16 +0000 Subject: [PATCH] content/blog/pgp_encrypting_all_incoming_emails.md: new article --- .../pgp_encrypting_all_incoming_emails.md | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 content/blog/pgp_encrypting_all_incoming_emails.md diff --git a/content/blog/pgp_encrypting_all_incoming_emails.md b/content/blog/pgp_encrypting_all_incoming_emails.md new file mode 100644 index 0000000..d7bdeb9 --- /dev/null +++ b/content/blog/pgp_encrypting_all_incoming_emails.md @@ -0,0 +1,257 @@ ++++ +date = '2024-12-13T16:06:23Z' +draft = false +title = 'PGP Encrypting All (Incoming) Emails' +tags = ['mail server', 'alpine linux', 'dovecot', 'sieve'] ++++ + +Let's say we want emails on our mail server to be encrypted at rest, such that only the user has the key. Luckily, +there already exists a popular solution for encrypting emails such that only the recipient can read them: OpenPGP. + +Using [Dovecot Sieve scripts](https://doc.dovecot.org/main/core/plugins/sieve.html), we can easily PGP-encrypt all +incoming email for a user. + +A lot of people have done this before, and I didn't come up with the idea. Please see the [Further +reading](#further-reading) section for some recommended articles I referred to. + +The only prerequisite is an existing Dovecot server set up and running. This guide will be 100% compatible with [my +mail server guide](/blog/mail_server_alpine_postfix_dovecot_tutorial/). This guide assumes you are using system users +as mail users, and may require changes if you are using virtual users. + +This instructions should be distro-agnostic, though it was written for an Alpine Linux server. I think the only +Alpine-specific part should be how to install the required packages, which can just be replaced by the relevant command +for your distro's package manager.[^alpine_searchability] + +This is compatible with Dovecot's mail\_crypt plugin, because mail\_crypt's encryption is transparent to the user. + +Finally, this only encrypts incoming mail, because Sieve scripts aren't applied to outgoing mail. + +# Admin guide + +Install Pigeonhole (a Sieve implementation for Dovecot): + + # apk add dovecot-pigeonhole-plugin + +Set Dovecot to use Pigeonhole. Edit `/etc/dovecot/conf.d/20-lmtp.conf`: + +```conf +protocol lmtp { + mail_plugins = $mail_plugins sieve +} +``` + +If you use LDA, you should do: + +```conf +protocol lda { + mail_plugins = $mail_plugins sieve +} +``` + +Now set Dovecot to use the `sieve_extprograms` Sieve plugin. This allows Sieve to run external executables. Don't +worry; it won't allow users to execute arbitrary executables, but only executables you specify. + +`sieve_extprograms` may come installed with Pigeonhole, or you may have to install it separately. For me, my +`/etc/dovecot/conf.d/90-sieve.conf` contains a comment that states: + +> The sieve\_extprograms plugin is included in this release. + +To enable `sieve_extprograms`, anywhere in your Dovecot config (I put it in `/etc/dovecot/conf.d/90-sieve.conf`): + +```conf +plugin { + sieve_plugins = sieve_extprograms + sieve_extensions = +vnd.dovecot.filter + sieve_filter_bin_dir = /etc/dovecot/sieve-filters +} +``` + +We add `vnd.dovecot.filter` to the list of Sieve extensions, to allow users to use the `filter` Sieve command. A filter +is an executable that takes an email from stdin, performs an action on it, and outputs the modified email to stdout. + +By specifying `sieve_filter_bin_dir`, we are saying that we will place any Sieve filters in +`/etc/dovecot/sieve-filters`. + +> WARNING! +> +> Users can execute *any executable* you place in `/etc/dovecot/sieve-filters`. Only put executables you trust in +> there! + +Sieve filters will be executed with the following environment variables, and *only* the following environment +variables: + +* `HOME` +* `USER` +* `SENDER` +* `RECIPIENT` +* `ORIG_RECIPIENT` + +They can take one argument specified by the user in their Sieve script. + +Now let's add the Sieve filter itself. This can be any executable that takes an email from stdin and outputs the +PGP-encrypted email from stdout. + +[Here's one written in Perl](https://gitlab.com/mikecardwell/gpgit). I had trouble with installing the Perl +dependencies on Alpine, so I ended up using [a Python script by Julian Andres +Klode](https://github.com/julian-klode/ansible.jak-linux.org/blob/dovecot/roles/mailserver/files/usr/local/lib/dovecot-sieve-filters/gpgmymail). + +On Alpine Linux, to use this script, you should install [python-gnupg](https://gnupg.readthedocs.io/en/latest/) as: + + # apk add py3-gnupg + +Now place the executable you want to use in `/etc/dovecot/sieve-filters` and make it executable (`chmod +x`). You can +see the version of Julian Andres Klode's script I use on my server +[here](https://git.revsuine.xyz/revsuine/gpgmymail). + +You also need to make sure that users can set their own personal Sieve scripts. You can set: + +```conf +plugin { + sieve = ~/.dovecot.sieve +} +``` + +to make it so that a user's Sieve script would be at `~/.dovecot.sieve`. You can also set + +```conf +plugin { + sieve = file:~/sieve;active=~/.dovecot.sieve +} +``` + +so that `~/sieve/` is a directory full of sieve scripts, and the active one is symlinked at `~/.dovecot.sieve`. See +[Dovecot docs on Sieve script locations](https://doc.dovecot.org/main/core/plugins/sieve.html#script-locations), or [my +further explanation of the `sieve = file:~/sieve;active=~/.dovecot.sieve` example on my previous blog +post](/blog/mail_server_alpine_postfix_dovecot_tutorial/#installing-and-setting-up-pigeonhole). + +Restart Dovecot for your changes to take effect: + + # rc-service dovecot restart + +You are now done from the admin side of things. + +# User guide + +In order for gpgmymail (the script linked above) to have the user's public PGP key, they need to import it to their +system GnuPG keyring. If they have shell access, + + user@localhost $ gpg --export --armor user@revsuine.xyz > public.asc + user@localhost $ scp public.asc user@revsuine.xyz:~/public.asc + user@localhost $ ssh user@revsuine.xyz + user@revsuine.xyz $ gpg --import ~/public.asc + +Or you could copy and paste the ASCII armored public key into an SSH shell, etc. + +> ==***DO NOT PUT YOUR PRIVATE KEY ON THE SERVER!***== +> +> Not only is this a security hole, but this also entirely defeats the point of this setup, which is designed to +> protect against an attacker who gains full disk access to the mail server from reading your emails. If the private +> key is stored on the server, this attacker with full disk access will have the key to decrypt and read your emails. + +Because `$HOME` and `$USER` are included in a Sieve filter's environment, python-gnupg can see the public keys in the +user's personal GPG keyring. + +If the user doesn't have shell access, they need to send their public key to a server admin who can run `gpg --import` +as their user (e.g. with `doas -u`). + +You also need to mark the public key as trusted so that GPG doesn't refuse to encrypt data with the key: + + user@revsuine.xyz $ gpg --edit-key user@revsuine.xyz + +Then enter `trust`, select `5`, enter `y`, then enter `save`: + + gpg> trust + 1 = I don't know or won't say + 2 = I do NOT trust + 3 = I trust marginally + 4 = I trust fully + 5 = I trust ultimately + m = back to the main menu + Your decision? 5 + Do you really want to set this key to ultimate trust? (y/N) y + gpg> save + +Now the user needs to create or amend their Sieve script. A minimal Sieve script could be + +```sieve +require "vnd.dovecot.filter"; + +filter "gpgmymail" "user@revsuine.xyz"; +``` + +Note that `filter` commands need to go *before* `fileinto` commands for them to take effect. + +If your Sieve filter is named something else, replace `gpgmymail` with the name of your script (relative to +`/etc/dovecot/sieve-filters/`). + +Your Sieve filter does not need to implement behaviour such as "don't encrypt emails from `domain.com`", because this +is exactly what Sieve scripting is for. If you want to apply conditions to encrypting mail, do it with Sieve, e.g. + +```sieve +require "vnd.dovecot.filter"; + +if not address :is :domain "from" ["revsuine.xyz", "gmail.com"] { + filter "gpgmymail" "user@revsuine.xyz"; +} +``` + +# Assessment + +Given a trusted server admin to implement this, and not spy on emails prior to them passing through the Sieve filter, +this solution protects against an attacker who can read the full disk of the server, as stated previously. Potential +threats this defends against include seizure of the server by law enforcement who bypass full disk encryption, or a VPS +host who reads the FDE key from RAM and reads the disk contents; essentially, +any instance of a third party who gains full disk access. + +This does not do much to protect against a server admin who is intent upon reading their users' emails, because the +email is unencrypted the whole time it moves through the Postfix queue. At the end of the day, there is really nothing +at all that can be done to stop a server admin from reading something that arrives at the server unencrypted, such as +an unencrypted email. + +This solution is an improvement over services such as Protonmail or Tuta, because unlike with Protonmail, users of a +mail server with this Sieve filter do not have to have their private keys stored on the server. Protonmail, assuming an +entirely non-technical user, manages PGP keys for the user, and therefore generates and stores them server-side. +However, with our solution, emails become encrypted on the server, but only get decrypted on the user's local machine. +Their public key is stored on the server, but not their private key. Also, unlike Protonmail and Tuta, this solution +works out-of-the-box with IMAP or POP3, not requiring a bridge like Protonmail does. + +As mentioned at the start, outgoing mail is still stored unencrypted on the server, unless the user has encrypted it +themselves (e.g. with PGP). + +This solution will also make it impossible to search for message contents, as they are all encrypted. If you also +encrypt subject lines, you can essentially only search for emails by sender or date. + +This solution shouldn't break spam filters if they are [integrated with your MTA](https://cwiki.apache.org/confluence/display/SPAMASSASSIN/IntegratedInMta), +but if your spam filter happens after Sieve filtering for some reason (likely only if your spam filter is client-side), +it obviously won't work because the message contents are encrypted and unreadable to a spam filter. Modifying the email +in this way also renders DKIM signing invalid, but DKIM validation should be integrated with your MTA, in which case +you'll still have an email header indicating DKIM status prior to encryption. + +## Compatibility + +[OpenKeychain](https://www.openkeychain.org/) fails to decrypt emails encrypted this way. As far as I can tell, the +only way these emails can be read on Android is by using Termux to decrypt emails with GnuPG, i.e. not with any +conventional Android IMAP client. + +[Desktop Thunderbird](https://www.thunderbird.net/) seems to have an issue rendering quoted-printable or base64-encoded +emails encrypted this way, however I've had no problem with [GNOME Evolution](https://wiki.gnome.org/Apps/Evolution/). +I haven't tested with other desktop email clients. + +Because all gpgmymail does is, essentially, encrypt emails with a Python wrapper for GnuPG, any desktop email client +that uses GPG to decrypt PGP-encrypted emails should be able to read gpgmymail-encrypted emails. When you run +`gpg --decrypt` on a gpgmymail'ed email, you will see the email headers twice, and then the email as it was prior to +being gpgmymail'ed. You could easily not have a client render your email, and just read the raw decrypted email with +`gpg --decrypt`. + +# Further reading + +In order: + +1. https://www.grepular.com/Automatically_Encrypting_all_Incoming_Email +2. https://perot.me/encrypt-specific-incoming-emails-using-dovecot-and-sieve +3. https://blog.jak-linux.org/2019/06/13/encrypted-email-storage/ +4. https://github.com/julian-klode/ansible.jak-linux.org/blob/dovecot/roles/mailserver/files/usr/local/lib/dovecot-sieve-filters/gpgmymail + +[^alpine_searchability]: Tagged [#alpine linux](/tags/alpine-linux/) for findability when searching. +