git » chasquid » commit e98464c

docker: Add Dockerfile for running chasquid+dovecot+letsencrypt

author Alberto Bertogli
2018-06-04 22:33:49 UTC
committer Alberto Bertogli
2019-08-12 00:21:41 UTC
parent a7d49792f53612423fc92f47bd8998921fb81229

docker: Add Dockerfile for running chasquid+dovecot+letsencrypt

This patch adds a new docker directory, which contains a Dockerfile plus
some additional configuration for creating a container that runs
chasquid+dovecot+letsencrypt.

It also updates the gitlab CI pipeline to automatically build and
publish an image on each commit.

This is experimental and likely to break.

.gitlab-ci.yml +12 -0
.mkdocs.yml +1 -0
docker/Dockerfile +84 -0
docker/README.md +84 -0
docker/add-user.sh +43 -0
docker/chasquid.conf +26 -0
docker/dovecot.conf +134 -0
docker/entrypoint.sh +105 -0
docs/docker.md +1 -0

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8be0f5e..b6345a0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,7 @@
 
 stages:
   - test
+  - docker_image
 
 integration_test:
   stage: test
@@ -12,3 +13,14 @@ integration_test:
     - docker build -t chasquid-test -f test/Dockerfile .
     - docker run chasquid-test  env
     - docker run chasquid-test  make test
+
+image_build:
+  stage: docker_image
+  image: docker:stable
+  services:
+    - docker:dind
+  script:
+    - docker info
+    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+    - docker build -t $CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME -f docker/Dockerfile .
+    - docker push $CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME
diff --git a/.mkdocs.yml b/.mkdocs.yml
index 436e6ac..99e486d 100644
--- a/.mkdocs.yml
+++ b/.mkdocs.yml
@@ -25,6 +25,7 @@ nav:
     - hooks.md
     - dovecot.md
     - dkim.md
