author | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-06-04 22:33:49 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2019-08-12 00:21:41 UTC |
parent | a7d49792f53612423fc92f47bd8998921fb81229 |
.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