From 928e5d64c7d94e67adc6dc5764e4c7270b3efb7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20=C4=8Cekrli=C4=87?= Date: Tue, 19 Feb 2019 10:49:55 +0100 Subject: [PATCH] Initial support for DKIM and testing This commit brings two important features: - **DKIM support** It's now possible to configure this postfix image to sign messages using DKIM by simply generating the keys and providing them in the approprate folder. This should bring us one step closer to directly sending out emails without relying on a 3rd-party proxy. - **test support** A nice and handy script, conviniently called `test.sh` has been provided, builds the image, spins it up and tries to send out an email. You'll need `docker-compose` to run it, though. --- Dockerfile | 14 ++++--- README.md | 37 +++++++++++++++--- docker-compose.yml | 19 +++++++++ opendkim.conf | 20 ++++++++++ opendkim.sh | 10 +++++ run.sh | 63 ++++++++++++++++++++++++++++-- supervisord.conf | 13 ++++++ test-keys/example.org/mail.private | 15 +++++++ test-keys/example.org/main.txt | 2 + test.sh | 32 +++++++++++++++ 10 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 docker-compose.yml create mode 100644 opendkim.conf create mode 100644 opendkim.sh create mode 100644 test-keys/example.org/mail.private create mode 100644 test-keys/example.org/main.txt create mode 100755 test.sh diff --git a/Dockerfile b/Dockerfile index 7ff79d6..2dfbe13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM alpine:latest -MAINTAINER Bojan Cekrlic - https://github.com/bokysan/docker-postfix/ +LABEL maintaner="Bojan Cekrlic - https://github.com/bokysan/docker-postfix/" # See README.md for details @@ -27,23 +27,25 @@ ENV INBOUND_DEBUGGING= # Install supervisor, postfix RUN true && \ - apk add --no-cache --update postfix ca-certificates tzdata supervisor rsyslog && \ + apk add --no-cache --update postfix ca-certificates tzdata supervisor rsyslog opendkim && \ apk add --no-cache --upgrade musl musl-utils && \ (rm "/tmp/"* 2>/dev/null || true) && (rm -rf /var/cache/apk/* 2>/dev/null || true) # Set up configuration COPY supervisord.conf /etc/supervisord.conf COPY rsyslog.conf /etc/rsyslog.conf +COPY opendkim.conf /etc/opendkim/opendkim.conf COPY run.sh /run.sh -RUN chmod +x /run.sh +COPY opendkim.sh /opendkim.sh +RUN chmod +x /run.sh /opendkim.sh -# Set up spool volume -VOLUME [ "/var/spool/postfix", "/etc/postfix" ] +# Set up volumes +VOLUME [ "/var/spool/postfix", "/etc/postfix", "/etc/opendkim/keys" ] # Run supervisord USER root WORKDIR /tmp EXPOSE 587 -ENTRYPOINT ["/run.sh"] +CMD ["/bin/sh", "-c", "/run.sh"] diff --git a/README.md b/README.md index 57a069e..af79dec 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,18 @@ To run the container, do the following: docker run --rm --name postfix -e "ALLOWED_SENDER_DOMAINS=example.com" -p 1587:587 boky/postfix ``` -You can now send emails by using `localhost:1587` as your SMTP server address. **Please note that -the image uses the submission (587) port by default**. Port 25 is not exposed on purpose, as it's -regularly blocked by ISP or already occupied by other services. +You can now send emails by using `localhost:1587` as your SMTP server address. Of course, if +you haven't configured your `example.com` domain to allow sending from this IP (see +[openspf](http://www.openspf.org/)), your emails will most likely be regarded as spam. All standard caveats of configuring the SMTP server apply -- e.g. you'll need to make sure your DNS entries are updated properly if you don't want your emails marked as spam. +**Please note that the image uses the submission (587) port by default**. Port 25 is not +exposed on purpose, as it's regularly blocked by ISP or already occupied by other services. + + + ## Configuration options The following configuration options are available: @@ -40,6 +45,7 @@ $RELAYHOST_PASSWORD = An (optional) login password for the relay server $MYNETWORKS = allow domains from per Network ( default 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ) $ALLOWED_SENDER_DOMAINS = domains sender domains $MASQUERADED_DOMAINS = domains where you want to masquerade internal hosts + ``` ### `HOSTNAME` @@ -132,6 +138,27 @@ Example: docker run --rm --name postfix -e "ALLOWED_SENDER_DOMAINS=example.com example.org" -e "MASQUERADED_DOMAINS=example.com" -p 1587:587 boky/postfix ``` +## `DKIM` + +**This image is equiped with support for DKIM.** If you want to use DKIM you will need to generate DKIM keys yourself. +You'll need to create a folder for every domain you want to send through Postfix and generate they key(s) with the following command, e.g. + +``` +mkdir -p /host/keys; cd /host/keys +mkdir example.com; cd example.com +opendkim-genkey -s mail -d example.com +cd .. +mkdir example.org; cd example.org +opendkim-genkey -s mail -d example.corg +``` + +`opendkim-genkey` is usually in your favourite distribution provided by installing `opendkim-tools` or `opendkim-utils`. + +Add the created `mail.txt` files to your DNS record. Afterwards, just mount `/etc/opendkim/keys` into your image and DKIM +will be used automatically, e.g.: +``` +docker run --rm --name postfix -e "ALLOWED_SENDER_DOMAINS=example.com example.org" -v /host/keys:/etc/opendkim/keys -p 1587:587 boky/postfix +``` ## Extending the image @@ -142,7 +169,7 @@ startup script. E.g.: create a custom `Dockerfile` like this: ``` FROM boky/postfix -MAINTAINER Some Randombloke "randombloke@example.com" +LABEL maintainer="Jack Sparrow " ADD Dockerfiles/additional-config.sh /docker-init.db/ ``` @@ -161,4 +188,4 @@ postconf -e "address_verify_negative_cache=yes" ## Security Postfix will run the master proces as `root`, because that's how it's designed. Subprocesses will run under the `postfix` account -which will use `UID:GID` of `100:101`. +which will use `UID:GID` of `100:101`. `opendkim` will run under account `102:103`. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..76a0584 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.6' +services: + postfix_test_587: + hostname: "postfix" + image: "boky/postfix" + restart: always + healthcheck: + test: [ "CMD", "sh", "-c", "netstat -an | fgrep 587 | fgrep -q LISTEN" ] + interval: 10s + timeout: 5s + start_period: 10s + retries: 2 + ports: + - "1587:587" + volumes: + - "./test-keys:/etc/opendkim/keys" + environment: + ALLOWED_SENDER_DOMAINS: "example.org" + INBOUND_DEBUGGING: 1 \ No newline at end of file diff --git a/opendkim.conf b/opendkim.conf new file mode 100644 index 0000000..f9540db --- /dev/null +++ b/opendkim.conf @@ -0,0 +1,20 @@ +AutoRestart Yes +AutoRestartRate 10/1h +UMask 002 +Syslog yes +SyslogSuccess Yes +LogWhy Yes + +Canonicalization relaxed/simple + +ExternalIgnoreList refile:/etc/opendkim/TrustedHosts +InternalHosts refile:/etc/opendkim/TrustedHosts +KeyTable refile:/etc/opendkim/KeyTable +SigningTable refile:/etc/opendkim/SigningTable + +Mode sv +PidFile /var/run/opendkim/opendkim.pid +SignatureAlgorithm rsa-sha256 + +UserID opendkim:opendkim +Socket inet:8891@localhost \ No newline at end of file diff --git a/opendkim.sh b/opendkim.sh new file mode 100644 index 0000000..1065402 --- /dev/null +++ b/opendkim.sh @@ -0,0 +1,10 @@ +#!/bin/sh +if [ ! -d /etc/opendkim/keys ]; then + sleep 9999999999999999999 +elif [ -z "$(find /etc/opendkim/keys -type d ! -name .)" ]; then + sleep 9999999999999999999 +else + /usr/sbin/opendkim -D -f -x /etc/opendkim/opendkim.conf +fi + + diff --git a/run.sh b/run.sh index 2905510..9140281 100644 --- a/run.sh +++ b/run.sh @@ -154,11 +154,22 @@ postconf -e "mynetworks=$MYNETWORKS" if [ ! -z "$INBOUND_DEBUGGING" ]; then echo -e "‣ $notice Enabling additional debbuging for: ${emphasis}$MYNETWORKS${reset}" postconf -e "debug_peer_list=$MYNETWORKS" + + sed -i -E 's/^[ \t]*#?[ \t]*LogWhy[ \t]*.+$/LogWhy yes/' /etc/opendkim/opendkim.conf + if ! egrep -q '^LogWhy' /etc/opendkim/opendkim.conf; then + echo >> /etc/opendkim/opendkim.conf + echo "LogWhy yes" >> /etc/opendkim/opendkim.conf + fi +else + sed -i -E 's/^[ \t]*#?[ \t]*LogWhy[ \t]*.+$/LogWhy no/' /etc/opendkim/opendkim.conf + if ! egrep -q '^LogWhy' /etc/opendkim/opendkim.conf; then + echo >> /etc/opendkim/opendkim.conf + echo "LogWhy no" >> /etc/opendkim/opendkim.conf + fi fi -# Split with space if [ ! -z "$ALLOWED_SENDER_DOMAINS" ]; then - echo -en "‣ $notice Setting up allowed SENDER domains:" + echo -en "‣ $info Setting up allowed SENDER domains:" allowed_senders=/etc/postfix/allowed_senders rm -f $allowed_senders $allowed_senders.db > /dev/null touch $allowed_senders @@ -179,7 +190,7 @@ if [ ! -z "$ALLOWED_SENDER_DOMAINS" ]; then # Since we are behind closed doors, let's just permit all relays. postconf -e "smtpd_relay_restrictions=permit" else - echo -e "ERROR: You need to specify sender domains otherwise Postfix will not run!" + echo -e "ERROR: You need to specify ALLOWED_SENDER_DOMAINS otherwise Postfix will not run!" exit 1 fi @@ -189,6 +200,50 @@ if [ ! -z "$MASQUERADED_DOMAINS" ]; then postconf -e "local_header_rewrite_clients = static:all" fi +DKIM_ENABLED= +if [ -d /etc/opendkim/keys ] && [ ! -z "$(find /etc/opendkim/keys -type d ! -name .)" ]; then + DKIM_ENABLED=", ${emphasis}opendkim${reset}" + echo -e "‣ $notice Configuring OpenDKIM." + mkdir -p /var/run/opendkim + chown -R opendkim:opendkim /var/run/opendkim + dkim_socket=$(cat /etc/opendkim/opendkim.conf | egrep ^Socket | awk '{ print $2 }') + if [ $(echo "$dkim_socket" | cut -d: -f1) == "inet" ]; then + dkim_socket=$(echo "$dkim_socket" | cut -d: -f2) + dkim_socket="inet:$(echo "$dkim_socket" | cut -d@ -f2):$(echo "$dkim_socket" | cut -d@ -f1)" + fi + echo -e " ...using socket $dkim_socket" + + postconf -e "milter_protocol=2" + postconf -e "milter_default_action=accept" + postconf -e "smtpd_milters=$dkim_socket" + postconf -e "non_smtpd_milters=$dkim_socket" + + echo > /etc/opendkim/TrustedHosts + echo > /etc/opendkim/KeyTable + echo > /etc/opendkim/SigningTable + + echo "127.0.0.1" >> /etc/opendkim/TrustedHosts + echo "localhost" >> /etc/opendkim/TrustedHosts + echo "" >> /etc/opendkim/TrustedHosts + if [ ! -z "$ALLOWED_SENDER_DOMAINS" ]; then + for i in $ALLOWED_SENDER_DOMAINS; do + private_key=/etc/opendkim/keys/$i/mail.private + if [ -f $private_key ]; then + echo -e " ...for domain ${emphasis}$i${reset}" + echo "*.$i" >> /etc/opendkim/TrustedHosts + echo "mail._domainkey.$i $i:mail:$private_key" >> /etc/opendkim/KeyTable + echo "*@$i mail._domainkey.$i" > /etc/opendkim/SigningTable + else + echo " ...$warn skipping for domain ${emphasis}$i${reset}. File $private_key not found!" + fi + done + fi +else + echo -e "‣ $info No DKIM keys found, will not use DKIM." + postconf -# smtpd_milters + postconf -# non_smtpd_milters +fi + # Use 587 (submission) sed -i -r -e 's/^#submission/submission/' /etc/postfix/master.cf @@ -202,6 +257,6 @@ if [ -d /docker-init.db/ ]; then done fi -echo -e "‣ $notice Staring ${emphasis}rsyslog${reset} and ${emphasis}postfix${reset}" +echo -e "‣ $notice Starting: ${emphasis}rsyslog${reset}, ${emphasis}postfix${reset}$DKIM_ENABLED" exec supervisord -c /etc/supervisord.conf diff --git a/supervisord.conf b/supervisord.conf index a755759..fb59eea 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -22,3 +22,16 @@ autorestart = false directory = /etc/postfix command = /usr/sbin/postfix -c /etc/postfix start startsecs = 0 + + +[program:opendkim] +command = /opendkim.sh +user = opendkim +autostart = true +autorestart = true +startsecs = 2 +stopwaitsecs = 2 +stdout_logfile = /dev/stdout +stderr_logfile = /dev/stderr +stdout_logfile_maxbytes = 0 +stderr_logfile_maxbytes = 0 diff --git a/test-keys/example.org/mail.private b/test-keys/example.org/mail.private new file mode 100644 index 0000000..959d3ea --- /dev/null +++ b/test-keys/example.org/mail.private @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQC/f+ltPdT/pAp0EjKSiR0m8KT3dNSoC2XSyK0viIHNwwx0IS/G +7Xkl1wR2vzIN+3EvXXeQtajJQ+55+xEUQlXa1RIStZmamCGEKNbXsgNhUBW5tlMJ +HMxiUbHQHDrv0rDl/9nqpEGeCGRMlWTM4HbQgXm9LWc4bKaDe7ByzmWWhwIDAQAB +AoGAfz9jwK9Bhc1fFNmyuSRbNrDZLYV4eWF5e6LVUayHdE6TS+dxc+IAimBiGvXZ +MOlPwIV/Ga1cRsDZCj0GkEuz99kCC4S59jb4N1d44y7NIzzdBn3dAWe1gYRB4gZF +NiBlt63zOUBw26j2PCN98GKzHl2u/rmpkI6CyUqws0zZ2MECQQD6LEqQcEYhQjm9 +4Ygu/CUm5QZ1lkMT9F9kiS4u5f/h67tQsAHjCPZSGH3wm2fmII0MfmcY4XxeV8DU +2fDg4hhZAkEAw/XEuCP5k3+S8Aw4rmkE1/JkIkQOOB0o5EAWHDlIOii/0FNGiB3B +EsRDsAmu5yz9Wy25lrgi3CHNXZRHlGlJ3wJBANzKmkQ/0qUnva5Hjdlpz6A0IS9G +Ho1N76LbL1dUxjCty/O6Yu/syPhLhVsEzP3dXJ7aMENVPl5FNHqmnmDbKoECQQCI +eodD6GpOPC/OV/SyJBpnfD4ZT2TH2Cc+sRO1HIAdYXwWT6lz66UouOCbnmLeRws9 +kZ8MRbqhrjklz7v6tUnpAkEAjCkM1+NWpXtdafH20kDXyZ/EEIYPoNKFJlCDuAhh +drB2AbWplr7U5q1Gx/r+TNHLj461L+Ys0WtvjQ+mi9p+OQ== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/test-keys/example.org/main.txt b/test-keys/example.org/main.txt new file mode 100644 index 0000000..6885b63 --- /dev/null +++ b/test-keys/example.org/main.txt @@ -0,0 +1,2 @@ +mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " + "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/f+ltPdT/pAp0EjKSiR0m8KT3dNSoC2XSyK0viIHNwwx0IS/G7Xkl1wR2vzIN+3EvXXeQtajJQ+55+xEUQlXa1RIStZmamCGEKNbXsgNhUBW5tlMJHMxiUbHQHDrv0rDl/9nqpEGeCGRMlWTM4HbQgXm9LWc4bKaDe7ByzmWWhwIDAQAB" ) ; ----- DKIM key mail for example.org diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..fdadfbc --- /dev/null +++ b/test.sh @@ -0,0 +1,32 @@ +#!/bin/sh +docker build . -t boky/postfix +docker-compose up -d + +# Wait for postfix to startup +echo "Waiting for startup..." +while ! docker ps | fgrep postfix_test_587 | grep -q healthy; do + sleep 1 +done + +cat <<"EOF" | nc -C localhost 1587 +HELO test +MAIL FROM:test@example.org +RCPT TO:check-auth@verifier.port25.com +DATA +Subject: Postfix message test +From: test@example.org +To: check-auth@verifier.port25.com +Content-Type: text/plain + +This is a simple text +. +QUIT +EOF + +# Wait for email to be delivered +echo "Waiting to shutdown..." +sleep 5 + +# Shut down tests +docker-compose down +