Move lesson source into the teaching structure, align the course path with the learner's Go and DevOps experience, and add the interactive lesson guides. Add advanced DevOps lifecycle study as a separate section. Bound MQTT message work and drain accepted jobs during shutdown; align the backend implementation with the revised course through Section 6.
77 lines
9.5 KiB
HTML
77 lines
9.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Lesson 11B — Mosquitto Dynamic Security</title>
|
|
<link rel="stylesheet" href="../assets/lesson.css"><script src="../assets/quiz.js" defer></script>
|
|
</head>
|
|
<body><main class="page">
|
|
<p class="eyebrow">Go MQTT Vending Backend · Lesson 11B · Optional extension</p>
|
|
<h1>Provision per-device broker permissions at runtime</h1>
|
|
<p class="dek">Use Mosquitto Dynamic Security to create device clients and roles without editing static password and ACL files.</p>
|
|
<div class="meta"><span class="pill">🎯 One skill: separate provisioning from runtime traffic</span><span class="pill">🧠 Predict revocation behavior</span><span class="pill">🧪 Isolated broker config</span></div>
|
|
<section class="card callout"><p class="question">❓ Start with the provisioning story</p><p>A new vending machine is installed after Mosquitto starts. How can it receive an individual credential and narrow topic permissions while the broker is running?</p><p>Name the privileged actor and the separate device identity before continuing.</p></section>
|
|
<h2>🧭 Three identities, three responsibilities</h2>
|
|
<ol><li><strong>Provisioning operator/service:</strong> authenticates with a protected management identity and uses the Dynamic Security control API.</li><li><strong>Device client:</strong> receives its own account and a role limited to its topic namespace.</li><li><strong>Backend:</strong> receives ordinary device publications, validates their contracts, and applies application policy. It should not hold broker-administrator credentials just to process traffic.</li></ol>
|
|
<p>Dynamic Security is a Mosquitto plugin and broker control API, not a Go MQTT 5 library feature. The plugin manages clients, groups, roles, and ACLs and persists its configuration state. The ordinary backend/device listener and a separate management identity keep operations clearly scoped.</p>
|
|
<form class="card quiz" data-quiz data-correct="b" data-explanation="A protected provisioning identity manages plugin state; the device uses only its own ordinary runtime account, and the backend keeps validating messages." data-retry="Do not grant the backend runtime process the broker's management identity by default.">
|
|
<p><strong>🧠 Check the boundary:</strong> Which credential should the device use to publish telemetry?</p>
|
|
<label><input type="radio" name="q" value="a"> The Dynamic Security administrator credential</label><br><label><input type="radio" name="q" value="b"> Its own provisioned client credential with a narrow role</label><br><label><input type="radio" name="q" value="c"> A shared backend credential used by every device</label><br><button type="submit">Check answer</button><p data-feedback aria-live="polite"></p>
|
|
</form>
|
|
<h2>🧱 Keep the extension isolated</h2>
|
|
<p>The normal course lab uses Mosquitto password and ACL files and does not load this plugin. Create a separate broker configuration and persistent, broker-writable Dynamic Security state file. Verify the plugin path inside the pinned image. Do not silently mix plugin-managed accounts with the existing static authentication setup.</p>
|
|
<h3>📄 Complete broker configuration: <code>lab/dynsec/mosquitto.conf</code></h3>
|
|
<p><strong>File overview:</strong> This isolated configuration loads only Dynamic Security for the lab listener, disables anonymous access, and stores plugin state under the persistent broker data mount. The Linux plugin path below is the standard Debian multiarch path; confirm it inside the exact image before starting the comparison broker because the container build may package it elsewhere.</p>
|
|
<details class="card" open><summary><strong>🧩 Full configuration file</strong></summary><pre><code>listener 1883
|
|
allow_anonymous false
|
|
per_listener_settings false
|
|
plugin /usr/lib/x86_64-linux-gnu/mosquitto_dynamic_security.so
|
|
plugin_opt_config_file /mosquitto/data/dynamic-security.json
|
|
persistence true
|
|
persistence_location /mosquitto/data/
|
|
log_dest stdout
|
|
connection_messages true
|
|
</code></pre></details>
|
|
<p>Before writing that file, inspect the pinned image's plugin path with a one-off container command such as <code>docker run --rm --entrypoint sh eclipse-mosquitto:2.0.22 -c 'find /usr -name mosquitto_dynamic_security.so -print'</code>. The command searches inside the image and removes the temporary container; it does not start or modify the course broker. If the path differs, update the single <code>plugin</code> line and record the observed path. I could not inspect this host's Docker image because the Docker socket denied access.</p>
|
|
<h3>🪪 Complete device and backend role rules</h3>
|
|
<p><strong>File overview:</strong> These are the full topic permissions to install through the management API. Device ACL patterns bind the authenticated username as one device-ID segment. Backend permissions are fleet scoped. Set receive default-deny explicitly before grants.</p>
|
|
<details class="card"><summary><strong>📋 Role matrix and complete control commands</strong></summary><pre><code># These commands run against the dedicated Dynamic Security broker.
|
|
# Supply the admin secret through your protected local shell/session.
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec setDefaultACLAccess publishClientReceive deny
|
|
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec createRole device
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec addRoleACL device publishClientSend 'lab/v1/devices/%u/telemetry' allow 10
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec addRoleACL device publishClientSend 'lab/v1/devices/%u/responses' allow 10
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec addRoleACL device subscribePattern 'lab/v1/devices/%u/commands' allow 10
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec addRoleACL device publishClientReceive 'lab/v1/devices/%u/commands' allow 10
|
|
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec createRole backend
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec addRoleACL backend publishClientSend 'lab/v1/devices/+/commands' allow 10
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec addRoleACL backend subscribePattern 'lab/v1/devices/+/telemetry' allow 10
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec addRoleACL backend publishClientReceive 'lab/v1/devices/+/telemetry' allow 10
|
|
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec createClient device-001 -i lab-device-001
|
|
mosquitto_ctrl -h 127.0.0.1 -p 18883 -u dynsec-admin -P "$DYNSEC_ADMIN_PASSWORD" \
|
|
dynsec addClientRole device-001 device 10
|
|
</code></pre><p>Run the commands in order and only once per role/ACL. The client-creation command prompts for the device password. For a repeated setup, query existing plugin state first and handle already-created roles/ACLs explicitly rather than widening access.</p></details>
|
|
<p>Start with default-deny behavior for the relevant direction, grant only the required device publish/backend subscribe paths, and test real message delivery. A successful SUBACK or an ACL line alone does not prove a cross-device publication is blocked.</p>
|
|
<h2>🧪 Provision, test, revoke</h2>
|
|
<ol><li>Create the isolated plugin configuration and initialize its state using the secured management procedure documented in the full lesson.</li><li>Provision one test device account and a least-privilege role. Connect it and verify its own telemetry succeeds.</li><li>Attempt cross-device publish and subscription, then send a permitted topic with a mismatched payload identity. Identify whether the broker or application rejects each case.</li><li>Revoke/disable the client and observe behavior for both a new connection and an already-connected device.</li><li>Restart the broker and confirm the plugin state persists from its dedicated volume.</li></ol>
|
|
<div class="card"><h3>⚠️ What this experiment does not prove</h3><p>Successful provisioning does not establish secure remote administration unless transport and management access are protected. A plugin role does not replace application schema checks. Revoking an account may not by itself terminate every existing connection; verify the live-client behavior for the pinned broker/plugin.</p></div>
|
|
<h2>✅ Done when</h2><p>You can trace provisioning, broker authentication, topic authorization, application validation, and revocation as distinct steps. The normal static lab remains available as a separate configuration.</p>
|
|
<p>📚 Full failure and revocation experiments: <a href="../course-source/11b-dynamic-security.md">Section 11B source lesson</a>. Primary references: the official <a href="https://mosquitto.org/documentation/dynamic-security/">Mosquitto Dynamic Security guide</a> and <a href="https://mosquitto.org/man/mosquitto_ctrl_dynsec-1.html">mosquitto_ctrl DynSec manual</a>.</p>
|
|
<p>Ask me to troubleshoot a denied operation using its client reason and broker log evidence.</p>
|
|
<footer><p>Previous: <a href="0011-security-boundaries.html">Lesson 11 — security boundaries</a> · Continue: <a href="0012-vending-workflows.html">Lesson 12 — vending workflows</a>.</p></footer>
|
|
</main></body></html>
|