diff --git a/Changes.md b/Changes.md index 5c17c47ba..123c17d2d 100644 --- a/Changes.md +++ b/Changes.md @@ -1,6 +1,7 @@ ### Unreleased +- feat(auth_vpopmaild): when outbound, assure the envelope domain matches AUTH domain #3265 - docs(outbound): remove example setting outbound_ip #3253 - dep(plugin-es): bump version to 8.0.2 #3206 - transaction: simplify else condition in add_data #3252 diff --git a/config/auth_flat_file.ini b/config/auth_flat_file.ini index dc6118fb7..65723d361 100644 --- a/config/auth_flat_file.ini +++ b/config/auth_flat_file.ini @@ -1,5 +1,6 @@ [core] methods=CRAM-MD5 +; constrain_sender=true [users] ; matt=test diff --git a/config/auth_vpopmaild.ini b/config/auth_vpopmaild.ini index ff15a0d4c..b6ae7dc5c 100644 --- a/config/auth_vpopmaild.ini +++ b/config/auth_vpopmaild.ini @@ -1,7 +1,9 @@ +[main] host=127.0.0.6 port=89 -;sysadmin=postmaster@example.com:sekret +; sysadmin=postmaster@example.com:sekret +; constrain_sender=true [example.com] host=127.0.0.10 -;sysadmin=postmaster@example.com:sekret +; sysadmin=postmaster@example.com:sekret diff --git a/docs/plugins/auth/auth_vpopmaild.md b/docs/plugins/auth/auth_vpopmaild.md index ccf0d32d2..4ce65b3f9 100644 --- a/docs/plugins/auth/auth_vpopmaild.md +++ b/docs/plugins/auth/auth_vpopmaild.md @@ -1,26 +1,20 @@ -auth/auth\_vpopmaild -=============== +# auth/auth\_vpopmaild -The `auth/vpopmaild` plugin allows you to authenticate against a vpopmaild -daemon. +The `auth/vpopmaild` plugin allows SMTP users to authenticate against a vpopmaild daemon. ## Configuration -Configuration is stored in `config/auth_vpopmaild.ini` and uses INI -style formatting. +The configuration file is stored in `config/auth_vpopmaild.ini`. -There are three configuration settings: +### settings * host: The host/IP that vpopmaild is listening on (default: localhost). * port: The TCP port that vpopmaild is listening on (default: 89). -* sysadmin: A colon separated username:password of a vpopmail user with - SYSADMIN privileges (see vpopmail/bin/vmoduser -S). This is **only** - necessary to support CRAM-MD5 which requires access to the clear text - password. On new installs, it's best not to use CRAM-MD5, as it requires - storing clear text passwords. Legacy clients with MUAs configured - to authenticate with CRAM-MD5 will need this enabled. +* sysadmin: A colon separated username:password of a vpopmail user with SYSADMIN privileges (see vpopmail/bin/vmoduser -S). This is **only** necessary to support CRAM-MD5 which requires access to the clear text password. On new installs, it's best not to use CRAM-MD5, as it requires storing clear text passwords. Legacy clients with MUAs configured to authenticate with CRAM-MD5 will need this enabled. + +* constrain_sender: (default: true). For outbound messages (due to successful AUTH), constrain the envelope sender (MAIL FROM) to the same domain as the authenticated user. This setting, combined with `rate_rcpt_sender` in the [limit](https://github.com/haraka/haraka-plugin-limit) plugin can dramatically reduce the amount of backscatter and spam sent when an email account is compromised. ### Per-domain Configuration @@ -29,10 +23,12 @@ Additionally, domains can each have their own configuration for connecting to vpopmaild. The defaults are the same, so only the differences needs to be declared. Example: - [example.com] - host=192.168.0.1 - port=999 +```ini +[example.com] +host=192.168.0.1 +port=999 - [example2.com] - host=192.168.0.2 - sysadmin=postmaster@example2.com:sekret +[example2.com] +host=192.168.0.2 +sysadmin=postmaster@example2.com:sekret +``` diff --git a/docs/plugins/auth/flat_file.md b/docs/plugins/auth/flat_file.md index 30a74835b..58021a8a7 100644 --- a/docs/plugins/auth/flat_file.md +++ b/docs/plugins/auth/flat_file.md @@ -1,47 +1,40 @@ -auth/flat\_file -============== +# auth/flat\_file -The `auth/flat_file` plugin allows you to create a file containing username -and password combinations, and have relaying users authenticate from that -file. +The `auth/flat_file` plugin allows you to create a file containing username and password combinations, and have relaying users authenticate from that file. -Note that passwords are stored in clear-text, so this may not be a great idea -for large scale systems. However the plugin would be a good start for someone -looking to implement authentication using some other form of auth. +Note that passwords are stored in clear-text, so this may not be a great idea for large scale systems. However the plugin would be a good start for someone looking to implement authentication using some other form of auth. -**Security** - it is recommended to switch to [auth-encfile][url-authencflat] -to protect your user credentials. +**Security** - it is recommended to switch to [auth-encfile][url-authencflat] to protect your user credentials. -**IMPORANT NOTE** - this plugin requires that STARTTLS be used via the tls plugin -before it will advertise AUTH capabilities by the EHLO command. This is to -improve security out-of-the-box. Localhost and any IP in RFC1918 ranges -are automatically exempt from this rule. +**IMPORANT NOTE** - this plugin requires that STARTTLS be used via the tls plugin before it will advertise AUTH capabilities by the EHLO command. Localhost and IPs in RFC1918 ranges +are exempt from this rule. -Configuration -------------- +## Configuration -Configuration is stored in `config/auth_flat_file.ini` and uses the INI -style formatting. +Configuration is stored in `config/auth_flat_file.ini`. -Authentication methods are listed in the `[core]` section under `methods` -parameter. Lists of authentification methods are comma separated. Currently -supported methods are: `CRAM-MD5`, `PLAIN` and `LOGIN`. The `PLAIN` -and `LOGIN` methods are not secure. That is why TLS is required before AUTH is -offered. +* [core]methods -Example: +Authentication methods are listed in the `[core]methods` parameter. Authentification methods are comma separated. Currently supported methods are: `CRAM-MD5`, `PLAIN` and `LOGIN`. The `PLAIN` and `LOGIN` methods are insecure and require TLS to be enabled. + +* [core]constrain_sender: (default: true). For outbound messages (due to successful AUTH), constrain the envelope sender (MAIL FROM) to the same domain as the authenticated user. This setting, combined with `rate_rcpt_sender` in the [limit](https://github.com/haraka/haraka-plugin-limit) plugin can dramatically reduce the amount of backscatter and spam sent when an email account is compromised. - [core] - methods=PLAIN,LOGIN,CRAM-MD5 +Example: +```ini +[core] +methods=PLAIN,LOGIN,CRAM-MD5 +constrain_sender=true +``` Users are stored in the `[users]` section. Example: - [users] - user1=password1 - user@domain.com=password2 - +```ini +[users] +user1=password1 +user@domain.com=password2 +``` [url-authencflat]: https://github.com/AuspeXeu/haraka-plugin-auth-enc-file diff --git a/plugins/auth/auth_base.js b/plugins/auth/auth_base.js index 2d0e40e99..57333beee 100644 --- a/plugins/auth/auth_base.js +++ b/plugins/auth/auth_base.js @@ -5,7 +5,10 @@ // Note: You can disable setting `connection.notes.auth_passwd` by `plugin.blankout_password = true` const crypto = require('crypto'); -const utils = require('haraka-utils'); + +const tlds = require('haraka-tld') +const utils = require('haraka-utils'); + const AUTH_COMMAND = 'AUTH'; const AUTH_METHOD_CRAM_MD5 = 'CRAM-MD5'; const AUTH_METHOD_PLAIN = 'PLAIN'; @@ -15,7 +18,7 @@ const LOGIN_STRING2 = 'UGFzc3dvcmQ6'; //Password: base64 coded exports.hook_capabilities = (next, connection) => { // Don't offer AUTH capabilities unless session is encrypted - if (!connection.tls.enabled) { return next(); } + if (!connection.tls.enabled) return next(); const methods = [ 'PLAIN', 'LOGIN', 'CRAM-MD5' ]; connection.capabilities.push(`AUTH ${methods.join(' ')}`); @@ -47,9 +50,7 @@ exports.hook_unrecognized_command = function (next, connection, params) { exports.check_plain_passwd = function (connection, user, passwd, cb) { function callback (plain_pw) { - if (plain_pw === null ) return cb(false); - if (plain_pw !== passwd) return cb(false); - cb(true); + cb(plain_pw === null ? false : plain_pw === passwd); } if (this.get_plain_passwd.length == 2) { this.get_plain_passwd(user, callback); @@ -71,7 +72,7 @@ exports.check_cram_md5_passwd = function (connection, user, passwd, cb) { if (hmac.digest('hex') === passwd) return cb(true); - return cb(false); + cb(false); } if (this.get_plain_passwd.length == 2) { this.get_plain_passwd(user, callback); @@ -117,7 +118,7 @@ exports.check_user = function (next, connection, credentials, method) { connection.auth_results(`auth=pass (${method.toLowerCase()})`); connection.notes.auth_user = credentials[0]; if (!plugin.blankout_password) connection.notes.auth_passwd = credentials[1]; - return next(OK); + next(OK); }); return; } @@ -125,9 +126,7 @@ exports.check_user = function (next, connection, credentials, method) { if (!connection.notes.auth_fails) connection.notes.auth_fails = 0; connection.notes.auth_fails++; - connection.results.add({name: 'auth'}, { - fail:`${plugin.name}/${method}`, - }); + connection.results.add({name: 'auth'}, { fail:`${plugin.name}/${method}` }); let delay = Math.pow(2, connection.notes.auth_fails - 1); if (plugin.timeout && delay >= plugin.timeout) { @@ -230,7 +229,7 @@ exports.auth_cram_md5 = function (next, connection, params) { return this.check_user(next, connection, credentials, AUTH_METHOD_CRAM_MD5); } - const ticket = `<${this.hexi(Math.floor(Math.random() * 1000000))}. ${this.hexi(Date.now())}@${connection.local.host}>`; + const ticket = `<${this.hexi(Math.floor(Math.random() * 1000000))}.${this.hexi(Date.now())}@${connection.local.host}>`; connection.loginfo(this, `ticket: ${ticket}`); connection.respond(334, utils.base64(ticket), () => { @@ -240,3 +239,20 @@ exports.auth_cram_md5 = function (next, connection, params) { } exports.hexi = number => String(Math.abs(parseInt(number)).toString(16)) + +exports.constrain_sender = function (next, connection, params) { + const au = connection.results.get('auth')?.user + if (!au) return next() + + const ad = /@/.test(au) ? au.split('@').pop() : au + const ed = params[0].host + + if (!ad || !ed) return next() + + const auth_od = tlds.get_organizational_domain(ad) + const envelope_od = tlds.get_organizational_domain(ed) + + if (auth_od === envelope_od) return next() + + next(DENY, `Envelope domain '${envelope_od}' doesn't match AUTH domain '${auth_od}'`) +} diff --git a/plugins/auth/auth_vpopmaild.js b/plugins/auth/auth_vpopmaild.js index e25554c1f..64dfe9c5e 100644 --- a/plugins/auth/auth_vpopmaild.js +++ b/plugins/auth/auth_vpopmaild.js @@ -4,25 +4,36 @@ const net = require('net'); exports.register = function () { this.inherits('auth/auth_base'); - this.load_vpop_ini(); + this.blankout_password=true + + this.load_vpopmaild_ini(); + + if (this.cfg.main.constrain_sender) { + this.register_hook('mail', 'constrain_sender') + } } -exports.load_vpop_ini = function () { - this.cfg = this.config.get('auth_vpopmaild.ini', () => { - this.load_vpop_ini(); +exports.load_vpopmaild_ini = function () { + this.cfg = this.config.get('auth_vpopmaild.ini', { + booleans: [ + '+main.constrain_sender', + ] + }, + () => { + this.load_vpopmaild_ini(); }); } exports.hook_capabilities = function (next, connection) { - if (!connection.tls.enabled) { return next(); } + if (!connection.tls.enabled) return next(); const methods = [ 'PLAIN', 'LOGIN' ]; - if (this.cfg.main.sysadmin) { methods.push('CRAM-MD5'); } + if (this.cfg.main.sysadmin) methods.push('CRAM-MD5'); connection.capabilities.push(`AUTH ${methods.join(' ')}`); connection.notes.allowed_auth_methods = methods; - return next(); + next(); } exports.check_plain_passwd = function (connection, user, passwd, cb) { @@ -49,11 +60,12 @@ exports.check_plain_passwd = function (connection, user, passwd, cb) { } socket.end(); // disconnect } - }); + }) + socket.on('end', () => { connection.loginfo(this, `AUTH user="${user}" success=${auth_success}`); - return cb(auth_success); - }); + cb(auth_success); + }) } exports.get_sock_opts = function (user) { @@ -66,13 +78,11 @@ exports.get_sock_opts = function (user) { const domain = (user.split('@'))[1]; let sect = this.cfg.main; - if (domain && this.cfg[domain]) { - sect = this.cfg[domain]; - } + if (domain && this.cfg[domain]) sect = this.cfg[domain]; - if (sect.port) { this.sock_opts.port = sect.port; } - if (sect.host) { this.sock_opts.host = sect.host; } - if (sect.sysadmin) { this.sock_opts.sysadmin = sect.sysadmin; } + if (sect.port) this.sock_opts.port = sect.port; + if (sect.host) this.sock_opts.host = sect.host; + if (sect.sysadmin) this.sock_opts.sysadmin = sect.sysadmin; this.logdebug(`sock: ${this.sock_opts.host}:${this.sock_opts.port}`); return this.sock_opts; @@ -89,14 +99,14 @@ exports.get_vpopmaild_socket = function (user) { socket.on('timeout', () => { this.logerror("vpopmaild connection timed out"); socket.end(); - }); + }) socket.on('error', err => { this.logerror(`vpopmaild connection failed: ${err}`); socket.end(); - }); + }) socket.on('connect', () => { this.logdebug('vpopmail connected'); - }); + }) return socket; } diff --git a/plugins/auth/flat_file.js b/plugins/auth/flat_file.js index 67c1b35d0..5ad1b0f99 100644 --- a/plugins/auth/flat_file.js +++ b/plugins/auth/flat_file.js @@ -3,26 +3,32 @@ exports.register = function () { this.inherits('auth/auth_base'); this.load_flat_ini(); + + if (this.cfg.core.constrain_sender) { + this.register_hook('mail', 'constrain_sender') + } } exports.load_flat_ini = function () { - this.cfg = this.config.get('auth_flat_file.ini', () => { + this.cfg = this.config.get('auth_flat_file.ini', { + booleans: [ + '+core.constrain_sender', + ] + }, + () => { this.load_flat_ini(); }); + + if (this.cfg.users === undefined) this.cfg.users = {} } exports.hook_capabilities = function (next, connection) { - // don't allow AUTH unless private IP or encrypted if (!connection.remote.is_private && !connection.tls.enabled) { - connection.logdebug(this, - "Auth disabled for insecure public connection"); + connection.logdebug(this, "Auth disabled for insecure public connection"); return next(); } - let methods = null; - if (this.cfg.core?.methods ) { - methods = this.cfg.core.methods.split(','); - } + const methods = this.cfg.core?.methods ? this.cfg.core.methods.split(',') : null if (methods && methods.length > 0) { connection.capabilities.push(`AUTH ${methods.join(' ')}`); connection.notes.allowed_auth_methods = methods; @@ -31,8 +37,7 @@ exports.hook_capabilities = function (next, connection) { } exports.get_plain_passwd = function (user, connection, cb) { - if (this.cfg.users[user]) { - return cb(this.cfg.users[user].toString()); - } - return cb(); + if (this.cfg.users[user]) return cb(this.cfg.users[user].toString()); + + cb(); }