[Dovecot] Feature: Move authentication to LUA and prepare for http based authentication, log last SASL logins to SQL
parent
fc93c5e2a8
commit
6d22ae8d02
|
@ -74,6 +74,7 @@ RUN groupadd -g 5000 vmail \
|
||||||
liburi-perl \
|
liburi-perl \
|
||||||
libwww-perl \
|
libwww-perl \
|
||||||
lua-sql-mysql \
|
lua-sql-mysql \
|
||||||
|
lua-socket \
|
||||||
mariadb-client \
|
mariadb-client \
|
||||||
procps \
|
procps \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
|
|
|
@ -60,14 +60,6 @@ map {
|
||||||
}
|
}
|
||||||
EOF
|
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
|
# Create dict used for sieve pre and postfilters
|
||||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
|
@ -118,12 +110,12 @@ EOF
|
||||||
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
|
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
|
||||||
|
|
||||||
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
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 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 last_login' > /etc/dovecot/mail_plugins_imap
|
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
|
echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl notify replication' > /etc/dovecot/mail_plugins_lmtp
|
||||||
else
|
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 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 last_login' > /etc/dovecot/mail_plugins_imap
|
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
|
echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl fts fts_solr notify replication' > /etc/dovecot/mail_plugins_lmtp
|
||||||
fi
|
fi
|
||||||
chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
|
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'))
|
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
|
EOF
|
||||||
|
|
||||||
cat <<EOF > /etc/dovecot/lua/app-passdb.lua
|
cat <<EOF > /etc/dovecot/lua/passwd-verify.lua
|
||||||
function auth_password_verify(req, pass)
|
function auth_password_verify(req, pass)
|
||||||
|
|
||||||
if req.domain == nil then
|
if req.domain == nil then
|
||||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||||
end
|
end
|
||||||
|
|
||||||
if cur == nil then
|
if cur == nil then
|
||||||
script_init()
|
script_init()
|
||||||
end
|
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'
|
WHERE mailbox = '%s'
|
||||||
AND active = '1'
|
AND active = '1'
|
||||||
AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.user), con:escape(req.domain)))
|
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
|
while row do
|
||||||
if req.password_verify(req, row.password, pass) == 1 then
|
if req.password_verify(req, row.password, pass) == 1 then
|
||||||
cur:close()
|
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
|
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
||||||
end
|
end
|
||||||
row = cur:fetch (row, "a")
|
row = cur:fetch (row, "a")
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
function auth_passdb_lookup(req)
|
function auth_passdb_lookup(req)
|
||||||
|
@ -174,6 +218,9 @@ end
|
||||||
|
|
||||||
function script_init()
|
function script_init()
|
||||||
mysql = require "luasql.mysql"
|
mysql = require "luasql.mysql"
|
||||||
|
http = require "socket.http"
|
||||||
|
http.TIMEOUT = 5
|
||||||
|
ltn12 = require "ltn12"
|
||||||
env = mysql.mysql()
|
env = mysql.mysql()
|
||||||
con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
|
con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
|
||||||
return 0
|
return 0
|
||||||
|
@ -186,9 +233,9 @@ end
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Replace patterns in app-passdb.lua
|
# Replace patterns in app-passdb.lua
|
||||||
sed -i "s/__DBUSER__/${DBUSER}/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/app-passdb.lua
|
sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||||
sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/app-passdb.lua
|
sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||||
|
|
||||||
|
|
||||||
# Migrate old sieve_after file
|
# Migrate old sieve_after file
|
||||||
|
@ -302,8 +349,8 @@ sievec /usr/lib/dovecot/sieve/report-ham.sieve
|
||||||
|
|
||||||
# Fix permissions
|
# Fix permissions
|
||||||
chown root:root /etc/dovecot/sql/*.conf
|
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
|
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/app-passdb.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/vmail/sieve
|
||||||
chown -R vmail:vmail /var/volatile
|
chown -R vmail:vmail /var/volatile
|
||||||
chown -R vmail:vmail /var/vmail_index
|
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
|
# 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
|
# 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 "$@"
|
exec "$@"
|
||||||
|
|
|
@ -45,36 +45,25 @@ recipient_delimiter = +
|
||||||
auth_master_user_separator = *
|
auth_master_user_separator = *
|
||||||
mail_shared_explicit_inbox = yes
|
mail_shared_explicit_inbox = yes
|
||||||
mail_prefetch_count = 30
|
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
|
# try a master passwd
|
||||||
passdb {
|
passdb {
|
||||||
driver = passwd-file
|
driver = passwd-file
|
||||||
args = /etc/dovecot/dovecot-master.passwd
|
args = /etc/dovecot/dovecot-master.passwd
|
||||||
master = yes
|
master = yes
|
||||||
pass = yes
|
skip = authenticated
|
||||||
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
|
# 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
|
# a return of the following passdb is mandatory
|
||||||
passdb {
|
passdb {
|
||||||
args = /etc/dovecot/sql/dovecot-dict-sql-passdb.conf
|
driver = lua
|
||||||
driver = sql
|
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
|
||||||
result_success = return-ok
|
|
||||||
result_failure = continue
|
|
||||||
result_internalfail = continue
|
|
||||||
}
|
|
||||||
passdb {
|
|
||||||
driver = passwd-file
|
|
||||||
args = /etc/dovecot/dovecot-master.passwd
|
|
||||||
skip = authenticated
|
|
||||||
}
|
}
|
||||||
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
|
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
|
||||||
service doveadm {
|
service doveadm {
|
||||||
|
|
Loading…
Reference in New Issue