Howto: Using LE from Traefik to protect Mosquitto docker container

I want to protect my mosquitto container with certificates preferably not using self-signed certificates. Therefore i have the question if it would be possible with Traefik to extract the MQTT certificates from the acme.json (cafile/certfile/keyfile) and save it to a different docker volume upon changes (probably using a bash script on the docker host)

For MQTT i have the following settings i need to set:

cafile
certfile
keyfile

As ca-file i probably need this one: https://letsencrypt.org/certs/trustid-x3-root.pem.txt
and certfile & keyfile map to the string contents of the corresponding certificate and keyfile sections in acme.json if i am correct.

The copy step is needed because i think mosquitto needs to load the certificates in order to ensure WSS and MQTTS protocols. I prefer all external connections (mqtt.mydomain.com) to be TLS encrypted. Locally/internally i don't need certificates.

Or is there a better/alternative way to do this?

I've fixed it, here is the info for others who are trying to do this; It consists of a few parts:

  • Working Docker container with traefik v2.02
  • Working Docker container with Eclipse-mosquitto
  • Some bash scripting knowledge
  • 2 port forwards in your firewall.

(note: below solution does not enforce user/password connection yet; i first wanted to get SSL/TLS working)

1 - Working Docker container with traefik v2.02
Obviously out of scope for this reply.

2 - Working Docker container with Eclipse-mosquitto
This is the part of the docker-compose file containing Eclipse-mosquitto:

  network-mosquitto:
    image: eclipse-mosquitto
    container_name: network-mosquitto
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=999
    networks:
      f43lan:
        ipv4_address: 192.168.1.39
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - network-mosquitto-config:/mosquitto/config
      - network-mosquitto-data:/mosquitto/data
      - network-mosquitto-log:/mosquitto/log
    labels:
      # Enable traefik
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"

      # Router: Plex-specific
      - "traefik.http.routers.network-mosquitto.rule=Host(`mqtt.at.domain.com`)"
      - "traefik.http.routers.network-mosquitto.entrypoints=websecure"
      - "traefik.http.routers.network-mosquitto.tls=true"
      - "traefik.http.routers.network-mosquitto.tls.certresolver=leresolver"

The volume network-mosquitto-config is a named volume which maps to /var/lib/docker/volumes/network-mosquitto-config/.

My mosquitto configuration:

# Plain MQTT protocol
listener 1883

# MQTT over TLS/SSL
listener 8883
cafile /mosquitto/config/certs/trustid-x3-root.pem
certfile /mosquitto/config/certs/certificate.crt
keyfile /mosquitto/config/certs/privatekey.key

# Plain WebSockets configuration
listener 9001
protocol websockets

# WebSockets over TLS/SSL
listener 9883
protocol websockets
cafile /mosquitto/config/certs/trustid-x3-root.pem
certfile /mosquitto/config/certs/certificate.crt
keyfile /mosquitto/config/certs/privatekey.key

3 - Some bash scripting knowledge
With the addition of the following self-written script, i've managed to get Lets encrypt working. This script runs daily (at night) as a cronjob.

#!/bin/bash

#----------------------------------------------------------------------------------------------------
# Variables
#----------------------------------------------------------------------------------------------------

# Location of the 'traefik-certs-dumper' script, written by LDEZ <https://github.com/ldez/traefik-certs-dumper>
CERT_DUMP_SCRIPT="/mnt/nas/docker/scripts/traefik-certs-dumper/traefik-certs-dumper"

# Temp directory to write all exported certificates to (unfortunately the script from ldez doesn't support to export 'just one' domain.) - will be removed at the end of the script.
CERT_DUMP_DIR="/mnt/nas/docker/scripts/traefik-certs-dumper/tmp"

# Location of the Traefik generated acme.json file
TRAEFIK_ACME_FILE="/var/lib/docker/volumes/core-traefik-acme/_data/acme.json"

# File format version of Traefik to use (i am running v2.0.2 when i wrote this script, hence i need to pass 'v2')
TRAEFIK_ACME_VERSION="v2"

# My docker container and hostname to look for
MQTT_DOCKER_CONTAINER="network-mosquitto"
MQTT_HOSTNAME="mqtt.at.domain.com"

# The location of where to save the certificates
MQTT_CERT_DIR="/var/lib/docker/volumes/network-mosquitto-config/_data/certs"

# Ensure the certificates eventually have this file ownership
MQTT_CERT_OWNERSHIP="docker:users"



#----------------------------------------------------------------------------------------------------
# Start script (no changed needed below)
#----------------------------------------------------------------------------------------------------

# Check if certificate dump script exists
if [ ! -f "$CERT_DUMP_SCRIPT" ]; then
	echo "ERROR: The 'traefik-certs-dumper' script doesn't exist: $CERT_DUMP_SCRIPT."
	exit 1
fi

# Check if the Traefik ACME file exists
if [ ! -f "$TRAEFIK_ACME_FILE" ]; then
	echo "ERROR: The Traefik acme file doesn't exist: $TRAEFIK_ACME_FILE."
	exit 1
fi

# Check if container exists
if [[ $(docker ps --filter "name=^/$MQTT_DOCKER_CONTAINER$" --format '{{.Names}}') != $MQTT_DOCKER_CONTAINER ]]; then
	echo "ERROR: The docker container doesn't seem to exist: $MQTT_DOCKER_CONTAINER."
	exit 1
