Files
mqtt-5-learning/teaching/lessons/0011-security-boundaries.html
Ryan fb30e8e884 Refactor MQTT course and bound backend message processing
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.
2026-10-22 10:19:56 +03:30

72 lines
8.7 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lesson 11 — MQTT Security Boundaries</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 11 · 12 minutes</p>
<h1>Trace security checks from socket to business action</h1>
<p class="dek">TLS, MQTT credentials, topic ACLs, payload validation, and HTTP user permissions answer different questions.</p>
<div class="meta"><span class="pill">🎯 One skill: locate a trust boundary</span><span class="pill">🧠 Predict which layer rejects</span><span class="pill">🧪 Negative tests</span></div>
<section class="card callout"><p class="question">❓ Start with the system story</p><p>A client with a valid username publishes to another device's topic and claims that device's ID in JSON. Which component can reject the topic access, and which component checks the body claim?</p><p>Pause and trace the request from connection setup through business authorization.</p></section>
<h2>🔐 The checks run in sequence</h2>
<div class="flow" aria-label="security checks"><div class="step">TLS<br><strong>server identity + encryption</strong></div><span class="arrow">→</span><div class="step">MQTT CONNECT<br><strong>client authentication</strong></div><span class="arrow">→</span><div class="step">Broker ACL<br><strong>topic authorization</strong></div><span class="arrow">→</span><div class="step">Go service<br><strong>contract + business rules</strong></div><span class="arrow">→</span><div class="step">HTTP API<br><strong>user's device permission</strong></div></div>
<p>A JSON <code>device_id</code> is a claim, not authenticated identity. A Client ID names a session; it is not a secret. The broker usually does not attach the MQTT publisher username to a forwarded PUBLISH, so bind unique client credentials to narrow topic ACLs, then validate topic/body agreement in the application.</p>
<form class="card quiz" data-quiz data-correct="a" data-explanation="The broker authenticates the MQTT client and enforces its topic ACL. The application validates topic and payload identity agreement after delivery; neither a payload field nor TLS does both jobs." data-retry="Distinguish broker authorization from application contract validation.">
<p><strong>🧠 Check the boundary:</strong> Which check enforces that a device credential can publish only to its assigned device namespace?</p>
<label><input type="radio" name="q" value="a"> Broker topic ACL tied to the authenticated client identity</label><br><label><input type="radio" name="q" value="b"> TLS encryption alone</label><br><label><input type="radio" name="q" value="c"> The JSON device_id field alone</label><br><button type="submit">Check answer</button><p data-feedback aria-live="polite"></p>
</form>
<h2>🌐 Verify the broker before sending credentials</h2>
<p>For TLS, the certificate chain must lead to a trusted CA and its subject alternative name must match the host/IP used by the client. A trusted certificate for <code>broker.lab</code> can still fail when the client verifies <code>127.0.0.1</code>. Correct the URL or certificate identity; never turn off certificate verification to make the experiment pass.</p>
<p>Server TLS encrypts the channel and verifies the broker. Username/password authenticates the MQTT client. Per-device ACLs authorize topics. Mutual TLS adds client-certificate authentication only when the broker maps that identity to a deliberate policy. These choices do not authorize an HTTP user to operate a particular vending machine.</p>
<h3>📄 Complete new file: <code>internal/mqtt/tls.go</code></h3>
<p><strong>File overview:</strong> This file reads a public CA certificate and builds a client TLS configuration that verifies both the chain and broker hostname. It does not load client private keys or disable verification.</p>
<p><strong><code>NewVerifiedTLSConfig</code> summary:</strong> Rejects empty inputs and invalid PEM, then returns TLS 1.2+ settings with an explicit <code>ServerName</code>.</p>
<p><strong>Details:</strong> The caller supplies the DNS name or IP address used by the broker certificate SAN. This lesson's implementation is deliberately a complete, standalone trust-config helper; it does not replace the existing MQTT client or config loader. The CA file is public trust material; never embed a private CA key in the client.</p>
<details class="card" open><summary><strong>🧩 Full file contents</strong></summary><pre><code>package mqtt
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"strings"
)
// NewVerifiedTLSConfig builds a client config that verifies the broker's
// certificate chain and the exact DNS name or IP used by the client.
func NewVerifiedTLSConfig(caPath, serverName string) (*tls.Config, error) {
if strings.TrimSpace(caPath) == "" {
return nil, fmt.Errorf("CA certificate path is required")
}
if strings.TrimSpace(serverName) == "" {
return nil, fmt.Errorf("broker server name is required")
}
caPEM, err := os.ReadFile(caPath)
if err != nil {
return nil, fmt.Errorf("read broker CA: %w", err)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(caPEM) {
return nil, fmt.Errorf("broker CA file contains no valid certificates")
}
return &amp;tls.Config{
RootCAs: roots,
ServerName: serverName,
MinVersion: tls.VersionTLS12,
}, nil
}
</code></pre></details>
<p><strong>Integration exercise:</strong> In the existing <code>autopaho.ClientConfig</code>, pass this helper's result through <code>TlsCfg</code> and make the configured broker URL and certificate SAN name agree. Treat changes to the client and config loader as separate files: show each whole updated file in your work, including imports and validation, rather than pasting a partial field diff. Do not use <code>InsecureSkipVerify</code>. Until both files are updated, this helper alone does not make the running client use TLS.</p>
<h2>🧪 Change one input at a time</h2>
<table><thead><tr><th>Change</th><th>Expected boundary</th><th>Evidence</th></tr></thead><tbody><tr><td>Wrong CA</td><td>TLS trust</td><td>No authenticated MQTT session</td></tr><tr><td>Wrong certificate hostname</td><td>TLS server identity</td><td>Name mismatch</td></tr><tr><td>Wrong password</td><td>CONNECT authentication</td><td>Connection rejected</td></tr><tr><td>Valid login, cross-device topic</td><td>Broker ACL</td><td>Denied publish/subscribe and broker evidence</td></tr><tr><td>Allowed topic, body ID mismatch</td><td>Application validation</td><td>Rejected before state mutation</td></tr><tr><td>Valid HTTP user, unauthorized target</td><td>API authorization</td><td>Denied before command creation/publication</td></tr></tbody></table>
<div class="card"><h3>📬 Response Topic is also a trust boundary</h3><p>The simulator should compare a command's Response Topic with its exact authorized response topic before publishing. Correlation Data helps match a response to a request; it does not authorize the destination or authenticate the publisher. An application-level denial also does not reverse an MQTT acknowledgement already sent.</p><p>Rotate credentials by verifying fresh connections with both old and new credentials and checking subscription restoration. Updating a password file alone does not prove existing connections were closed.</p></div>
<h2>✅ Done when</h2><p>Run the negative matrix in the disposable lab and show that each failure is rejected at its intended boundary. A broad <code>#</code> ACL is not a fix for an authorization failure.</p>
<details class="card"><summary><strong>📋 Short self-check answers</strong></summary><ol><li>Broker ACL: which authenticated MQTT client may use which topic.</li><li>Application: whether the delivered topic and payload claims agree and whether the operation is valid.</li><li>API authorization: whether this HTTP principal may act on the requested device.</li></ol></details>
<p>📚 Full failure matrix: <a href="../course-source/11-security-boundaries.md">Section 11 source lesson</a>. Primary references: <a href="https://pkg.go.dev/crypto/tls#Config">Go TLS Config API</a>, <a href="https://mosquitto.org/man/mosquitto-conf-5.html">Mosquitto configuration manual</a>, and <a href="https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html">OASIS MQTT 5.0 Standard</a>.</p>
<p>Ask me to inspect a TLS or ACL failure with you; the layer and evidence should agree.</p>
<footer><p>Previous: <a href="0010-expiry-presence-state.html">Lesson 10 — expiry and presence</a> · Optional: <a href="0011b-dynamic-security.html">Lesson 11B — Dynamic Security</a> · Next: <a href="0012-vending-workflows.html">Lesson 12 — vending workflows</a>.</p></footer>
</main></body></html>