Strong crypt scheme with Dovecot, Postfixadmin and Roundcube
Hi everyone, happy new year :)
While Dovecot support a lot of different password schemes, making both Postfixadmin and Roundcube playing nice by using something else than the old MD5 scheme need a little work.
In this post I'll explain how to configure them to use a Blowfish scheme (BLF-CRYPT in the Dovecot terminology), but you can easily adapt the steps to use something else. You should already have Dovecot, Postfixadmin, and Roundcube (with the password plugin) up and running.
First of all
Let me state the obvious: backup your configuration files, database, etc. before any changes.
Checking BLF-CRYPT support
We need to check if the system support BLF-CRYPT. As stated in the documentation: BLF-CRYPT, SHA256-CRYPT, and SHA512-CRYPT are optional and their availability depends on the system's libc.
If you get something like:
Then BLF-CRYPT is not available on your system. You can run doveadm pw -l to see the list of supported scheme, and pick the best one available. If you don't know how to choose, the Dovecot documentation is a good start.
Computing the ideal number of round
The shorter the number of rounds, the faster a password hash is computed; the faster a password hash is computed, the faster is an attack trying many passwords (i.e. Brute-force attack). On the other hand, a high number rounds means it is harder to brute-force but also harder to compute for legitimate use (in this case users connecting to their emails). For a good read on the question check Thomas Pornin's answer on stackexchange.
In this example we'll pick the least number of rounds taking more than
250ms to compute a password hash (try different values for -r
until happy):
Be sure to run theses commands on the same machine that will run the Dovecot server, the whole point is to find a minimum computation time when user actually login on the "production" server.
Postfixadmin configuration
Here is the relevant configuration part:
config.local.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php // ... // Encrypt // In what way do you want the passwords to be crypted? // md5crypt = internal postfix admin md5 // md5 = md5 sum of the password // system = whatever you have set as your PHP system default // cleartext = clear text passwords (ouch!) // mysql_encrypt = useful for PAM integration // authlib = support for courier-authlib style passwords // dovecot:CRYPT-METHOD = use dovecotpw -s 'CRYPT-METHOD'. Example: dovecot:CRAM-MD5 // (WARNING: don't use dovecot:* methods that include the username in the // hash - you won't be able to login to PostfixAdmin in this case) $CONF['encrypt'] = 'dovecot:BLF-CRYPT'; // If you use the dovecot encryption method: where is the dovecotpw binary located? // for dovecot 1.x // $CONF['dovecotpw'] = "/usr/sbin/dovecotpw"; // for dovecot 2.x (dovecot 2.0.0 - 2.0.7 is not supported!) // $CONF['dovecotpw'] = "/usr/sbin/doveadm pw"; $CONF['dovecotpw'] = "/usr/local/bin/doveadm pw -r 12"; |
Old password scheme backward compatibility
Now in order to allow admin account to still login using the "old" password scheme (which is still the one used in your database at this point!), you need to figure out which one it is. First look at one of the password from the postfix database:
Then check the
Key derivation Functions table
to find the corresponding scheme. For example, if your password look like
$1$etNnh7FA$OlM7eljE/B7F1J4XYNnk81
then it is using the MD5
scheme.
Once you know the old passwords scheme you need to find how Dovecot prefix it. The prefix should be easy to guess knowing the scheme name (for MD5 it is MD5-CRYPT), you can check by running doveadm pw -s CRYPT-NAME-TO-TEST -p secret and see if the result looks like the corresponding Wikipedia's example. Again, the full list of scheme supported by Dovecot doveadm pw -l may be helpful.
Alright! Now we can prefix all the old password so they can still be used to log in:
Postfixadmin checkup
Here is a (non-exhaustive) list of steps to check that the new configuration work as expected:
-
Login using a Postfixadmin admin account, it should work and still use the "old" password scheme.
-
Once logged in, change your password. Once updated, check that your new password has been hashed using the new password scheme:
-
Logout and login again, it should work now using the new password scheme.
-
Create a new mailbox and check that its password has been hashed using the new password scheme:
Roundcube password plugin configuration
Here is the relevant configuration part:
plugins/password/config.inc.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <?php // Password Plugin options // ----------------------- // ... // force saving when the new password is the same as the current password, so // that it uses the latest configured password scheme. $rcmail_config['password_force_save'] = true; // ... // SQL Driver options // ------------------ // ... // The SQL query used to change the password. // The query can contain the following macros that will be expanded as follows: // %p is replaced with the plaintext new password // %c is replaced with the crypt version of the new password, MD5 if available // otherwise DES. // %D is replaced with the dovecotpw-crypted version of the new password // %o is replaced with the password before the change // %n is replaced with the hashed version of the new password // %q is replaced with the hashed password before the change // %h is replaced with the imap host (from the session info) // %u is replaced with the username (from the session info) // %l is replaced with the local part of the username // (in case the username is an email address) // %d is replaced with the domain part of the username // (in case the username is an email address) // Escaping of macros is handled by this module. // Default: "SELECT update_passwd(%c, %u)" $rcmail_config['password_query'] = "UPDATE mailbox SET password = %D, modified = NOW() WHERE username = %u"; // By default domains in variables are using unicode. // Enable this option to use punycoded names $rcmail_config['password_idn_ascii'] = false; // Path for dovecotpw (if not in $PATH) $rcmail_config['password_dovecotpw'] = '/usr/local/bin/doveadm pw -r 12'; // Dovecot method (dovecotpw -s 'method') $rcmail_config['password_dovecotpw_method'] = 'BLF-CRYPT'; // Enables use of password with crypt method prefix in %D, e.g. {MD5}$1$LUiMYWqx$fEkg/ggr/L6Mb2X7be4i1/ $rcmail_config['password_dovecotpw_with_method'] = true; |
-
YMMV regarding the
password_query
but the important part is to use%D
as it is the dovecotpw-crypted version. -
Don't forget to adapt the
-r
option value to the number of rounds found in the last step. Also check your doveadm executable path (/usr/local/bin/doveadm in this example). -
We use crypt method prefix in
%D
to be consistent with Postfixadmin.
Roundcube checkup
Here is a (non-exhaustive) list of steps to check that the new configuration work as expected:
-
Login from Roundcube with your usual access. It should work and still use the "old" password scheme.
-
Once logged in, change your password. Once updated, check that your new password has been hashed using the new password scheme:
-
Logout and login again, it should work now using the new password scheme.
Automatic password scheme migration
Dovecot has a facility to run post-login script which can be used to migrate mailbox password to our freshly configured scheme.
Dovecot config
First, the password_query
of
dovecot-sql.conf.ext must be adapted to setup
the plaintext password in the Dovecot
environment. This is what we need to add to the SELECT
query:
For example, this is what my password_query
looked like before:
And now:
Then, make sure you have configured "userdb prefetch" correctly. In your Dovecot configuration, look for:
Finally, in order to have our script run on pop3 and imap login, we need to
adapt both service imap
and service pop3
. Here is
the diff of my configuration but of course YMMV:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | --- usr/local/etc/dovecot/conf.d/10-master.conf 2014-10-27 22:09:44.000000000 +0100 +++ /usr/local/etc/dovecot/conf.d/10-master.conf 2017-05-24 19:40:19.000353000 +0200 @@ -67,6 +67,8 @@ } service imap { + # see https://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes + executable = imap imap-postlogin # Most of the memory goes to mmap()ing files. You may need to increase this # limit if you have huge mailboxes. #vsz_limit = $default_vsz_limit @@ -76,6 +78,8 @@ } service pop3 { + # see https://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes + executable = pop3 pop3-postlogin # Max. number of POP3 processes (connections) #process_limit = 1024 } @@ -131,3 +135,19 @@ #group = } } + +# see https://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes +service imap-postlogin { + executable = script-login /usr/local/etc/dovecot/postlogin-updatepw.php + user = $default_internal_user + unix_listener imap-postlogin { + } +} + +# see https://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes +service pop3-postlogin { + executable = script-login /usr/local/etc/dovecot/postlogin-updatepw.php + user = $default_internal_user + unix_listener pop3-postlogin { + } +} |
Remember the configured script-login path on line 28 and 36, we'll use it right after.
password scheme update script
Now we'll write the post-login script that will update the mailbox password to the new scheme. I've chosen to write my own for several reasons I will outline later.
Let's start by creating the file directly with the right permissions. It is important because the script will contain the credentials to connect to our mailbox database.
I've set the group to dovecot
here because it match my
configured $default_internal_user
but adapt if needed. If the
permissions mismatch, you'll get some "Permission denied" messages in your
maillog looking like this:
The real issue though is that the login will fail, so take extra care.
Here is the full script. It has been written in PHP because since you have Postfixadmin and Roundcube installed you probably have PHP too :)
The pcntl shared extension for PHP
is required to run this script. It also need PDO (and your database PDO
driver) but Roundcube uses PDO, so it should
be safe to assume that it is installed (and working). If
Dovecot runs out of memory while calling the
postlogin script (fails to load libraries) try tweaking
default_vsz_limit
in the Dovecot
configuration.
/usr/local/etc/dovecot/postlogin-updatepw.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | #!/usr/bin/env php <?php // PDO dsn $pdo_dsn = 'pgsql:host=localhost;dbname=postfix;user=postfixadmin;password=secret'; // $rcmail_config['password_dovecotpw'] $dovecotpw = '/usr/local/bin/doveadm pw -r 12'; // $rcmail_config['password_dovecotpw_method'] $dovecotpw_method = 'BLF-CRYPT'; // where we log $syslog_facility = LOG_MAIL; // grab what we care about from the env. $username = getenv("USER"); $plainpass = getenv("PLAIN_PASS"); // init syslog. $progname = basename(__FILE__); openlog($progname, LOG_PID, $syslog_facility); // connect to the database. try { $dbh = new PDO($pdo_dsn); $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { syslog(LOG_CRIT, "database connection failed: {$e->getMessage()}"); goto out; } try { // retrieve the user's current password. $sth = $dbh->prepare('SELECT password FROM mailbox WHERE username = ?'); $sth->execute([$username]); $oldpasswd = $sth->fetchColumn(); if (!$oldpasswd) { syslog(LOG_WARNING, "unable to find the mailbox for $username"); goto out; } // bail out if the new password is empty. if (strlen($plainpass) === 0) { syslog(LOG_ERR, "empty password for $username!"); goto out; } // if we find the dovecot password method in the current password, it does // not need to be updated. if (strpos($oldpasswd, $dovecotpw_method) !== false) { syslog(LOG_INFO, "$username already use $dovecotpw_method, skipping."); goto out; } // generate a new password using `doveadm pw' without passing plainpass // on the cmdline for security concerns. $process = proc_open("$dovecotpw -s $dovecotpw_method", [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ], $pipes); if (!is_resource($process)) { syslog(LOG_CRIT, "proc_open() failed."); goto out; } // $pipes now looks like this: // 0 => writeable handle connected to child stdin // 1 => readable handle connected to child stdout // 2 => readable handle connected to child stderr fclose($pipes[2]); // immediately close stderr as we don't need it. fwrite($pipes[0], "$plainpass\n"); fwrite($pipes[0], "$plainpass\n"); fclose($pipes[0]); $newpasswd = trim(stream_get_contents($pipes[1])); fclose($pipes[1]); $retval = proc_close($process); if ($retval !== 0) { syslog(LOG_ERR, "$dovecotpw exited with status $retval, expected 0."); goto out; } // sanity check to ensure that the new password has been computed with the // requested method. if (strpos($newpasswd, $dovecotpw_method) === false) { syslog(LOG_ERR, "unexpected $dovecotpw output."); goto out; } // update the password in the database with the newly computed one. $sth = $dbh->prepare('UPDATE mailbox SET password = :newpasswd WHERE username = :username AND password = :oldpasswd'); $success = $sth->execute([ ':newpasswd' => $newpasswd, ':username' => $username, ':oldpasswd' => $oldpasswd, ]); // "close" the database connection, // see https://secure.php.net/manual/en/pdo.connections.php $sth = null; $dbh = null; } catch (PDOException $e) { syslog(LOG_CRIT, "database query failed: {$e->getMessage()}"); goto out; } if ($success) { syslog(LOG_INFO, "$username password successfully migrated to $dovecotpw_method."); } else { syslog(LOG_CRIT, "$username password migration to $dovecotpw_method failed."); } // FALLTHROUGH out: // cleanup. // close syslog. closelog(); /* * We have to continue execution from what we get on the command line argument, * i.e. $argv. * * see https://wiki.dovecot.org/PostLoginScripting */ // $argv[0] is our script (i.e. __FILE__), $argv[1] the next program to // execute, and $argv[2..] the next program's arguments. $next_exe = $argv[1]; $next_argv = array_slice($argv, 2); pcntl_exec($next_exe, $next_argv); |
Lines 3 through 10 are meant to be adapted, most of them from the Roundcube password plugin configuration. Also, depending on your database schema, the SQL queries on lines 31 and 81 may need to be changed too.
This script has several advantages over the example(s) you can find from the Dovecot wiki:
-
It uses exclusively syslog's
LOG_MAIL
for messages, so its output is handily around the Dovecot logs. You might care or not. - It is not limited to MySQL as I am using PostgreSQL. Again, not a big deal.
- It only compute the new password scheme when the password actually need to be updated. This is relevant as we specifically picked a number of round that would be costly.
-
It uses doveadm(1) in a way that doesn't leak
the password on the command line. This is critical, when you call
doveadm pw -p secret
secret
(the plaintext password) is part of the cmdline that can be seen from other processes (e.g. when running ps a).
Testing the password migration
Reload Dovecot's configuration:
Now when a user with a mailbox using the old password scheme login (through imap for example), you should see something like this in your maillog:
Then if you check in the database, its password scheme should have been
updated to BLF-CRYPT
. The next logins from this user should
generate messages like this:
The End.
And voilà! Now only the Postfixadmin account passwords needs to be updated, but it should be not too much trouble to force them to update their password :)
Many thanks to Aki Tuomi for pointing out the Dovecot wiki documentation about the password scheme migration. It prompted me to test it and complete this article.
-r
option value to the number of rounds found in the last step. Also check your doveadm executable path (/usr/local/bin/doveadm in this example).