fi


# Check if certificate directory exists
if [ ! -d "$MQTT_CERT_DIR" ]; then
	echo "ERROR: The location of the certificates doesn't seem to exist: $MQTT_CERT_DIR."
	exit 1
fi

# Run the certificate dump script
$CERT_DUMP_SCRIPT file --source $TRAEFIK_ACME_FILE --domain-subdir=true --version $TRAEFIK_ACME_VERSION --dest $CERT_DUMP_DIR >> /dev/null 2>&1
ERROR=$?
if [ $ERROR -eq 0 ]; then
	# Verify the certificate and the key
	CERT_EXPORTED=$CERT_DUMP_DIR/$MQTT_HOSTNAME/certificate.crt
	if [ ! -f "$CERT_EXPORTED" ]; then
		echo "ERROR: Unable to find the configured certificate in the export: $CERT_EXPORTED."
		ERROR=1
	fi

	KEY_EXPORTED=$CERT_DUMP_DIR/$MQTT_HOSTNAME/privatekey.key
	if [ ! -f "$KEY_EXPORTED" ]; then
		echo "ERROR: Unable to find the configured privatekey in the export: $KEY_EXPORTED."
		ERROR=1
	fi

	# Can we still continue?
	if [ $ERROR -eq 0 ]; then
		CERT_EXISTING=$MQTT_CERT_DIR/certificate.crt
		CERT_COPY=0
		if [ ! -f "$CERT_EXISTING" ]; then
			# Copy the exported certificate to the MQTT certificate directory if it doesn't exist yet.
			echo "Notice: Copying exported certificate since certificate doesn't exist yet..."
			CERT_COPY=1
		else
			# Verify if the exported certificate needs to be copied to the MQTT certificate directory
			diff --binary --brief $CERT_EXPORTED $CERT_EXISTING > /dev/null 2>&1
			if [ $? -eq 1 ]; then
				echo "Notice: Updating existing certificate since exported certificate is different..."
				CERT_COPY=1
			fi
		fi

		if [ $CERT_COPY -eq 1 ]; then
			echo "- Copying $CERT_EXPORTED to $CERT_EXISTING ..."
			cp $CERT_EXPORTED $CERT_EXISTING

			echo "- Updating file ownership of $CERT_EXISTING ..."
			chown $MQTT_CERT_OWNERSHIP $CERT_EXISTING
		fi

		KEY_EXISTING=$MQTT_CERT_DIR/privatekey.key
		KEY_COPY=0
		if [ ! -f "$KEY_EXISTING" ]; then
			# Copy the exported privatekey to the MQTT certificate directory if it doesn't exist yet.
			echo "Notice: Copying exported privatekey since privatekey doesn't exist yet..."
			KEY_COPY=1
		else
			# Verify if the exported privatekey needs to be copied to the MQTT certificate directory
			diff --binary --brief $KEY_EXPORTED $KEY_EXISTING > /dev/null 2>&1
			if [ $? -eq 1 ]; then
				echo "Notice: Updating existing privatekey since exported privatekey is different..."
				KEY_COPY=1
			fi
		fi

		if [ $KEY_COPY -eq 1 ]; then
			echo "- Copying $KEY_EXPORTED to $KEY_EXISTING ..."
			cp $KEY_EXPORTED $KEY_EXISTING

			echo "- Updating file ownership of $KEY_EXISTING ..."
			chown $MQTT_CERT_OWNERSHIP $KEY_EXISTING
		fi

		# If we did something, then...
		if [[ $CERT_COPY -eq 1 || $KEY_COPY -eq 1 ]]; then
			# Check if container is running (to determine automatic restart)
			if [[ $(docker inspect --format '{{.State.Running}}' $MQTT_DOCKER_CONTAINER) == "true" ]]; then
				echo "Sending RESTART to $MQTT_DOCKER_CONTAINER ..."
				RET=$(docker container restart $MQTT_DOCKER_CONTAINER)

				# Note: sending a SIGHUP would be better, but this causes the logfile to show: "Reloading config." followed by "Error: Unable to open config file /mosquitto/config/mosquitto.conf." in my eclipse-docker container.
				#echo "- Sending SIGHUP to $MQTT_DOCKER_CONTAINER ..."
				#RET=$(docker kill --signal=HUP $MQTT_DOCKER_CONTAINER)
			fi
		fi
	fi

	# Remove the exported certificate directory
	[ -d $CERT_DUMP_DIR ] && rm -rf $CERT_DUMP_DIR
fi

exit $ERROR

4 - Two port forwards in your firewall
Then all you need to do is forward port 8883 and 9883 in your UniFi security gateway or firewall / router towards the IP running your broker (in my case 192.168.1.39) and you should be able to connect via MQTTS and WSS protocols using a validated certificate.

2 Likes

I am on the same journey: docker, traefik, grafana and google auth is the goal. I am glad, I found your topic :slight_smile:

Although there are still some obstacles, I thing I will use the docker image of the cert dumper with watch-option. Look here: traefik-certs-dumper/docker-compose-traefik-v2.yml at master · ldez/traefik-certs-dumper · GitHub

Well 3 uses the cert-dumper and restarts the docker service for which the certificate has been updated if needed.