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.
72 lines
8.7 KiB
HTML
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 &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>
|