diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx
index 02a8700d..7c5147f0 100644
--- a/ZB.MOM.WW.OtOpcUa.slnx
+++ b/ZB.MOM.WW.OtOpcUa.slnx
@@ -108,6 +108,7 @@
+
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/.gitignore b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/.gitignore
new file mode 100644
index 00000000..b69c261a
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/.gitignore
@@ -0,0 +1,14 @@
+# Fixture credential + TLS material produced by gen-fixture-material.sh.
+#
+# NEVER commit anything in here: it holds the broker password hash, the fixture CA's
+# PRIVATE key and the server's private key. The generator script is the artifact in
+# git; its output is regenerated per machine.
+secrets/
+
+# Belt-and-braces: catch key/cert material written directly into this directory by a
+# hand-run openssl command that forgot the secrets/ prefix.
+*.key
+*.crt
+*.csr
+*.srl
+passwd
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/docker-compose.yml b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/docker-compose.yml
new file mode 100644
index 00000000..a1ecfec7
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/docker-compose.yml
@@ -0,0 +1,77 @@
+# MQTT integration-test fixture — Eclipse Mosquitto (auth + TLS) + a JSON publisher.
+#
+# PREREQUISITE — run this first, in this directory:
+#
+# MQTT_FIXTURE_PASSWORD='' ./gen-fixture-material.sh
+#
+# It writes ./secrets/ (password file + CA + server certificate/key), which is
+# gitignored and which both services below mount. Without it the broker will not
+# start: there is no anonymous fallback, by design.
+#
+# Bring up (on the shared Docker host — see infra/README.md §1):
+#
+# ssh dohertj2@10.100.0.35 'cd /opt/otopcua-mqtt \
+# && MQTT_FIXTURE_USERNAME=otopcua MQTT_FIXTURE_PASSWORD= docker compose up -d'
+#
+# Endpoints: 10.100.0.35:8883 (TLS + auth) · 10.100.0.35:1883 (plaintext + auth, smoke)
+#
+# Unlike the Modbus / S7 fixtures there are no compose profiles: both services are
+# always wanted together (a broker with nothing publishing into it tests nothing), and
+# they bind different ports so nothing contends.
+services:
+ mosquitto:
+ image: eclipse-mosquitto:2.0.22
+ container_name: otopcua-mosquitto
+ restart: unless-stopped
+ # Every lmxopcua fixture container carries this so the shared Docker host stays
+ # discoverable with `docker ps --filter label=project=lmxopcua`.
+ labels:
+ project: lmxopcua
+ ports:
+ - "1883:1883"
+ - "8883:8883"
+ volumes:
+ - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
+ - ./secrets:/mosquitto/secrets:ro
+ healthcheck:
+ # Authenticated round-trip against the broker's own $SYS tree ($$ escapes the
+ # compose variable syntax). Proves the password file loaded and the listener
+ # accepts a real session — a bare port check would go green on a broker that
+ # rejects every CONNECT.
+ test:
+ - CMD-SHELL
+ - >-
+ mosquitto_sub -h 127.0.0.1 -p 1883
+ -u "$$MQTT_FIXTURE_USERNAME" -P "$$MQTT_FIXTURE_PASSWORD"
+ -t '$$SYS/broker/uptime' -C 1 -W 3
+ interval: 5s
+ timeout: 8s
+ retries: 12
+ start_period: 5s
+ environment:
+ # Consumed only by the healthcheck above; the broker itself reads the hashed
+ # password file. Supplied by the operator's environment at `docker compose up` —
+ # never defaulted here, so a missing value fails loudly instead of silently
+ # provisioning a known-password broker.
+ MQTT_FIXTURE_USERNAME: ${MQTT_FIXTURE_USERNAME:?set MQTT_FIXTURE_USERNAME (must match gen-fixture-material.sh)}
+ MQTT_FIXTURE_PASSWORD: ${MQTT_FIXTURE_PASSWORD:?set MQTT_FIXTURE_PASSWORD (must match gen-fixture-material.sh)}
+
+ publisher:
+ image: eclipse-mosquitto:2.0.22
+ container_name: otopcua-mqtt-publisher
+ restart: unless-stopped
+ labels:
+ project: lmxopcua
+ depends_on:
+ mosquitto:
+ condition: service_healthy
+ volumes:
+ - ./publisher/publish.sh:/publish.sh:ro
+ environment:
+ BROKER_HOST: mosquitto
+ BROKER_PORT: "1883"
+ TOPIC_PREFIX: ${MQTT_FIXTURE_TOPIC_PREFIX:-otopcua/fixture}
+ PUBLISH_INTERVAL: ${MQTT_FIXTURE_PUBLISH_INTERVAL:-2}
+ MQTT_FIXTURE_USERNAME: ${MQTT_FIXTURE_USERNAME:?set MQTT_FIXTURE_USERNAME}
+ MQTT_FIXTURE_PASSWORD: ${MQTT_FIXTURE_PASSWORD:?set MQTT_FIXTURE_PASSWORD}
+ entrypoint: ["/bin/sh", "/publish.sh"]
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/gen-fixture-material.sh b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/gen-fixture-material.sh
new file mode 100755
index 00000000..3b72538f
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/gen-fixture-material.sh
@@ -0,0 +1,137 @@
+#!/usr/bin/env sh
+# ---------------------------------------------------------------------------
+# Generates the MQTT fixture's password file + TLS material into ./secrets/.
+#
+# NOTHING this script writes is committed: ./secrets/ is excluded by the sibling
+# .gitignore. The SCRIPT is the artifact in git, the OUTPUT never is. Re-run it on any
+# machine that needs the fixture (or on the Docker host); the material is throwaway by
+# construction — a self-issued CA, a 2048-bit server key, and whatever password the
+# operator supplies in the environment.
+#
+# Usage:
+# MQTT_FIXTURE_PASSWORD='' ./gen-fixture-material.sh
+#
+# Environment:
+# MQTT_FIXTURE_PASSWORD (required) broker password. No default — a committed or
+# defaulted password is exactly what the plan's
+# "secrets from env, never committed" rule forbids.
+# MQTT_FIXTURE_USERNAME broker username (default: otopcua)
+# MQTT_FIXTURE_SAN_IPS comma-separated IP SANs for the server certificate
+# (default: 10.100.0.35,127.0.0.1)
+# MQTT_FIXTURE_SAN_DNS comma-separated DNS SANs
+# (default: localhost,mosquitto)
+# MQTT_FIXTURE_CERT_CN server certificate CN (default: first IP SAN)
+# MQTT_FIXTURE_CERT_DAYS certificate lifetime in days (default: 825)
+# MQTT_FIXTURE_SECRETS_DIR output directory (default: