public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[Dovecot] Feature: Move authentication to LUA and prepare for http based authentication, log last SASL logins to SQL
6d22ae8d
data/Dockerfiles/dovecot/Dockerfile | 1 +
data/Dockerfiles/dovecot/docker-entrypoint.sh | 89 ++++++++++++++++++++-------
data/conf/dovecot/dovecot.conf | 29 +++------
3 files changed, 78 insertions(+), 41 deletions(-)
Diff
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 <<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 "$@"
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,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 {