+    - docker.md
     - flow.md
     - monitoring.md
     - sec-levels.md
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..cb82700
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,84 @@
+# Docker file for creating a container that will run chasquid and Dovecot.
+#
+# THIS IS EXPERIMENTAL AND LIKELY TO CHANGE.
+#
+# This is not recommended for serious installations, you're probably better
+# off following the documentation and setting the server up manually.
+#
+# See the README.md file for more details.
+
+# Build the binaries.
+FROM golang:latest as build
+WORKDIR /go/src/blitiri.com.ar/go/chasquid
+COPY . .
+RUN go get -d ./...
+RUN go install ./...
+
+# Create the image.
+FROM debian:stable
+
+# Make debconf/frontend non-interactive, to avoid distracting output about the
+# lack of $TERM.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install the packages we need.
+# This includes chasquid, which sets up good defaults.
+RUN apt-get update -q
+RUN apt-get install -y -q \
+	chasquid \
+	dovecot-lmtpd dovecot-imapd dovecot-pop3d \
+	dovecot-sieve dovecot-managesieved \
+	acl sudo certbot
+
+# Copy the binaries. This overrides the debian packages with the ones we just
+# built above.
+COPY --from=build /go/bin/chasquid /usr/bin/
+COPY --from=build /go/bin/chasquid-util /usr/bin/
+COPY --from=build /go/bin/smtp-check /usr/bin/
+COPY --from=build /go/bin/mda-lmtp /usr/bin/
+
+# Let chasquid bind privileged ports, so we can run it as its own user.
+RUN setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/chasquid
+
+# Copy docker-specific configurations.
+COPY docker/dovecot.conf /etc/dovecot/dovecot.conf
+COPY docker/chasquid.conf /etc/chasquid/chasquid.conf
+
+# Copy utility scripts.
+COPY docker/add-user.sh /
+COPY docker/entrypoint.sh /
+
+# chasquid: SMTP, submission, submission+tls.
+EXPOSE 25 465 587
+
+# dovecot: POP3s, IMAPs, managesieve.
+EXPOSE 993 995 4190
+
+# http for letsencrypt/certbot.
+EXPOSE 80 443
+
+# Store emails and chasquid databases in an external volume, to be mounted at
+# /data, so they're independent from the image itself.
+VOLUME /data
+
+# Put some directories where we expect persistent user data into /data.
+RUN rmdir /etc/chasquid/domains/
+RUN ln -sf /data/chasquid/domains/ /etc/chasquid/domains
+RUN rm -rf /etc/letsencrypt/
+RUN ln -sf /data/letsencrypt/ /etc/letsencrypt
+
+# Give the chasquid user access to the necessary configuration.
+RUN setfacl -R -m u:chasquid:rX /etc/chasquid/
+RUN mv /etc/chasquid/certs/ /etc/chasquid/certs-orig
+RUN ln -s /etc/letsencrypt/live/ /etc/chasquid/certs
+
+
+# NOTE: Set AUTO_CERTS="example.com example.org" to automatically obtain and
+# renew certificates upon startup, via Letsencrypt. You're agreeing to their
+# ToS by setting this variable, so please review them carefully.
+# CERTS_EMAIL should be set to your email address so letsencrypt can send you
+# critical notifications.
+
+# Custom entry point that does some configuration checks and ensures
+# letsencrypt is properly set up.
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000..18fb61f
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,84 @@
+
+# Docker
+
+chasquid comes with a Dockerfile to create a container running [chasquid],
+[dovecot], and managed certificates with [Let's Encrypt].
+
+**IT IS EXPERIMENTAL AND LIKELY TO BREAK**
+
+The more traditional setup is **highly recommended**, see the
+[how-to](howto.md) documentation for more details.
+
+[chasquid]: https://blitiri.com.ar/p/chasquid
+[dovecot]: https://dovecot.org
+[Let's Encrypt]: https://letsencrypt.org
+
+
+## Images
+
+There are [pre-built images at gitlab
+registry](https://gitlab.com/albertito/chasquid/container_registry).  They are
+automatically built, and tagged with the corresponding branch name. Use the
+*master* tag for a stable version.
+
+If, instead, you want to build the image yourself, just run:
+
+```sh
+$ docker build -t chasquid -f docker/Dockerfile .
+```
+
+
+## Running
+
+First, pull the image into your target machine:
+
+```sh
+$ docker pull registry.gitlab.com/albertito/chasquid:master
+```
+
+You will need a data volume to store persistent data, outside the image. This
+will contain the mailboxes, user databases, etc.
+
+```sh
+$ docker volume create chasquid-data
+```
+
+To add your first user to the image:
+
+```
+$ docker run \
+	--mount source=chasquid-data,target=/data \
+	-it --entrypoint=/add-user.sh \
+	registry.gitlab.com/albertito/chasquid:master
+Email (full user@domain format): pepe@example.com
+Password:
+pepe@example.com added to /data/dovecot/users
+```
+
+Upon startup, the image will obtain a TLS certificate for you using [Let's
+Encrypt](https://letsencrypt.com/). You need to tell it the domain(s) to get a
+certificate from by setting the `AUTO_CERTS` variable.
+
+Because certificates expire, you should restart the container every week or
+so. Certificates will be renewed automatically upon startup if needed.
+
+In order for chasquid to get access to the source IP address, you will need to
+use host networking, or create a custom docker network that does IP forwarding
+and not proxying.
+
+Finally, start the container:
+
+```sh
+$ docker run -e AUTO_CERTS=mail.yourdomain.com \
+	--mount source=chasquid-data,target=/data \
+	--network host \
+	registry.gitlab.com/albertito/chasquid:master
+```
+
+
+## Debugging
+
+To get a shell on the running container for debugging, you can use `docker ps`
+to find the container ID, and then `docker exec -it CONTAINERID /bin/bash` to
+open a shell on the running container.
+
diff --git a/docker/add-user.sh b/docker/add-user.sh
new file mode 100755
index 0000000..83ee3a8
--- /dev/null
+++ b/docker/add-user.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+#
+# Creates a user. If it exists, updates the password.
+#
+# Note this is not robust, it's only for convenience on extremely simple
+# setups.
+
+set -e
+
+read -p "Email (full user@domain format): " EMAIL
+
+if ! echo "${EMAIL}" | grep -q @; then
+	echo "Error: email should have '@'."
+	exit 1
+fi
+
+
+read -p "Password: " -s PASSWORD
+echo
+
+DOMAIN=$(echo echo "${EMAIL}" | cut -d '@' -f 2)
+
+
+# If the domain doesn't exist in chasquid's config, create it.
+mkdir -p "/data/chasquid/domains/${DOMAIN}/"
+
+
+# Encrypt password.
+ENCPASS=$(doveadm pw -u "${EMAIL}" -p "${PASSWORD}")
+
+# Edit dovecot users: remove user if it exits.
+mkdir -p /data/dovecot
+if grep -q "^${EMAIL}:" /data/dovecot/users; then
+	cp /data/dovecot/users /data/dovecot/users.old
+	cat /data/dovecot/users.old | grep -v "^${EMAIL}:" \
+		> /data/dovecot/users
+fi
+
+# Edit dovecot users: add user.
+echo "${EMAIL}:${ENCPASS}::::" >> /data/dovecot/users
+
+echo "${EMAIL} added to /data/dovecot/users"
+
diff --git a/docker/chasquid.conf b/docker/chasquid.conf
new file mode 100644
index 0000000..20f6aa2
--- /dev/null
+++ b/docker/chasquid.conf
@@ -0,0 +1,26 @@
+
+# Listening addresses.
+smtp_address: ":25"
+submission_address: ":587"
+submission_over_tls_address: ":465"
+
+# Monitoring HTTP server only bound to localhost, just in case.
+monitoring_address: "127.0.0.1:1099"
+
+# Auth against dovecot.
+dovecot_auth: true
+
+# Use mda-lmtp to talk to dovecot.
+mail_delivery_agent_bin: "/usr/bin/mda-lmtp"
+mail_delivery_agent_args: "--addr"
+mail_delivery_agent_args: "/run/dovecot/lmtp"
+mail_delivery_agent_args: "-f"
+mail_delivery_agent_args: "%from%"
+mail_delivery_agent_args: "-d"
+mail_delivery_agent_args: "%to%"
+
+# Store data in the container volume.
+data_dir: "/data/chasquid/data"
+
+# Mail log to the container volume.
+mail_log_path: "/data/chasquid/mail.log"
diff --git a/docker/dovecot.conf b/docker/dovecot.conf
new file mode 100644
index 0000000..cd07b73
--- /dev/null
+++ b/docker/dovecot.conf
@@ -0,0 +1,134 @@
+
+#
+# Logging
+#
+log_path = /data/dovecot/dovecot.log
+
+#
+# Email storage
+#
+
+# Store emails in /data/mail/home/domain/user, which will be inside the
+# container's volume.
+mail_home = /data/mail/home/%d/%n
+
+# Use Dovecot's native format.
+mail_location = mdbox:~/mdbox
+
+# User and group used to store and access mailboxes.
+mail_uid = dovecot
+mail_gid = dovecot
+
+# As we're using virtual mailboxes, the system user will be "dovecot", which
+# has uid in the 100-500 range. By default using uids <500 is blocked, so we
+# need to explicitly lower the value to allow storage of mail as "dovecot".
+first_valid_uid = 100
+first_valid_gid = 100
+
+#
+# Authentication
+#
+
+# Static file, in /data/dovecot/users.
+auth_mechanisms = plain
+passdb {
+  driver = passwd-file
+  args = scheme=CRYPT username_format=%u /data/dovecot/users
+}
+userdb {
+  driver = passwd-file
+  args = /data/dovecot/users
+}
+
+
+#
+# TLS
+#
+
+# TLS is mandatory.
+# The entrypoint generates auto-ssl.conf, with all the certificates.
+ssl = required
+!include_try /etc/dovecot/auto-ssl.conf
+
+# Only allow TLS 1.2 and up.
+ssl_min_protocol = TLSv1.2
+
+
+#
+# Protocols
+#
+protocols = lmtp imap pop3 sieve
+
+#
+# IMAP
+#
+service imap-login {
+  inet_listener imap {
+    # Disable plain text IMAP, just in case.
+    port = 0
+  }
+  inet_listener imaps {
+    port = 993
+    ssl = yes
+  }
+}
+
+service imap {
+}
+
+#
+# POP3
+#
+service pop3-login {
+  inet_listener pop3 {
+    # Disable plain text POP3, just in case.
+    port = 0
+  }
+  inet_listener pop3s {
+    port = 995
+    ssl = yes
+  }
+}
+
+service pop3 {
+}
+
+#
+# Sieve/managesieve
+#
+service managesieve-login {
+}
+service managesieve {
+}
+protocol sieve {
+}
+plugin {
+  sieve = file:~/sieve;active=~/.dovecot.sieve
+}
+
+#
+# Internal services
+#
+service auth {
+  unix_listener auth-userdb {
+  }
+
+  # Grant chasquid access to request user authentication.
+  unix_listener auth-chasquid-userdb {
+    mode = 0660
+    user = chasquid
+  }
+  unix_listener auth-chasquid-client {
+    mode = 0660
+    user = chasquid
+  }
+}
+service auth-worker {
+}
+dict {
+}
+service lmtp {
+  # This is used by mda-lmtp.
+  unix_listener lmtp {
+  }
+}
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100755
index 0000000..c383119
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+#
+# Script that is used as a Docker entrypoint.
+#
+
+set -e
+
+if ! grep -q data /proc/mounts; then
+	echo "/data is not mounted."
+	echo "Check that the /data volume is set up correctly."
+	exit 1
+fi
+
+# Create the directory structure if it's not there.
+# Some of these directories are symlink targets, see the Dockerfile.
+mkdir -p /data/chasquid
+mkdir -p /data/letsencrypt
+mkdir -p /data/chasquid
+mkdir -p /data/chasquid/domains
+mkdir -p /data/dovecot
+
+# Set up the certificates for the requested domains.
+if [ "$AUTO_CERTS" != "" ]; then
+	# If we were given an email to use for letsencrypt, use it. Otherwise
+	# continue without one.
+	MAIL_OPTS="--register-unsafely-without-email"
+	if [ "$CERTS_MAIL" != "" ]; then
+		MAIL_OPTS="-m $CERTS_MAIL"
+	fi
+
+	for DOMAIN in $(echo $AUTO_CERTS); do
+		# If it has never been set up, then do so.
+		if ! [ -e /etc/letsencrypt/live/$DOMAIN/fullchain.pem ]; then
+			certbot certonly \
+				--non-interactive \
+				--standalone \
+				--agree-tos \
+				$MAIL_OPTS \
+				-d $DOMAIN
+		else
+			echo "$DOMAIN certificate already set up."
+		fi
+	done
+
+	# Renew on startup, since the container won't have cron facilities.
+	# Note this requires you to restart every week or so, to make sure
+	# your certificate does not expire.
+	certbot renew
+fi
+
+CERT_DOMAINS=""
+for i in $(ls /etc/letsencrypt/live/); do
+	if [ -e "/etc/letsencrypt/live/$i/fullchain.pem" ]; then
+		CERT_DOMAINS="$CERT_DOMAINS $i"
+	fi
+done
+
+# We need one domain to use as a default - pick the last one.
+ONE_DOMAIN=$i
+
+# Check that there's at least once certificate at this point.
+if [ "$CERT_DOMAINS" == "" ]; then
+	echo "No certificates found."
+	echo
+	echo "Set AUTO_CERTS='example.com' to automatically get one."
+	exit 1
+fi
+
+# Give chasquid access to the certificates.
+# Dovecot does not need this as it reads them as root.
+setfacl -R -m u:chasquid:rX /etc/letsencrypt/{live,archive}
+
+# Give chasquid access to the data directory.
+mkdir -p /data/chasquid/data
+chown -R chasquid /data/chasquid/
+
+# Give dovecot access to the mailbox home.
+mkdir -p /data/mail/
+chown dovecot:dovecot /data/mail/
+
+# Generate the dovecot ssl configuration based on all the certificates we have.
+# The default goes first because dovecot complains otherwise.
+echo "# Autogenerated by entrypoint.sh" > /etc/dovecot/auto-ssl.conf
+cat >> /etc/dovecot/auto-ssl.conf <<EOF
+ssl_cert = </etc/letsencrypt/live/$ONE_DOMAIN/fullchain.pem
+ssl_key = </etc/letsencrypt/live/$ONE_DOMAIN/privkey.pem
+EOF
+for DOMAIN in $CERT_DOMAINS; do
+	echo "local_name $DOMAIN {"
+        echo "  ssl_cert = </etc/letsencrypt/live/$DOMAIN/fullchain.pem"
+        echo "  ssl_key = </etc/letsencrypt/live/$DOMAIN/privkey.pem"
+	echo "}"
+done >> /etc/dovecot/auto-ssl.conf
+
+# Pick the default domain as default hostname for chasquid. This is only used
+# in plain text sessions and on very rare cases, and it's mostly for aesthetic
+# purposes.
+echo "hostname: '$ONE_DOMAIN'" >> /etc/chasquid/chasquid.conf
+
+
+# Start the services: dovecot in background, chasquid in foreground.
+start-stop-daemon --start --quiet --pidfile /run/dovecot.pid \
+	--exec /usr/sbin/dovecot -- -c /etc/dovecot/dovecot.conf
+
+sudo -u chasquid -g chasquid  /usr/bin/chasquid  $CHASQUID_FLAGS
diff --git a/docs/docker.md b/docs/docker.md
new file mode 120000
index 0000000..f13766e
--- /dev/null
+++ b/docs/docker.md
@@ -0,0 +1 @@
+../docker/README.md
\ No newline at end of file