Go MQTT Vending Backend ยท Lesson 11 ยท 12 minutes

Trace security checks from socket to business action

TLS, MQTT credentials, topic ACLs, payload validation, and HTTP user permissions answer different questions.

๐ŸŽฏ One skill: locate a trust boundary๐Ÿง  Predict which layer rejects๐Ÿงช Negative tests

โ“ Start with the system story

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?

Pause and trace the request from connection setup through business authorization.

๐Ÿ” The checks run in sequence

TLS
server identity + encryption
โ†’
MQTT CONNECT
client authentication
โ†’
Broker ACL
topic authorization
โ†’
Go service
contract + business rules
โ†’
HTTP API
user's device permission

A JSON device_id 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.

๐Ÿง  Check the boundary: Which check enforces that a device credential can publish only to its assigned device namespace?




๐ŸŒ Verify the broker before sending credentials

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 broker.lab can still fail when the client verifies 127.0.0.1. Correct the URL or certificate identity; never turn off certificate verification to make the experiment pass.

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.

๐Ÿ“„ Complete new file: internal/mqtt/tls.go

File overview: 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.

NewVerifiedTLSConfig summary: Rejects empty inputs and invalid PEM, then returns TLS 1.2+ settings with an explicit ServerName.

Details: 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.

๐Ÿงฉ Full file contents
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
}

Integration exercise: In the existing autopaho.ClientConfig, pass this helper's result through TlsCfg 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 InsecureSkipVerify. Until both files are updated, this helper alone does not make the running client use TLS.

๐Ÿงช Change one input at a time

ChangeExpected boundaryEvidence
Wrong CATLS trustNo authenticated MQTT session
Wrong certificate hostnameTLS server identityName mismatch
Wrong passwordCONNECT authenticationConnection rejected
Valid login, cross-device topicBroker ACLDenied publish/subscribe and broker evidence
Allowed topic, body ID mismatchApplication validationRejected before state mutation
Valid HTTP user, unauthorized targetAPI authorizationDenied before command creation/publication

๐Ÿ“ฌ Response Topic is also a trust boundary

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.

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.

โœ… Done when

Run the negative matrix in the disposable lab and show that each failure is rejected at its intended boundary. A broad # ACL is not a fix for an authorization failure.

๐Ÿ“‹ Short self-check answers
  1. Broker ACL: which authenticated MQTT client may use which topic.
  2. Application: whether the delivered topic and payload claims agree and whether the operation is valid.
  3. API authorization: whether this HTTP principal may act on the requested device.

๐Ÿ“š Full failure matrix: Section 11 source lesson. Primary references: Go TLS Config API, Mosquitto configuration manual, and OASIS MQTT 5.0 Standard.

Ask me to inspect a TLS or ACL failure with you; the layer and evidence should agree.