diff --git a/conf/dovecot-checkpassword.py b/conf/dovecot-checkpassword.py new file mode 100644 index 00000000..91979ec3 --- /dev/null +++ b/conf/dovecot-checkpassword.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +# +# This script implement's Dovecot's checkpassword authentication mechanism: +# http://wiki2.dovecot.org/AuthDatabase/CheckPassword?action=show&redirect=PasswordDatabase%2FCheckPassword +# +# This allows us to perform our own password validation, such as for two-factor authentication, +# which Dovecot does not have any native support for. +# +# We will issue an HTTP request to our management server to perform authentication. + +import sys, os, urllib.request, base64, json, traceback + +try: + # Read fd 3 which provides the username and password separated + # by NULLs and two other undocumented/empty fields. + creds = b'' + while True: + b = os.read(3, 1024) + if len(b) == 0: break + creds += b + email, pw, dummy, dummy = creds.split(b'\x00') + + # Call the management server's "/me" method with the + # provided credentials + req = urllib.request.Request('http://127.0.0.1:10222/me') + req.add_header(b'Authorization', b'Basic ' + base64.b64encode(email + b':' + pw)) + response = urllib.request.urlopen(req) + + # The response is always success and always a JSON object + # indicating the authentication result. + resp = response.read().decode('utf8') + resp = json.loads(resp) + if not isinstance(resp, dict): raise ValueError("Response is not a JSON object.") + +except: + # Handle all exceptions. Print what happens (ends up in syslog, thanks + # to dovecot) and return an exit status that indicates temporary failure, + # which is passed on to the authenticating client. + traceback.print_exc() + print(json.dumps(dict(os.environ), indent=2), file=sys.stderr) + sys.exit(111) + +if resp.get('status') != 'authorized': + # Indicates login failure. + # (sys.exit should not be inside the try block.) + sys.exit(1) + +# Signal ok by executing the indicated process, per the Dovecot +# protocol. (Note that the second parameter is the 0th argument +# to the called process, which is required and is typically the +# file itself.) +os.execl(sys.argv[1], sys.argv[1]) + diff --git a/setup/mail-users.sh b/setup/mail-users.sh index 387ce698..408389a6 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -26,31 +26,55 @@ fi # ### User Authentication -# Have Dovecot query our database, and not system users, for authentication. -sed -i "s/#*\(\!include auth-system.conf.ext\)/#\1/" /etc/dovecot/conf.d/10-auth.conf -sed -i "s/#\(\!include auth-sql.conf.ext\)/\1/" /etc/dovecot/conf.d/10-auth.conf +# Disable all of the built-in authentication mechanisms. (We formerly uncommented +# a line to include auth-sql.conf.ext but we no longer use that.) +sed -i "s/#*\(\!include auth-.*.conf.ext\)/#\1/" /etc/dovecot/conf.d/10-auth.conf -# Specify how the database is to be queried for user authentication (passdb) -# and where user mailboxes are stored (userdb). -cat > /etc/dovecot/conf.d/auth-sql.conf.ext << EOF; +# Legacy: Delete our old sql conf files. +rm -f /etc/dovecot/conf.d/auth-sql.conf.ext /etc/dovecot/dovecot-sql.conf.ext + +# Specify how Dovecot should perform user authentication (passdb) and how it knows +# where user mailboxes are stored (userdb). +# +# For passwords, we would normally have Dovecot query our mail user database +# directly. The way to do that is commented out below. Instead, in order to +# provide our own authentication framework so we can handle two-factor auth, +# we will use a custom system that hooks into the Mail-in-a-Box management daemon. +# +# The user part of this is standard. The mailbox path and Unix system user are the +# same for all mail users, modulo string substitution for the mailbox path that +# Dovecot handles. +cat > /etc/dovecot/conf.d/10-auth-mailinabox.conf << EOF; passdb { - driver = sql - args = /etc/dovecot/dovecot-sql.conf.ext + driver = checkpassword + args = /usr/local/bin/dovecot-checkpassword } userdb { driver = static args = uid=mail gid=mail home=$STORAGE_ROOT/mail/mailboxes/%d/%n } EOF +chmod 0600 /etc/dovecot/conf.d/10-auth-mailinabox.conf -# Configure the SQL to query for a user's password. -cat > /etc/dovecot/dovecot-sql.conf.ext << EOF; -driver = sqlite -connect = $db_path -default_pass_scheme = SHA512-CRYPT -password_query = SELECT email as user, password FROM users WHERE email='%u'; -EOF -chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions +# Copy dovecot-checkpassword into place. +cp conf/dovecot-checkpassword.py /usr/local/bin/dovecot-checkpassword +chown dovecot.dovecot /usr/local/bin/dovecot-checkpassword +chmod 700 /usr/local/bin/dovecot-checkpassword + +# If we were having Dovecot query our database directly, which we did +# originally, `/etc/dovecot/conf.d/10-auth-mailinabox.conf` would say: +# +# passdb { +# driver = sql +# args = /etc/dovecot/dovecot-sql.conf.ext +# } +# +# and then `/etc/dovecot/dovecot-sql.conf.ext` (chmod 0600) would contain: +# +# driver = sqlite +# connect = $db_path +# default_pass_scheme = SHA512-CRYPT +# password_query = SELECT email as user, password FROM users WHERE email='%u'; # Have Dovecot provide an authorization service that Postfix can access & use. cat > /etc/dovecot/conf.d/99-local-auth.conf << EOF;