content/blog/pgp_encrypting_all_incoming_emails.md: new article
This commit is contained in:
parent
1031ebb381
commit
03095f0d7c
1 changed files with 257 additions and 0 deletions
257
content/blog/pgp_encrypting_all_incoming_emails.md
Normal file
257
content/blog/pgp_encrypting_all_incoming_emails.md
Normal 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.
|
||||
|
Loading…
Reference in a new issue