|
|
2 days ago | |
|---|---|---|
| .. | ||
| config | 2 days ago | |
| other/traefik | 2 days ago | |
| tmpfiles.d | 2 days ago | |
| Makefile | 2 days ago | |
| README.md | 2 days ago | |
| hooks.mk | 2 days ago | |
| mosquitto.container | 2 days ago | |
| mosquitto.image | 2 days ago | |
| mosquitto.target | 2 days ago | |
| overlay.bu | 2 days ago | |
README.md
Podman Quadlet: Mosquitto
Overview
Eclipse Mosquitto is a lightweight MQTT broker. This cookbook
runs it as a Podman Quadlet to serve as the MQTT backbone of a home-automation stack
(Home Assistant, zigbee2mqtt, ESPHome, ...).
This cookbook:
- Runs Mosquitto as a rootless container (UID 10033) with minimal privileges.
- Binds a plain-MQTT listener on loopback only (
127.0.0.1:1883). Nothing listens on the LAN by default. - Terminates TLS at Traefik (port 443, demultiplexed by SNI), not inside the broker —
it depends on the
traefikcookbook. - Refuses anonymous access; authentication and topic authorization come from operator-provided files.
- Stores retained messages and client sessions on the virtiofs precious-data mount
(
/var/lib/virtiofs/data/mosquitto/), so the host's ZFS snapshots and backups pick them up. - Logs everything to stdout, so
journalctlis the single place to look. - Supports automatic container image updates via Podman auto-update.
The image is docker.io/library/eclipse-mosquitto:2, a multi-architecture manifest
(amd64 + arm64, verified 2026-09-19, version 2.1.2), as required by the aarch64 target.
Prerequisites
- The
traefikcookbook must be installed and running (installed automatically as a dependency). - The virtiofs data mount
/var/lib/virtiofs/datamust be present (provided by thefcos-with-virtiofstemplate / thebasecookbook). - The operator must provide a password file and an ACL file (see below). Until the
password file exists, the broker is intentionally not started (the units carry a
ConditionPathExists=/etc/quadlets/mosquitto/passwdguard).
Files the operator must provide
Both files are provided by the operator (e.g. an Ansible role from a vault) and are not
part of the package. Working examples ship under config/examples/ for development only.
| Path | Mode | Owner | Purpose |
|---|---|---|---|
/etc/quadlets/mosquitto/passwd |
0640 | 10033:10000 |
MQTT username/password database |
/etc/quadlets/mosquitto/acl |
0640 | 10033:10000 |
Per-user topic authorization |
Site-specific Traefik routing (real SNI host name + certificate resolver) is injected
downstream as an overlay into /etc/quadlets/traefik/conf.d/ — see
TLS at Traefik below. No site value is baked into this cookbook.
Generating a password entry
Use mosquitto_passwd (available inside the broker image, or from the mosquitto-clients
package):
# Create/replace the file and add the first user:
mosquitto_passwd -c -b /etc/quadlets/mosquitto/passwd homeassistant 'S3cr3t!'
# Add more users (omit -c so the file is not truncated):
mosquitto_passwd -b /etc/quadlets/mosquitto/passwd zigbee2mqtt 'An0th3r!'
Then make sure ownership and mode are correct:
chown 10033:10000 /etc/quadlets/mosquitto/passwd
chmod 0640 /etc/quadlets/mosquitto/passwd
The ACL file uses Mosquitto's ACL syntax; see config/examples/acl for a starting point.
Because acl_file is set and anonymous access is refused, access is default-deny: an
authenticated user can only touch the topics granted to it.
TLS at Traefik
The broker speaks plain MQTT on loopback. Traefik terminates TLS on port 443 and
demultiplexes connections by SNI, forwarding the plaintext stream to 127.0.0.1:1883. This
is expressed as a Traefik TCP router (not an HTTP one), shipped as a hook fragment in
other/traefik/mosquitto.yaml and installed to /etc/quadlets/traefik/conf.d/mosquitto.yaml
during development.
What this implies for clients:
-
All clients are remote and connect over TLS on port 443 using the broker's SNI host name (e.g.
mqtt.itix.frin production,mqttin the shipped example), not to port 1883. Example round trip through Traefik:mosquitto_sub --cafile /path/to/ca.crt -h mqtt -p 443 -u homeassistant -P '…' -t 'test/#' & mosquitto_pub --cafile /path/to/ca.crt -h mqtt -p 443 -u homeassistant -P '…' -t 'test/x' -m hello -
Credentials travel in clear text between Traefik and the broker over the loopback interface (Traefik has already terminated TLS). This is acceptable because loopback traffic never leaves the host, but it is stated here so it is not a surprise.
-
The shipped fragment uses a bare
tls: {}, which makes Traefik serve its own self-signed certificate — good enough to test the path end-to-end. In production the operator overlays a fragment with the real SNI host name andcertResolver: le(the certificate resolver is a site value and is kept commented out in the package).
A TCP router whose HostSNI is not `*` requires a tls section, a TCP service uses
address: (never url:), and this router coexists on the https entry point with the HTTP
routers of the other cookbooks, which match HostSNI(*) implicitly.
Making the broker listen on the LAN (optional)
If an operator nevertheless wants the broker to accept connections directly on the LAN, they
can drop a fragment such as listener 8883 0.0.0.0 (plus its own TLS/authentication settings)
into /etc/quadlets/mosquitto/conf.d/. This cookbook ships no LAN listener and no firewall
rules.
Ports
| Port | Bind | Purpose |
|---|---|---|
| 1883 | 127.0.0.1 |
Plain MQTT listener (loopback only) |
| 443 | Traefik | TLS-terminated MQTT ingress (SNI-routed by Traefik) |
Port 1883 is above 1024, so the non-root container binds it without any capability.
UID and GID
- User:
mosquitto, UID 10033 - Group:
itix-svc, GID 10000
Health check
The health probe is a bare TCP connect to the listener (nc -z 127.0.0.1 1883). It is
preferred over a credentialed mosquitto_sub on $SYS/# because it needs no secret —
the operator's password file may legitimately not be in place yet, and the probe must not
depend on it. As a side effect, each probe produces a benign New connection /
disconnected: connection closed by client log pair in the journal; raise HealthInterval
or set connection_messages false in a conf.d fragment if that noise is unwanted.
Deviations from the repository conventions
- No TLS inside the container and no
legodependency. TLS is Traefik's job, mirroring the production montage (TraefikHostSNI+certResolver: lein front of port 1883). The Traefik hook is a TCP router rather than the usual HTTP one. - The whole
/etc/quadlets/mosquittodirectory is mounted read-only into the container (at the same path) rather than mounting individual files, so thatmosquitto.conf, theconf.d/fragments and the operator-providedpasswd/aclall appear without enumerating them, and host and container paths stay identical. - Health check is an unauthenticated TCP connect (see above), not the credentialed
$SYS/#subscription some deployments use. - The
conf.d/directory is packaged as an empty directory because Mosquitto'sinclude_dirfails if the directory is missing (verified against 2.1.2; it is also non-recursive and only reads*.conf).
Usage
In a separate terminal, follow the logs.
sudo make tail-logs
Install the Podman Quadlets and start Mosquitto (pulls in traefik).
sudo make clean install
Verify the broker is listening on loopback (and nowhere else):
ss -ltnp | grep 1883
Publish/subscribe round trip with a password (using the shipped development credentials):
mosquitto_sub -h 127.0.0.1 -p 1883 -u homeassistant -P homeassistant -t 'test/#' &
mosquitto_pub -h 127.0.0.1 -p 1883 -u homeassistant -P homeassistant -t 'test/x' -m hello
Anonymous access must be refused:
mosquitto_pub -h 127.0.0.1 -p 1883 -t 'test/x' -m nope # -> Connection Refused: not authorised.
Finally, remove the quadlets, their configuration and their data.
sudo make I_KNOW_WHAT_I_AM_DOING=yes uninstall clean