Browse Source

[Dovecot] Feature: Move authentication to LUA and prepare for http based authentication, log last SASL logins to SQL

andryyy 4 years ago
parent
commit
6d22ae8d02

+ 1 - 0
data/Dockerfiles/dovecot/Dockerfile

@@ -74,6 +74,7 @@ RUN groupadd -g 5000 vmail \
   liburi-perl \
   libwww-perl \
   lua-sql-mysql \
+  lua-socket \
   mariadb-client \
   procps \
   python3-pip \

+ 68 - 21
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -60,14 +60,6 @@ map {
 }
 EOF
 
-# Write last logins to Redis
-if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
-  cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
-  echo -n "redis:host=${REDIS_SLAVEOF_IP}:port=${REDIS_SLAVEOF_PORT}" > /etc/dovecot/last_login
-else
-  echo -n "redis:host=${IPV4_NETWORK}.249:port=6379" > /etc/dovecot/last_login
-fi
-
 # Create dict used for sieve pre and postfilters
 cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
 # Autogenerated by mailcow
@@ -118,12 +110,12 @@ EOF
 echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
 
 if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify replication last_login' > /etc/dovecot/mail_plugins
-echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify replication mail_log last_login' > /etc/dovecot/mail_plugins_imap
+echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify replication' > /etc/dovecot/mail_plugins
+echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify replication mail_log' > /etc/dovecot/mail_plugins_imap
 echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl notify replication' > /etc/dovecot/mail_plugins_lmtp
 else
-echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify fts fts_solr replication last_login' > /etc/dovecot/mail_plugins
-echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log fts fts_solr replication last_login' > /etc/dovecot/mail_plugins_imap
+echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify fts fts_solr replication' > /etc/dovecot/mail_plugins
+echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log fts fts_solr replication' > /etc/dovecot/mail_plugins_imap
 echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl fts fts_solr notify replication' > /etc/dovecot/mail_plugins_lmtp
 fi
 chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
@@ -145,15 +137,41 @@ default_pass_scheme = ${MAILCOW_PASS_SCHEME}
 password_query = SELECT password FROM mailbox WHERE active = '1' AND username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) != '1' AND (JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')) = '1' OR ('%s' != 'imap' AND '%s' != 'pop3'))
 EOF
 
-cat <<EOF > /etc/dovecot/lua/app-passdb.lua
+cat <<EOF > /etc/dovecot/lua/passwd-verify.lua
 function auth_password_verify(req, pass)
+
   if req.domain == nil then
     return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
   end
+
   if cur == nil then
     script_init()
   end
-  local cur,errorString = con:execute(string.format([[SELECT mailbox, password FROM app_passwd
+
+  if req.user == nil then
+    req.user = ''
+  end
+
+  respbody = {}
+
+  -- check against mailbox passwds
+  local cur,errorString = con:execute(string.format([[SELECT password FROM mailbox
+    WHERE username = '%s'
+      AND active = '1'
+      AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.user), con:escape(req.domain)))
+  local row = cur:fetch ({}, "a")
+  while row do
+    if req.password_verify(req, row.password, pass) == 1 then
+      cur:close()
+      con:execute(string.format([[INSERT INTO sasl_logs (success, service, app_password, username, real_rip)
+        VALUES (1, "%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
+      return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
+    end
+    row = cur:fetch (row, "a")
+  end
+
+  -- check against app passwds
+  local cur,errorString = con:execute(string.format([[SELECT id, password FROM app_passwd
     WHERE mailbox = '%s'
       AND active = '1'
       AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.user), con:escape(req.domain)))
@@ -161,11 +179,37 @@ function auth_password_verify(req, pass)
   while row do
     if req.password_verify(req, row.password, pass) == 1 then
       cur:close()
+      con:execute(string.format([[INSERT INTO sasl_logs (success, service, app_password, username, real_rip)
+        VALUES (1, "%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
       return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
     end
     row = cur:fetch (row, "a")
   end
-  return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
+
+  con:execute(string.format([[INSERT INTO sasl_logs (success, service, app_password, username, real_rip)
+    VALUES (0, "%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
+
+  return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
+
+  -- PoC
+  -- local reqbody = string.format([[{
+  --   "success":0,
+  --   "service":"%s",
+  --   "app_password":false,
+  --   "username":"%s",
+  --   "real_rip":"%s"
+  -- }]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip))
+  -- http.request {
+  --   method = "POST",
+  --   url = "http://nginx:8081/sasl_logs.php",
+  --   source = ltn12.source.string(reqbody),
+  --   headers = {
+  --     ["content-type"] = "application/json",
+  --     ["content-length"] = tostring(#reqbody)
+  --   },
+  --   sink = ltn12.sink.table(respbody)
+  -- }
+
 end
 
 function auth_passdb_lookup(req)
@@ -174,6 +218,9 @@ end
 
 function script_init()
   mysql = require "luasql.mysql"
+  http = require "socket.http"
+  http.TIMEOUT = 5
+  ltn12 = require "ltn12"
   env  = mysql.mysql()
   con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
   return 0
@@ -186,9 +233,9 @@ end
 EOF
 
 # Replace patterns in app-passdb.lua
-sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/app-passdb.lua
-sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/app-passdb.lua
-sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/app-passdb.lua
+sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
+sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
+sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/passwd-verify.lua
 
 
 # Migrate old sieve_after file
@@ -302,8 +349,8 @@ sievec /usr/lib/dovecot/sieve/report-ham.sieve
 
 # Fix permissions
 chown root:root /etc/dovecot/sql/*.conf
-chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/app-passdb.lua
-chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/lua/app-passdb.lua
+chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua
+chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/lua/passwd-verify.lua
 chown -R vmail:vmail /var/vmail/sieve
 chown -R vmail:vmail /var/volatile
 chown -R vmail:vmail /var/vmail_index
@@ -373,6 +420,6 @@ done
 
 # For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth
 # May be related to something inside Docker, I seriously don't know
-touch /etc/dovecot/lua/app-passdb.lua
+touch /etc/dovecot/lua/passwd-verify.lua
 
 exec "$@"

+ 9 - 20
data/conf/dovecot/dovecot.conf

@@ -45,37 +45,26 @@ recipient_delimiter = +
 auth_master_user_separator = *
 mail_shared_explicit_inbox = yes
 mail_prefetch_count = 30
-# try a master passwd
-passdb {
-  driver = passwd-file
-  args = /etc/dovecot/dovecot-master.passwd
-  master = yes
-  pass = yes
-  result_failure = continue
-  result_internalfail = continue
-}
-# try an app passwd
 passdb {
   driver = lua
-  args = file=/etc/dovecot/lua/app-passdb.lua blocking=yes
-  pass = yes
-  result_failure = continue
-  result_internalfail = continue
-}
-# check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail
-# a return of the following passdb is mandatory
-passdb {
-  args = /etc/dovecot/sql/dovecot-dict-sql-passdb.conf
-  driver = sql
+  args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
   result_success = return-ok
   result_failure = continue
   result_internalfail = continue
 }
+# try a master passwd
 passdb {
   driver = passwd-file
   args = /etc/dovecot/dovecot-master.passwd
+  master = yes
   skip = authenticated
 }
+# check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail
+# a return of the following passdb is mandatory
+passdb {
+  driver = lua
+  args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
+}
 # Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
 service doveadm {
   inet_listener {