diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 5a1df99e..de900e61 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/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 \ diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 5df135cf..45ae6010 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/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 < /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 < /etc/dovecot/lua/app-passdb.lua +cat < /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 "$@" diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 1076c31c..9818e5b0 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -45,36 +45,25 @@ recipient_delimiter = + auth_master_user_separator = * mail_shared_explicit_inbox = yes mail_prefetch_count = 30 +passdb { + driver = lua + 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 - 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 + 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 { - args = /etc/dovecot/sql/dovecot-dict-sql-passdb.conf - driver = sql - result_success = return-ok - result_failure = continue - result_internalfail = continue -} -passdb { - driver = passwd-file - args = /etc/dovecot/dovecot-master.passwd - skip = authenticated + 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 {