Having an Additional Password Just for Email

On my server I usually use PAM for services that I only want to provide for internal users because then you only need to manage one password. That doesn't only mean that you have to remember only one password but also that if you change your password, it's changed everywhere. Now there are situations where such a single sign on principle might become a problem. For example if you want to have your emails on your smartphone or check your email in an internet café when travelling the world, but don't want an attacker to be able to have access to all your data if the phone gets stolen or hacked or the internet café computer is infected with a malware.

That's why I have set up my server to allow users to set a second password just for using email in environments that might not be safe. The system password will continue to work as usual. For my mail server I use Postfix and Dovecot. Postfix is only used as an MTA while Dovecot does all the heavy lifting: authenticating remote users for Postfix via SASL, delivering mail to the actual inbox while applying Sieve filters to them and of course providing the emails to end users via IMAP. Because Dovecot does the authentication for Postfix too, I only had to care about Dovecot in order to have the authentication working for IMAP and SMTP.

First I tried using a checkpassword script but that proved to be pretty complicated because although there is an excellent blog post describing how to do authentication via a checkpassword script. If you want to adapt to custom interfaces, this could be the way to go unless you want to go all the way to provide a custom key value interface to Dovecot, but for my task it was a bit too sophisticated. I also looked at doing the additional authentication in PAM using the libpam-pwdfile module, but as I have never worked with PAM before that also posed too big a challenge.

What I did instead was using the passwd-file module from Dovecot which, lo and behold, allows to authenticate users against a file similar to /etc/passwd. So I created a mailpasswd file and made it only readable for dovecot:

sudo touch /etc/mailpasswd
sudo chown mail:dovecot /etc/mailpasswd
sudo chmod 640 /etc/mailpasswd

Then adjusted Dovecots config to read that file. On Debian that's as simple as changing the passdb[args] parameter in /etc/dovecot/conf.d/auth-passwdfile.conf.ext to

args = scheme=SHA512-CRYPT username_format=%u /etc/mailpasswd

commenting out the userdb section in that file (all information that would need to be looked up from a userdb Dovecot can already retrieve via the usual means for system users) and telling Dovecot to use that config file by uncommenting the corresponding line at the bottom of /etc/dovecot/conf.d/10-auth.conf. Now tell Dovecot to reload its config files by running sudo service dovecot reload and there you go, dovecot is reading from that file.

Problem is: that file is still empty. So I needed a possibility for my users to set their mail password. Unfortunately passwd is kind of single-minded and will only set the password on /etc/passwd or other sources configured via nsswitch.conf, but even then it's a bit picky and only wants to set password on certain services. So I wrote my own variant /usr/local/bin/mailpasswd:

#!/bin/sh
set -e
/usr/bin/sudo -u mail /usr/local/lib/mailpasswd-helper

And /usr/local/lib/mailpasswd-helper:

#!/bin/sh
set -e
 
PWFILE="/etc/mailpasswd"
 
user="$SUDO_USER"
if [ -z "$user" ]; then
    echo "ERROR: please use the mailpasswd command" >&2
    exit 1
fi
 
echo "Changing mail password for $user."
pw="$(doveadm pw -s SHA512-CRYPT)"
 
if echo | doveadm pw -t "$pw" > /dev/null 2>&1; then
    echo "Empty password provided. Password not changed" >&2
    exit 3
fi
 
tmp="$(grep -v "$user:" "$PWFILE" || true; echo "$user:$pw")"
echo "$tmp" > "$PWFILE"
 
echo "Email password successfully changed"
exit 0

The first script is just a wrapper so users don't have to remember to call the second script with sudo (because you can't make scripts setuid for security reasons). So the only thing left is actually allowing all users to call that script by using sudo visudo -f /etc/sudoers.d/mailpasswd to create a sudoers config with the following contents (if you use an older Debian or another distro it may be that the /etc/sudoers.d/ doesn't exist, then you can just add the lines to the main /etc/sudoers file):

# Allow all users to change their mail password via mailpasswd
ALL ALL=(mail) NOPASSWD:NOSETENV: /usr/local/lib/mailpasswd-helper

That's it. The mailpasswd-helper is run as user mail which can edit the /etc/mailpasswd file. It checks whether it was properly called via sudo and reads the user name of the user who called it via sudo by reading it from the $SUDO_USER variable. This is to prevent users from changing other users passwords. If a user calls the script by other means and manually sets the $SUDO_USER variable he either won't have permissions to change the /etc/mailpasswd file or you gave him permission to execute the script by other means with elevated privileges (e.g. because he may run any command as root) then he could also have called editor /etc/mailpasswd with those privileges.

Then the script prompts the user to enter the new password two times by using the doveadm pw command (yay, Dovecot rules!) and because doveadm pw allows for empty passwords it just checks that case by testing the empty password against the authentication token. If you want to enforce any special password requirements (numbers, special characters etc.) then you probably have to prompt the user yourself, and check those conditions on the input. When the check is successful the script extracts all existing lines except for the one for the user from the mailpasswd file and then replaces it's contents with those lines plus the new line (using sed --in-place would probably have been nicer, especially I don't know if there are limitations for variable sizes when using large mailpasswd files).

At the end you should check if the authentication and user lookup works by executing

sudo doveadm auth $user
sudo doveadm user $user

TODO: allow users to delete their mailpasswd password instead of changing it. Maybe by using the empty password case.