content/blog/pgp_encrypting_all_incoming_emails.md: new article

This commit is contained in:
revsuine 2024-12-14 00:04:16 +00:00
parent 1031ebb381
commit 03095f0d7c
Signed by: revsuine
GPG key ID: 3F257B68F5BC9339

View file

@ -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`.
> <mark><strong>WARNING!</strong></mark>
>
> 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 <abbr title="full disk encryption">FDE</abbr> 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 <abbr
title="mail transfer agent">MTA</abbr>](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.