Skip to content

Commit

Permalink
- tls_socket: load TLS certs in subdirs (config/tls/sub/*.(key|crt)
Browse files Browse the repository at this point in the history
- tls_socket:
  - getSocketOpts is now async
  - parse_x509 is now async
  - shed dependency on caolan/async & openssl-wrapper
  - get_certs_dir is now async
    - completely refactored.
    - config/tls loading is now recursive
    - watches config/tls for changes
  - tolerate spaces in CN string
- outbound: use HarakaMx class for MX objects
- line_socket: remove unused callback
- outbound/client_pool: don't use line_socket, use tls_socket directly
  - client_pool: sock.name is now JSON of socket args
  - client_pool.get_client & release_client: arity of 5 -> 2
- catch ENOENT for config/tls
- doc(tls): updated with TLS dir rules
- workaround for windows * restriction
  • Loading branch information
msimerson committed May 3, 2024
1 parent b888217 commit 097874c
Show file tree
Hide file tree
Showing 14 changed files with 360 additions and 372 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
with:
node-version: ${{ matrix.node-version }}
# - run: openssl x509 -in test/config/tls/ec.pem -noout -enddate -subject -ext subjectAltName
- run: npm install --omit=optional
- run: npm run test

Expand Down
42 changes: 27 additions & 15 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#### Added

- doc: add CONTRIBUTORS #3312
- tls_socket: `config/tls` dir loading is now recursive

#### Changed

Expand All @@ -14,32 +15,38 @@
- dkim: repackaged as NPM module #3311
- new NPM plugin dns-list, repackages dnsbl, dnswl, backscatterer #3313
- test: add a connection.response test case with DSN #3305
- deps: bump all versions to latest #3303
- deps: bump all versions to latest #3303, #3344
- when using message-stream, don't send default options #3290
- auth_base: enable disabling constrain_sender at runtime #3298
- auth_base: skip constrain_sender when auth user has no domain #3319
- rcpt_to.host_list: add connection ID to log messages #3322
- connection: support IPv6 when setting remote.is_private #3295
- in setTLS, replace forEach with for...of
- NOTE: remove a handful of 3.0 sunset property names #3315
- outbound/mx_lookup: make it async/await
- outbound/mx_lookup: deleted. Logic moved into net_utils #3322
- outbound: emit log message when ignoring local MX #3285
- outbound: pass in config when initiating txn #3315
- outbound: minor es6 updates #3315, #3322
- outbound: logging improvements #3322
- was: [-] [core] [outbound] Failed to get socket: Outbound connection error: Error: connect ECONNREFUSED 172.16.16.14:25
- now: [A63B62DF-F3B8-4096-8996-8CE83494A188.1.1] [outbound] Failed to get socket: connect ECONNREFUSED 172.16.16.14:25
- shorter logger syntax: logger.loginfo -> logger.info
- outbound: remove log prefixes of `[outbound] `, no longer needed
- line_socket: remove unused callback #3344
- logger: don't load outbound (race condition). Instead, set name property #3322
- logger: extend add_log_methods to Classes (connection, plugins, hmail) #3322
- logger: when logging via `logger` methods, use short names #3322
- logger: check Object.hasOwn to avoid circular deps
- outbound: delete try_deliver_host. Use net_utils to resolve MX hosts to IPs #3322
- outbound: remove config setting ipv6_enabled #3322
- outbound: remove undocumented use of send_email with arity of 2. #3322
- outbound: encapsulate force_tls logic into get_force_tls #3322
- outbound
- client_pool: use tls_socket directly (shed line_socket)
- client_pool: sock.name is now JSON of socket args
- client_pool.get_client & release_client: arity of 5 -> 2
- mx_lookup: make it async/await
- mx_lookup: deleted. Logic moved into net_utils #3322
- use net_utils.HarkaMx for get_mx parsing #3344
- emit log message when ignoring local MX #3285
- pass in config when initiating txn #3315
- minor es6 updates #3315, #3322
- logging improvements #3322
- was: [-] [core] [outbound] Failed to get socket: Outbound connection error: Error: connect ECONNREFUSED 172.16.16.14:25
- now: [A63B62DF-F3B8-4096-8996-8CE83494A188.1.1] [outbound] Failed to get socket: connect ECONNREFUSED 172.16.16.14:25
- shorter logger syntax: logger.loginfo -> logger.info
- remove log prefixes of `[outbound] `, no longer needed
- delete try_deliver_host. Use net_utils to resolve MX hosts to IPs #3322
- remove config setting ipv6_enabled #3322
- remove undocumented use of send_email with arity of 2. #3322
- encapsulate force_tls logic into get_force_tls #3322
- queue/lmtp: refactored for DRY and improved readability #3322
- mail_from.resolvable: refactored, leaning on improved net_utils #3322
- fixes haraka/haraka-net-utils#88
Expand All @@ -51,6 +58,11 @@
- server.js: use the local logger methods
- get Haraka version from utils.getVersion (which includes git id if running from repo)
- tls_socket: remove secureConnection. Fixes #2743
- getSocketOpts is now async
- parse_x509 is now async
- shed dependency on caolin/async & openssl-wrapper
- get_certs_dir is now async
- completely refactored.
- .gitignore: add config/me and config/*.pem
- test: convert test runner to mocha
- test: rename tests -> test (where test runner expect) #3340
Expand Down
26 changes: 4 additions & 22 deletions docs/Outbound.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,28 +148,10 @@ Upon starting delivery the `get_mx` hook is called, with the parameter set to
the domain in question (for example a mail to `[email protected]` will call the
`get_mx` hook with `(next, hmail, domain)` as parameters). This is to allow
you to implement a custom handler to find MX records. For most installations
there is no reason to implement this hook - Haraka will find the correct MX
records for you.

The MX record is sent via next(OK, mx) and can be one of:

* A string of one of the following formats:
* hostname
* hostname:port
* ipaddress
* ipaddress:port
* An MX object of the form: `{priority: 0, exchange: hostname}` with the
following optional properies:
* `port` to specify an alternate port
* `bind` to specify an outbound IP address to bind to
* `bind_helo` to specify an outbound helo for IP address to bind to
* `using_lmtp` boolean to specify that delivery should be attempted using
LMTP instead of SMTP.
* `auth_user` to specify an AUTH username (required if AUTH is desired)
* `auth_pass` to specify an AUTH password (required if AUTH is desired)
* `auth_type` to specify an AUTH type that should be used with the MX.
If this is not specified then Haraka will pick an appropriate method.
* A list of MX objects in an array, each in the same format as above.
there is no reason to implement this hook - Haraka will find the MX
records via DNS.

The MX is sent via next(OK, mx). `mx` is a [HarakaMx](https://github.com/haraka/haraka-net-utils?tab=readme-ov-file#harakamx) object, an array of HarakaMx objects, or any suitable HarakaMx input.

### The deferred hook

Expand Down
38 changes: 29 additions & 9 deletions docs/plugins/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ This plugin enables the use of TLS (via `STARTTLS`) in Haraka.

For this plugin to work you must have SSL certificates installed correctly.

Haraka has [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication) support. When the remote MUA/MTA presents a servername during the TLS handshake and a TLS certificate with that Common Name matches, that certificate will be presented. If no match is found, the default certificate (see Certificate Files) is presented.

## Certificate Files

Defaults are shown and can be overridden in `config/tls.ini`.
Defaults settings are shown and can be overridden in `config/tls.ini`.

```ini
key=tls_key.pem
Expand All @@ -16,20 +18,39 @@ dhparam=dhparams.pem

## Certificate Directory

If the directory `config/tls` exists, each file within the directory is expected to be a PEM encoded TLS bundle. Generate the PEM bundles in The Usual Way[TM] by concatenating the key, certificate, and CA/chain certs in that order. Example:
If the directory `config/tls` exists, files within the directory are PEM encoded TLS files in one of two formats: bundles or Wild Wild West.

### Certificate bundles

Generate PEM bundles in The Usual Way[TM] by concatenating the key, certificate, and CA/chain certs in that order. Example:

```sh
cat example.com.key example.com.crt ca.crt > config/tls/example.com.pem
cat example.com.key example.com.crt ca-int.crt > haraka/config/tls/example.com.pem
```

An example [acme.sh](https://acme.sh) deployment [script](https://github.com/msimerson/Mail-Toaster-6/blob/master/provision/letsencrypt.sh) demonstrates how to install [Let's Encrypt](https://letsencrypt.org) certificates to the Haraka `config/tls`directory.
An example [acme.sh](https://acme.sh) deployment [script](https://github.com/msimerson/Mail-Toaster-6/blob/master/provision/letsencrypt.sh) installs [Let's Encrypt](https://letsencrypt.org) certificate bundles to the Haraka `config/tls`directory.

### Wild Wild West

PEM encoded TLS certificates and keys can be stored in files in `config/tls`. The certificate loader is recursive, so TLS files can be in subdirs like `config/tls/mx1.example.com`. The certificate names are parsed from the 1st cert in each file and indexed by the certs Common Name(s). Subject Alternate Names are supported. The file name containing the certificates does *not* matter. Additional certificates within each file are presumed to be CA chain (intermediate) certificates.

If the TLS key is stored in the same file as the matching certificate, then the name of the file does not matter. If the TLS key is alone in a file, the file MUST be named with the keys Common Name. The file extension does not matter, `.pem` and `.key` are common. If the key is used for multiple CNs, the key must be stored in a file name matching each CN. Examples of working TLS key/cert file pairs for the Common Name mx1.example.com:

Haraka has [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication) support. When the remote MUA/MTA presents a servername during the TLS handshake and a TLS certificate with that Common Name matches, that certificate will be presented. If no match is found, the default certificate (see Certificate Files above) is presented.
1. certificate bundle (see above), key & cert in same file
- config/tls/mx1.example.com.pem (recommended)
- config/tls/any-unique-name.pem (CN is extracted from 1st cert)
2. files in TLS dir
- config/tls/mx1.example.com.crt
- config/tls/mx1.example.com.key
3. files in subdir
- config/tls/example.com/mx1.cert
- config/tls/example.com/mx1.example.com.key
4. wildcard bundle on Windows platform (* is not allowed in file names)
- config/tls/_.example.com.pem

## Purchased Certificate

If you have a purchased certificate, append any intermediate/chained/ca-cert
files to the certificate in this order:
For purchased certificate, append any intermediate/chained/ca-cert files to the certificate in this order:

1. The CA signed SSL cert
2. Any intermediate certificates
Expand All @@ -39,8 +60,7 @@ See also [Setting Up TLS](https://github.com/haraka/Haraka/wiki/Setting-up-TLS-w

## Self Issued (unsigned) Certificate

Create a certificate and key file in the config directory with the following
command:
Create a certificate and key file in the config directory with the following command:

openssl req -x509 -nodes -days 2190 -newkey rsa:2048 \
-keyout config/tls_key.pem -out config/tls_cert.pem
Expand Down
1 change: 0 additions & 1 deletion haraka.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ catch (e) {
require('module')._initPaths(); // Horrible hack
}

const fs = require('fs');
const utils = require('haraka-utils');
const logger = require('./logger');
const server = require('./server');
Expand Down
5 changes: 2 additions & 3 deletions line_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@ function setup_line_processor (socket) {
exports.Socket = Socket;

// New interface - uses TLS
exports.connect = (port, host, cb) => {
exports.connect = (port, host) => {
let options = {};
if (typeof port === 'object') {
options = port;
cb = host;
}
else {
options.port = port;
options.host = host;
}
const sock = tls_socket.connect(options, cb);
const sock = tls_socket.connect(options);
setup_line_processor(sock);
return sock;
}
29 changes: 15 additions & 14 deletions outbound/client_pool.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
'use strict';

const utils = require('haraka-utils');
const utils = require('haraka-utils');
const net_utils = require('haraka-net-utils')

const sock = require('../line_socket');
const logger = require('../logger');

const obc = require('./config');
const tls_socket = require('../tls_socket');
const logger = require('../logger');
const obc = require('./config');

exports.name = 'outbound'

// Get a socket for the given attributes.
exports.get_client = function (port = 25, host = 'localhost', localAddress, is_unix_socket, callback) {
exports.get_client = function (mx, callback) {
const socketArgs = mx.path ? { path: mx.path } : { port: mx.port, host: mx.exchange, localAddress: mx.bind };

const socketArgs = is_unix_socket ? {path: host} : {port, host, localAddress};
const socket = sock.connect(socketArgs);
const socket = tls_socket.connect(socketArgs);
net_utils.add_line_processor(socket);

socket.name = `outbound::${port}:${host}:${localAddress}`;
socket.name = `outbound::${JSON.stringify(socketArgs)}`;
socket.__uuid = utils.uuid();
socket.setTimeout(obc.cfg.connect_timeout * 1000);

logger.debug(exports, `created. host: ${host} port: ${port}`, { uuid: socket.__uuid });
logger.debug(exports, `created ${socket.name}`, { uuid: socket.__uuid });

socket.once('connect', () => {
socket.removeAllListeners('error'); // these get added after callback
Expand All @@ -38,13 +39,13 @@ exports.get_client = function (port = 25, host = 'localhost', localAddress, is_u
socket.end();
socket.removeAllListeners();
socket.destroy();
callback(`connection timed out to ${host}:${port}`, null);
callback(`connection timed out to ${socket.name}`, null);
})
}

exports.release_client = (socket, port, host, local_addr, error) => {
let logMsg = `release_client: ${socket.__uuid} ${host}:${port}`
if (local_addr) logMsg += ` from ${local_addr}`
exports.release_client = (socket, mx) => {
let logMsg = `release_client: ${socket.name}`
if (mx.bind) logMsg += ` from ${mx.bind}`
logger.debug(exports, logMsg);
socket.removeAllListeners();
socket.destroy();
Expand Down
Loading

0 comments on commit 097874c

Please sign in to comment.