Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: if AUTH, assure Env From dom matches AUTH dom #3265

Merged
merged 6 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions config/auth_flat_file.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[core]
methods=CRAM-MD5
; constrain_sender=true

[users]
; matt=test
6 changes: 4 additions & 2 deletions config/auth_vpopmaild.ini
Original file line number Diff line number Diff line change
@@ -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
34 changes: 15 additions & 19 deletions docs/plugins/auth/auth_vpopmaild.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
```
53 changes: 23 additions & 30 deletions docs/plugins/auth/flat_file.md
Original file line number Diff line number Diff line change
@@ -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
38 changes: 27 additions & 11 deletions plugins/auth/auth_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(' ')}`);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -117,17 +118,15 @@ 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;
}

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) {
Expand Down Expand Up @@ -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), () => {
Expand All @@ -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}'`)
}
48 changes: 29 additions & 19 deletions plugins/auth/auth_vpopmaild.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down
29 changes: 17 additions & 12 deletions plugins/auth/flat_file.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
Loading