Add SEND_ON and use it to validate outbound IPs

This commit is contained in:
PJ Eby 2020-03-08 03:11:55 -04:00
parent 51aa170185
commit 5de841f063
4 changed files with 56 additions and 56 deletions

View file

@ -3,7 +3,7 @@ RUN apt-get update && apt-get install less # 'less' is Useful for debugging
# Default to listening only on IPs bound to the container hostname
ENV LISTEN_ON=host
ENV OUTBOUND_MAIL_IP=
ENV SEND_ON=
COPY files /
RUN /patches && rm /patches

View file

@ -10,7 +10,7 @@ So, this image fixes these issues by adding support for two environment variable
The first variable it adds is `LISTEN_ON`, which can be set to either a list of specific IP addresses to listen on, `host` (to listen only on addresses bound to the container's hostname), or `*` (for poste's default behavior of listening on every available interface).
The second variable is `OUTBOUND_MAIL_IP`, which can be set to a specific IP to use, `*` to let the operating system pick an address, or it can be left empty or undefined, in which case the [configuration file](#managing-sender-ips) will be used to pick an IP address based on the domain the mail is sent from. (Or if no IP is found in the config file, the first listening IP will be used, unless `LISTEN_ON` is `*`, in which case the operating system will pick the address.)
The second variable is `SEND_ON`, which can also be set to a list of IP addresses, `host`, or `*`. (If unset or empty, it defaults to the value set in `LISTEN_ON`.) By default, mail will be sent from the first IP address in the resulting list, unless it's `*`, in which case the operating system will pick the IP. If there's only one sending IP, all mail will be sent from the default IP. Otherwise, a [configuration file](#managing-sender-ips) will be used to pick an IP address from the list, based on the domain the mail is being sent from.
### Basic Usage
@ -34,21 +34,22 @@ services:
# ==== Optional settings below: you don't need any environment vars by default ====
environment:
# Whitespace-separated list of IP addresses to listen on; first will be the
# default sending IP for outgoing mail. If this variable is set to "host"
# (the default if not given), the container will listen on all the IPs (v4
# and v6) found in DNS or /etc/hosts for the container's hostname. Or it can
# be set to "*", to listen on ALL available addresses (the way the standard
# poste.io image does).
# Whitespace-separated list of IP addresses to listen on. If this variable
# is set to "host" (which is also the default if it's empty or unset), the
# container will listen on all the IPs (v4 and v6) found in DNS or /etc/hosts
# for the container's hostname. Or it can be set to "*", to listen on ALL
# available addresses (the way the standard poste.io image does).
- "LISTEN_ON=1.2.3.4 5.6.7.8 90a:11:12::13"
# Force *all* outgoing mail to go via the specified IP address. Do NOT set
# this if you need multiple outgoing IPs: use a data/outbound-hosts.yml
# file instead! If this variable isn't set, the first LISTEN_ON address
# or DNS address for the hostname will be used, unless overridden in
# data/outbound-hosts.yml. (You can also set this to '*' to disable
# IP selection entirely, and let the OS pick the IP to use.)
- "OUTBOUND_MAIL_IP=9.10.11.12"
# Whitespace-separated list of IP addresses mail can be sent from; the first
# one in the list will be the default. Like LISTEN_ON, it can be set to '*'
# for "any available address" or 'host' for "any IP (v4 or v6) attached to
# the container hostname". If the list expands to only one address, it
# will be used for all outgoing mail. Otherwise, data/outbound-hosts.yml
# is read to determine the outgoing IP for each domain, and the result is
# validated against this list. If this variable is empty or unset, it defaults
# to whatever LISTEN_ON was set to.
- "SEND_ON=9.10.11.12"
# Other standard poste.io vars can also be used, e.g. HTTPS_PORT, etc.
@ -59,8 +60,8 @@ Take note of the following, however:
* You **must** configure the container with a fully-qualified hostname (e.g. `mail.example.com` above), with at least one IP address listed in the public DNS system
* The hostname's IP addresses (or those listed in `LISTEN_ON`) must be public IPs attached to the server hosting the container
* The listening IPs must *not* have any other services listening on ports 25, 80, 110, 143, 443, 466, 587, 993, 995, or 4190. (Though you can change or disable some of those ports using poste.io's environment variables.)
* You should be using **host-mode networking** (`network_mode: host` as shown above), since in any other networking mode, this image will behave roughly the same as the original `analogic/poste.io` image, and have the same limitations and caveats. (Specifically, using any other networking mode means putting IP addresses in `LISTEN_ON`, `OUTBOUND_MAIL_IP`, or `outbound-hosts.yml` will not do anything useful.)
* By default, outgoing email to other mail servers will be sent via the first IP address found in `LISTEN_ON` or returned by running `hostname -i` in the container. If you need to override this behavior, configure the container with an `OUTBOUND_MAIL_IP` environment variable specifying the IP address to be used, OR create a `/data/outbound-hosts.yml` file as described in [Managing Sender IPs](#managing-sender-ips) below.
* You should be using **host-mode networking** (`network_mode: host` as shown above), since in any other networking mode, this image will behave roughly the same as the original `analogic/poste.io` image, and have the same limitations and caveats. (Specifically, using any other networking mode means putting specific IP addresses into `LISTEN_ON`, `SEND_ON`, or `outbound-hosts.yml` will not do anything useful!)
* By default, outgoing email to other mail servers will be sent via the first IP address found in `LISTEN_ON` or returned by running `hostname -i` in the container. If you need to override this behavior, configure the container with `SEND_ON` set to the specific IP address to be used, OR create a `/data/outbound-hosts.yml` file as described in [Managing Sender IPs](#managing-sender-ips) below.
Notice, by the way, that there are **no port mappings** used in this example, because the container uses host-mode networking and thus has direct access to all of the server's network interfaces. This means that the IP addresses to be used by the container must be explicitly defined (either by the DNS address(es) of the hostname, or by setting the `LISTEN_ON` variable to the exact IP addresses) so that the container doesn't take over every IP address on the server. (Unless that's what you *want*, in which case you can set `LISTEN_ON` to `*`.)
@ -88,7 +89,7 @@ You must, however, still pick *one* primary hostname for the container, as that'
#### Separate IPs for Different Domains
If you want to give different domains their own IPs as well as separate hostnames, the steps are the same, except that each private-label hostname would have `A` or `AAAA` records pointing to the relevant IP address, instead of a CNAME pointing to the primary hostname. If you want these IPs to be used for outgoing mail as well, you'll also need to configure an `outbound-hosts.yml` file, as described in the next section.
If you want to give different domains their own IPs as well as separate hostnames, the steps are the same, except that each private-label hostname would have `A` or `AAAA` records pointing to the relevant IP address, instead of a CNAME pointing to the primary hostname. If you want these IPs to be used for outgoing mail as well, you'll also need to configure an `outbound-hosts.yml` file, as described in the next section. (And if needed, add them to the `SEND_ON` variable.)
You will, of course, still need to configure the container to listen on all these IPs, either by explicitly putting them in `LISTEN_ON`, or by adding them as `A` or `AAAA` records for the primary hostname. Or, if you're dedicating the entire server to a single poste instance, you can use `LISTEN_ON=*` to listen on every IP the box has.
@ -111,7 +112,7 @@ exampledomain.com:
With the above configuration, mails sent from `exampledomain.com` will be sent with a HELO of `mx.exampledomain.com`, using an outbound IP of `5.6.7.8`, and mail for any other domain will use the defaults. (Assuming, of course, that `5.6.7.8` is one of the addresses the container listens on.)
Note that the information in this file is *not* validated against DNS or checked for security (aside from a basic check that the IP is one listened to by the container). It is your responsibility to ensure that all `helo` hostnames exist in DNS with the matching `ip` , and that all listed IP addresses are actually valid for the network interfaces on your server.
Note that the information in this file is *not* validated against DNS or checked for security (aside from a basic check that the IP is included in the expansion of `SEND_ON`). It is your responsibility to ensure that all `helo` hostnames exist in DNS with the matching `ip` , and that all listed IP addresses are actually valid for the network interfaces on your server.
In addition, for best deliverability, you should also:

View file

@ -1,65 +1,64 @@
#!/usr/bin/with-contenv bash
# === Configuration Variables ===
# bindhost = host name
# bindlist = array of IP addresses (or '*' and '::' wildcards)
# ipaddrs = comma-separated string form of bindlist
bindhost=$(hostname)
case ${LISTEN_ON:=host} in
host) read -ra bindlist < <(hostname -i) ;;
'*') bindlist=('*' '::') ;;
*) read -ra bindlist <<<"${LISTEN_ON}" ;;
# Given a variable name and setting, get the matching IP addresses as a comma-delimited list
function ip_list() {
local -n ips=$1
case $2 in
host) ips=$(hostname -i) ;;
'*') ips='* ::' ;;
*) read -ra ips <<<"$2"; ips=("${ips[*]}") ;; # trim/normalize whitespace
esac
ips="${ips// /,}"; ips=${ips:-*,::} # handle empty list
}
ipaddrs=${bindlist[*]}; ipaddrs=${ipaddrs// /,}
# Expand LISTEN_ON and SEND_ON into comma-delimited IP lists in `listen` and `send`
ip_list listen "${LISTEN_ON:=host}"
ip_list send "${SEND_ON:=${listen//,/ }}"
# Do simple sed subtitutions (assumes '"' not present in pattern/replacement strings)
function sub() { sed -i 's"'"$1"'"'"$2"'"' "$3"; } # replace $1 w/$2 in $3
function ins() { sed -i '\"'"$1"$'"i \\\n'"$2" "$3"; } # insert $2 before $1 in $3
function del() { sed -i '\"'"$1"'"d' "$2"; } # delete lines matching $1 from $2
# === Configure dovecot and nginx to bind or connect with the right IPs ===
bindhost=$(hostname)
# We only care about the hostname for connnecting to the submission port
sed -i 's/submission_host = .*:587$/submission_host = '"$bindhost:587/" /etc/dovecot/conf.d/15-lda.conf
sub 'submission_host = .*:587$' "submission_host = $bindhost:587" /etc/dovecot/conf.d/15-lda.conf
if [[ "$LISTEN_ON" == host ]]; then
# No IPs given, just use the hostname
sed -i 's/__HOST__/'"$bindhost"/ /etc/nginx/sites-enabled/administration
sed -i 's/^#\?listen = .*/listen = '"${bindhost}/" /etc/dovecot/dovecot.conf
sub '__HOST__' "$bindhost" /etc/nginx/sites-enabled/administration
sub '^#\?listen = .*' "listen = ${bindhost}" /etc/dovecot/dovecot.conf
else
# We have explicit listening IPs (or wildcards): give them to dovecot and nginx
sed -i 's/^#\?listen = .*/listen = '"${ipaddrs}/" /etc/dovecot/dovecot.conf
sub '^#\?listen = .*' "listen = ${listen}" /etc/dovecot/dovecot.conf
function add_nginx_listener() {
# Add a listen line above the default one, for the specified address, port and options
sed -i '/__HOST__:'"$2/i \\"$'\n'" listen $1:$2${3+ $3};" /etc/nginx/sites-enabled/administration
}
for addr in "${bindlist[@]}"; do
IFS=, read -ra ipaddrs <<<"$listen"
for addr in "${ipaddrs[@]}"; do
if [[ "$addr" == *:* ]]; then addr="[${addr}]"; fi # nginx needs IPv6 addresses to be in '[]'
add_nginx_listener "$addr" "$HTTP_PORT"
add_nginx_listener "$addr" "$HTTPS_PORT" ssl
# Add listen lines above the default ones, for the specified address, port and options
ins "__HOST__:$HTTP_PORT" " listen $addr:$HTTP_PORT;" /etc/nginx/sites-enabled/administration
ins "__HOST__:$HTTPS_PORT" " listen $addr:$HTTPS_PORT ssl;" /etc/nginx/sites-enabled/administration
done
# delete the original listening lines we were using as insertion targets
sed -i '/__HOST__:/d' /etc/nginx/sites-enabled/administration
del '__HOST__:' /etc/nginx/sites-enabled/administration
fi
# === Haraka needs each IP address to be listed explicitly, unless you're using wildcards ===
if [[ $ipaddrs != *'*'* ]]; then
listen025=${ipaddrs//,/:25,}:25
listen465=${ipaddrs//,/:465,}:465
listen587=${ipaddrs//,/:587,}:587
sed -i 's/^listen=.*:25$/listen='"$listen025/" /opt/haraka-smtp/config/smtp.ini
sed -i 's/^listen=.*:587,.*:465$/listen='"$listen587,$listen465/" /opt/haraka-submission/config/smtp.ini
if [[ $listen != *'*'* ]]; then
sub '^listen=.*:25$' "listen=${listen//,/:25,}:25" /opt/haraka-smtp/config/smtp.ini
sub '^listen=.*:587,.*:465$' "listen=${listen//,/:587,}:587,${listen//,/:465,}:465" /opt/haraka-submission/config/smtp.ini
fi
# Our Haraka sender-ip control plugin will validate outgoing IPs against the
# listening address list, and use the first address as the default (which may
# sending address list, and use the first address as the default (which may
# be '*', meaning "let the OS pick an outgoing IP".)
echo "$ipaddrs" >/opt/haraka-submission/config/my-ips
echo "$ipaddrs" >/opt/haraka-smtp/config/my-ips
echo "$send" >/opt/haraka-submission/config/my-ips
echo "$send" >/opt/haraka-smtp/config/my-ips

View file

@ -60,7 +60,7 @@ exports.hook_get_mx = async function(next, hmail, domain) {
set_outbound({"default": {helo: hostname, ip: default_ip}}, 'default');
}
if ( process.env.OUTBOUND_MAIL_IP || my_ips.length === 1 || default_ip === '*' ) {
if ( process.env.OUTBOUND_MAIL_IP || my_ips.length === 1 ) {
use_default();
} else {
const from_domain = hmail.todo.mail_from.host;