From 78b517d3c2128e8f38c9ed834210f660ac535d5d Mon Sep 17 00:00:00 2001 From: PJ Eby Date: Sat, 29 Dec 2018 00:10:36 -0500 Subject: [PATCH] Initial revision --- Dockerfile | 4 + README.md | 13 ++ files/etc/dovecot/local.conf | 49 +++++ .../rspamd/override.d/worker-controller.inc | 1 + files/etc/rspamd/override.d/worker-normal.inc | 1 + files/etc/rspamd/override.d/worker-proxy.inc | 1 + files/opt/haraka-smtp/config/redis.ini | 2 + files/opt/haraka-submission/config/redis.ini | 1 + files/patches | 173 ++++++++++++++++++ 9 files changed, 245 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 files/etc/dovecot/local.conf create mode 100644 files/etc/rspamd/override.d/worker-controller.inc create mode 100644 files/etc/rspamd/override.d/worker-normal.inc create mode 100644 files/etc/rspamd/override.d/worker-proxy.inc create mode 100644 files/opt/haraka-smtp/config/redis.ini create mode 120000 files/opt/haraka-submission/config/redis.ini create mode 100755 files/patches diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c116965 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM analogic/poste.io +RUN apt-get update && apt-get install less # 'less' is Useful for debugging +COPY files / +RUN /patches && rm /patches \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f3997c --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +## A Fully-Isolated Poste.io Image + +[poste.io](https://poste.io) is a pretty cool email server implementation for docker. Unfortunately, it doesn't play well with other mail servers on the same machine, which makes it hard to e.g., have both a development and production instance. + +Specifically, poste.io *requires* docker host mode networking, but then binds its outward-facing services to *every* IP address of the machine, *and* binds several of its internal services to localhost ports (24, 6379, 11332-11334, 11380, and 13001), which can conflict with things besides mail servers or other poste.io instances. + +As a result, poste.io not only doesn't play well with other mail servers, it doesn't play well with being used on a server that *does anything else*. (It almost might as well not be a docker container at all!) + +So this image fixes these issues, by tweaking service configurations to only bind services on the IP that corresponds to the container's hostname, and replace localhost TCP sockets with unix domain sockets, kept privately within the container. (Thereby preventing conflicts or confusion with other bindings of those ports on the localhost interface.) + +Unfortunately, poste's admin tool isn't written with unix sockets in mind, and neither are significant parts of haraka and its plugins. Thus, in addition to adding the configuration files found under [files/](files/), this image also has to [patch a lot of files](files/patches). (Most of the patching is done at image build time, but a few are tweaked at container start by a patched version of `/etc/cont-init.d/23-nginx.sh`, because nginx and haraka don't allow variable substitution in the part of their config files that set listening ports.) + +(Note: this image relies even more on a correct docker hostname than poste.io does. Make sure that the hostname you assign to the container is public, fully-qualified, and maps to exactly one IPv4 address.) \ No newline at end of file diff --git a/files/etc/dovecot/local.conf b/files/etc/dovecot/local.conf new file mode 100644 index 0000000..5329827 --- /dev/null +++ b/files/etc/dovecot/local.conf @@ -0,0 +1,49 @@ +# Bind services to current host only + +service imap-login { + inet_listener imap { + address = $hostname + } + inet_listener imaps { + address = $hostname + } +} + +service pop3-login { + inet_listener pop3 { + address = $hostname + } + inet_listener pop3s { + address = $hostname + } +} + +service managesieve-login { + inet_listener sieve { + address = $hostname + } +} + +# Move lmtp and quota to a socket in place of localhost + +service lmtp { + unix_listener lmtp { + path = /var/run/lmtp.sock + mode = 0666 + } + + inet_listener lmtp { + port = 0 + } +} + +service quota-status { + inet_listener { + port = 0 + } + unix_listener { + path = /var/run/dovecot-quota.sock + mode = 0666 + } +} + diff --git a/files/etc/rspamd/override.d/worker-controller.inc b/files/etc/rspamd/override.d/worker-controller.inc new file mode 100644 index 0000000..8650d73 --- /dev/null +++ b/files/etc/rspamd/override.d/worker-controller.inc @@ -0,0 +1 @@ +bind_socket = "/var/run/rspamd-web.sock mode=0666"; diff --git a/files/etc/rspamd/override.d/worker-normal.inc b/files/etc/rspamd/override.d/worker-normal.inc new file mode 100644 index 0000000..02c9a45 --- /dev/null +++ b/files/etc/rspamd/override.d/worker-normal.inc @@ -0,0 +1 @@ +bind_socket = "/var/run/rspamd-normal.sock mode=0666"; diff --git a/files/etc/rspamd/override.d/worker-proxy.inc b/files/etc/rspamd/override.d/worker-proxy.inc new file mode 100644 index 0000000..cbde2b4 --- /dev/null +++ b/files/etc/rspamd/override.d/worker-proxy.inc @@ -0,0 +1 @@ +bind_socket = "/var/run/rspamd-proxy.sock mode=0666"; diff --git a/files/opt/haraka-smtp/config/redis.ini b/files/opt/haraka-smtp/config/redis.ini new file mode 100644 index 0000000..2034aac --- /dev/null +++ b/files/opt/haraka-smtp/config/redis.ini @@ -0,0 +1,2 @@ +[server] +path = /var/run/redis/redis.sock diff --git a/files/opt/haraka-submission/config/redis.ini b/files/opt/haraka-submission/config/redis.ini new file mode 120000 index 0000000..7fa19bc --- /dev/null +++ b/files/opt/haraka-submission/config/redis.ini @@ -0,0 +1 @@ +../../haraka-smtp/config/redis.ini \ No newline at end of file diff --git a/files/patches b/files/patches new file mode 100755 index 0000000..8db1fc6 --- /dev/null +++ b/files/patches @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +# === Patches for poste.io free version to be a properly isolated service === +# +# - Restricts all public listening ports to the IP(s) associated with the +# container's hostname +# +# - Replaces all localhost listening ports with unix-domain sockets inside +# the container +# +# With these changes, multiple poste.io instances can be run on the same +# machine (as long as each container has its own public IP), and no internal +# services (such as lmtp, quota, websockets, etc.) are exposed to the host's +# loopback interface. + +set -eu # fail on any errors or undefined variables + +# A tiny DSL for editing files with sed: `~ edit files...; {{ commands }}` +edit() { local sed; ::block sed-dsl; sed -i -e "$sed" "$@"; } +sed-dsl() { sed."$@"; } +sed.sub() { sed+="s~$1~$2~${3-}"$'\n'; } +sed.del() { sed+="/$1/d"$'\n'; } +sed.append() { sed+='$a'; ((!$#))||__sedline "$@"; ::block __sedline; sed+=$'\n'; } +sed.after() { sed+='/'"$1"'/a'; (($#<2))||__sedline "${@:2}"; ::block __sedline; sed+=$'\n'; } +__sedline() { sed+="${*/#/\\$'\n'}"; } + +# DSL syntax macros: minified runtime copied from https://github.com/bashup/scale-dsl +shopt -q expand_aliases||{ unalias -a;shopt -s expand_aliases;};builtin alias +='{ ::__;::(){ ((!$#))||{ shift;"${__dsl__[@]-::no-dsl}" ' ~='{ ::__;::(){ ((!$#))||{ shift; ' -='"${__dsl__[@]-::no-dsl}" ' '{{=return;return;};__blk__=;set -- "${__blarg__[@]:1}"; ' '}}=};__:: 0 "$@";}';::block(){ ((!$#))||local __dsl__=("$@");${__blk__:+::};};__bsp__=0;::__(){ __bstk__[__bsp__++]="${__blk__:+__blk__=1;$(declare -f ::)}";};__::(){ local __blarg__=("$@");__blk__=1;:: "$@"||set -- $?;__blk__=;local REPLY;${__bstk__[--__bsp__]:+eval "${__bstk__[__bsp__]}"}||:;return $1;} + + +# === Restrict public ports to the container hostname IP === + +~ edit /opt/www/webmail/config/config.inc.php; {{ + # Make webmail connect to the public hostname, instead of localhost + + append ""; {{ + - "\$config['default_host'] = 'ssl://' . gethostname();" + - "\$config['smtp_server'] = 'tls://' . gethostname() . ':587';" + }} +}} + +~ edit /etc/nginx/sites-enabled.templates/{no-,}https; {{ + # Remove the listen lines that lack an address + - del 'listen __HTTP_PORT__;' + - del 'listen __HTTPS_PORT__ ssl;' + + # Replace the IPv6 wildcard and any localhost references w/explicit host + - sub 'listen \[::\]:' 'listen __HOST__:' + - sub localhost '$hostname' +}} + +~ edit /etc/cont-init.d/23-nginx.sh; {{ + # This file is run at container start, so we hijack it to inject the runtime + # hostname into the nginx and haraka configs (where variables aren't allowed) + + append ""; {{ + - 'bindhost=\$(hostname)' + for i in submission smtp; do + - "hostname -i >/opt/haraka-$i/config/my-ip" + done + - "sed -i 's/__HOST__/'\"\$bindhost\"/ /etc/nginx/sites-enabled/administration" + - "sed -i 's/^listen=.*:25\$/listen='\"\$bindhost/\" /opt/haraka-smtp/config/smtp.ini" + - "sed -i 's/^listen=.*:587,.*:465\$/listen='\"\$bindhost:587,\$bindhost:465/\" /opt/haraka-submission/config/smtp.ini" + }} +}} + +~ edit /opt/haraka-smtp/plugins/poste.js; {{ + # Force outbound smtp to go via container hostname, unless overridden by + # another plugin. (We only edit the haraka-smtp version because the + # haraka-submissions/plugins dir is a symlink.) + + after "plugin\.register_hook('init_child'"; {{ + - " plugin.register_hook('get_mx', 'get_mx');" + }} + + append ""; {{ + - 'const my_outbound_ip = require("haraka-config").get("my-ip");' + - 'exports.get_mx = function (next, hmail, domain) { hmail.todo.notes.outbound_ip=my_outbound_ip; next(); }' + }} +}} + + +# === Replace localhost ports with unix sockets ==== + +# Note: if you change any of these socket names or locations, they must also be +# changed in the corresponding files, as applicable: +# +# - files/etc/dovecot/local.conf +# - files/etc/rspamd/override.d/worker-*.inc +# - files/opt/haraka-smtp/config/redis.ini + +sockdir=/var/run +rspam_web=$sockdir/rspamd-web.sock +rspam=$sockdir/rspamd-normal.sock +lmtp=$sockdir/lmtp.sock +quota=$sockdir/dovecot-quota.sock + +# redis and haraka run unprivileged and so need directories of their own +mkdir -p "$sockdir"/redis "$sockdir"/haraka +chown redis "$sockdir"/redis +chown delivery "$sockdir"/haraka + +redis="$sockdir"/redis/redis.sock +haraka_web=$sockdir/haraka/web.sock + + +# Change nginx proxy settings to use unix sockets + +~ edit /etc/nginx/sites-enabled.templates/{no-,}https; {{ + - sub 127.0.0.1:11334 unix:"$rspam_web": + - sub 127.0.0.1:11380 unix:"$haraka_web": +}} + +# The rspamc command needs to reference the web socket explicitly + +~ edit /opt/admin/src/AppBundle/Server/System.php; {{ + - sub "rspamc stat" \ + "rspamc -h $rspam_web stat" +}} +~ edit /etc/dovecot/sieve/report-{spam,ham}.sieve; {{ + - sub '"rspamc" \[' \ + '"rspamc" ["-h" "'"$rspam_web"'" ' +}} + +# Haraka plugins need to use sockets for LMTP and quota instead of ports + +~ edit /opt/haraka-smtp/plugins/rcpt_database.js; {{ + - sub ", port: 24};" \ + ", port: 24, path: '$lmtp'}" +}} +~ edit /opt/haraka-smtp/plugins/dovecot_quota.js; {{ + - sub "socket\\.connect(13001, '127.0.0.1');" \ + "socket.connect('$quota');" +}} + +# Haraka web server needs to listen on a unix socket + +~ edit /usr/lib/node_modules/Haraka/server.js; {{ + - sub 'Server\.http\.server\.listen(.*, 0);' \ + "Server.http.server.listen('$haraka_web', 0);" +}} + +# Have haraka talk to rspamd via unix socket + +~ edit /usr/lib/node_modules/Haraka/node_modules/haraka-plugin-rspamd/index.js; {{ + - del 'port: plugin' + - sub 'host: plugin.*,' \ + "socketPath: '$rspam'," +}} + +# Redis plugin doesn't support unix paths by default; fix it + +~ edit /usr/lib/node_modules/Haraka/node_modules/haraka-plugin-redis/index.js; {{ + - sub ", 'db'\\]\\.forEach" \ + ", 'db', 'path'].forEach" + - sub '+ opts\.port;$' \ + '+ opts.port + (opts.path || "");' + - sub 'opts.port = plugin.redisCfg.server.port;' \ + 'opts.path = plugin.redisCfg.server.path;' +}} + +# Configure redis to listen on a unix socket, and rspamd/admin to connect there + +~ edit /etc/redis/redis.conf; {{ + - sub "^port 6379" "port 0" # disable the localhost port + - append "" "unixsocket $redis" "unixsocketperm 777" +}} + +~ edit /etc/rspamd/local.d/{redis,statistic}.conf; {{ + - sub 'servers = "127.*;$' \ + 'servers = "'"$redis"'";' +}} +~ edit /opt/admin/vendor/predis/predis/src/Client.php; {{ + # Make the Predis\Client constructor default to the redis unix socket + - sub '__construct($parameters = null,' \ + '__construct($parameters = "unix:'"$redis"'",' +}}