MQTT and Sparkplug B

MQTT and Sparkplug B

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Settings over MQTT

Settings over MQTT

A server with a login database (logins.md) can also keep its
settings
in that database, and you can read and change them over MQTT -
from the modbee config command, or from your own platform with nothing but
an admin login. Nobody needs a shell on the server or an editor on
modbee.yaml.

A change is stored at once and applied when the server restarts, and the
same request can ask for that restart. Logins are different: they change
while the broker runs (logins.md). Settings - ports, limits,
Kafka mappings - are what a broker is built from when it starts, so they wait
for the next start rather than half-apply to a running one.

The TLS certificate is the exception. Its certificate, key and client CA
are applied the moment they are stored, with no restart
(below), because a certificate is
renewed every few months and a broker a plant depends on should not stop for
it.


What it needs

  • authentication: database: - in the file, or as
    MODBEE_AUTHENTICATION_DATABASE. The settings are kept in the same SQLite
    file as the logins, in a table of their own. The docker/examples setups
    already have it.
  • The server started with a file (modbee serve -C modbee.yaml). The
    Docker image always is: /etc/modbee/modbee.yaml.
  • An admin login, as for managing logins.

Without a login database, modbee.yaml and the environment are all there is.


Two kinds of stored setting

What Changed with
An override One setting, named by its path - broker.port, broker.max-session-expiry, kafka.clusters[0].bootstrap-servers config.set
A document A whole modbee.yaml, kept in the database in place of the file config.apply

The path of a setting is its place in modbee.yaml, and it is exactly the
setting an environment variable names: broker.max-session-expiry is
MODBEE_BROKER_MAX_SESSION_EXPIRY. An override is therefore what a variable
is - a single value, or a list written comma-separated - and it can do what a
variable can do and no more:

  • It changes a setting the file (or document) could hold. A setting
    inside a section that is read only when it is written - metrics:,
    broker: tls: - can be overridden once that section exists, not before.
  • It cannot add a Kafka cluster or mapping, or a section. That is a
    change to the shape of the configuration, so it takes a whole document.

Which value wins

When the server starts, each setting comes from the first of these that has
it:

1 A flag on the command line (modbee serve -C modbee.yaml -p 1884)
2 An environment variable (MODBEE_BROKER_PORT)
3 A stored override (config.set)
4 The stored document (config.apply) - or, when there is none, the file
5 The default

The environment wins over the database. A setting a compose file pins
with a variable stays pinned, whatever is stored, and config.get lists
those variables as pinned so the reason is visible rather than guessed at.

Authentication is the file's and the environment's alone. A change over
MQTT that moved the login database, or changed the admin login, could lock
every admin out - and then nothing could change it back over MQTT. So
config.set refuses any authentication... path, and config.apply
refuses a document whose authentication: differs from what the file and
the environment give. That includes authentication.certificate-login
(logins.md): turned on, an admin
presenting a certificate is the login its CN names, so it is set with
MODBEE_AUTHENTICATION_CERTIFICATE_LOGIN=true or in the file - and
authentication.default-topics (MODBEE_AUTHENTICATION_DEFAULT_TOPICS). A document must therefore carry the file's
authentication: section as it is.

If the stored settings no longer load when the server starts - a newer
version that reads a setting differently, a file that has changed under
them - the server starts from the file alone, prints each problem, and says
so:

modbee serve: the stored settings would not load - starting from /etc/modbee/modbee.yaml alone

It does not refuse to start, because a broker that will not start over a
stored setting is one nobody can reach to fix it. config.get then lists the
same problems.


From the command line

modbee config uses the admin login, like modbee user. Inside a stack's
container it logs in by itself:

docker compose exec modbee modbee config get
settings from: file (/etc/modbee/modbee.yaml)
  broker.max-session-expiry = 86400
  pinned by the environment: MODBEE_AUTHENTICATION_USERNAME
  pinned by the environment: MODBEE_AUTHENTICATION_PASSWORD
the running server is behind these settings - restart it to apply them
Command Does
modbee config get Where the settings come from, every override, and what the environment pins. --json prints the whole answer, the document included
modbee config set broker.queue=128 broker.max-session-expiry=86400 Store overrides
modbee config set --remove broker.queue Drop an override, back to the file's value (--remove repeats)
modbee config apply --file new.yaml Store a whole document in place of the file
modbee config check --file new.yaml or modbee config check broker.port=1884 Would these load? Prints every problem, stores nothing
modbee config reset Drop the stored document and every override: back to the file alone

set, apply and reset take --restart: once the change is stored,
the server restarts to apply it (below).

A setting that does not exist, or a value the file would not accept either,
is refused with its problem, and nothing is stored:

docker compose exec modbee modbee config set broker.prot=1884
modbee: the settings were not stored: they would not load
  broker.prot: no setting is called "broker.prot"

A setting ending -pem takes @file for its value - the PEM text of a file
on the computer modbee runs on - because nobody types a certificate:

docker compose exec modbee modbee config set broker.tls.cert-pem=@/tmp/broker.pem broker.tls.key-pem=@/tmp/broker.key
stored - and the TLS listener is serving it now

config get then shows each stored PEM as (PEM, n bytes), the stored key
as (set), and what TLS is serving:

tls: serving on 8883 (wolfSSL)
  certificate: CN=mqtt.example.com, until 2027-01-05T10:00:00Z
               sha256 3dd049baf3c5e7eaeaea212abd68147b15c891250bf4f82f2c2b1e4974412dfc
  client certificates: required, from CN=Plant Devices CA
  certificate login: on

--file is read by the modbee command, on the computer it runs on. For a
document on your own computer, run modbee there against the broker -
modbee config apply --file new.yaml -h mqtt.example.com --tls -u admin -P '...' -
or copy it into the container first:

docker compose cp new.yaml modbee:/tmp/new.yaml
docker compose exec modbee modbee config apply --file /tmp/new.yaml --restart

From your own platform

Every command is a JSON request, published to $modbee/admin/request with a
reply topic of $modbee/admin/<your client id>/reply subscribed - the same
channel as managing logins (logins.md).

command Fields Answer
config.get source (file or database), file, document, overrides, pinned, problems, restart_required, tls (below)
config.check any of document, set, remove ok, and problems. Stores nothing
config.set set: {"path": value, ...}; optional remove: [paths], restart problems, restart_required, restarting, tls_applied
config.apply document: the YAML, as text; optional restart the same
config.reset optional restart the same
{"id": "7", "command": "config.set", "set": {"broker.port": 1884, "broker.max-session-expiry": 86400}}
{"problems": [], "restart_required": true, "restarting": false, "id": "7", "ok": true}
  • A value is a string, a number, true/false, or a list of those -
    which is stored comma-separated, as a variable would hold it.
  • problems lists each one as {"line", "path", "message", "warning"}.
    line is the line in the document, or 0 for an override. A warning does not
    stop a change being stored.
  • Refused, the answer is "ok": false with the problems and
    "error": "the settings were not stored: they would not load".
  • config.get's document is the stored document, or the file when there
    is none, with every password: and key-pem: value shown as (set); so
    is an override whose path ends in password or key-pem. A private key is
    never sent back, by any command. A document read back this way therefore
    cannot be applied as it is - its passwords would be the text (set). Keep
    secrets out of documents: password-file:, or the environment.
  • restart_required is true while what is stored differs from what the
    running server started with - leaving out the TLS material, which the
    running server takes as it is stored.
  • tls_applied is true when the change gave the TLS listener new
    material, which it is serving now.

The TLS material, applied while it runs

Four settings change what the TLS listener serves, and are applied to the
running server the moment they are stored - by config.set, config.apply
or config.reset alike:

Path
broker.tls.cert-pem The certificate as PEM text - a full chain is presented whole
broker.tls.key-pem Its private key, unencrypted PEM. Goes with cert-pem: one without the other is refused
broker.tls.client-ca-pem One or more CA certificates. Set, client certificates are verified against it
broker.tls.client-certificates required or optional (kafka.md)
{"id": "8", "command": "config.set", "set": {
  "broker.tls.cert-pem": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n",
  "broker.tls.key-pem": "-----BEGIN PRIVATE KEY-----\nMIGH...\n-----END PRIVATE KEY-----\n"}}
{"problems": [], "restart_required": false, "restarting": false, "tls_applied": true, "id": "8", "ok": true}
  • The text wins over the file's paths (broker.tls.cert, key,
    client-ca), as every stored setting wins over the file.
  • It is checked before it is stored. A certificate that will not parse, a
    key that is not its key, a CA that is not a certificate: each is refused
    with its problem while the old material is still being served. An expired
    certificate is stored, with a warning saying so. config.check asks the
    same question and stores nothing.
  • New handshakes get the new material; connections already up keep
    theirs.
    Nothing is disconnected, and every CPU core of the server
    switches together.
  • With no tls: section in the file, a certificate and its key stored
    this way turn TLS on, on broker.tls.port (8883 by default) - at the next
    restart
    : turning TLS on or off, and its port and address, are what a
    socket is bound to, so those still say restart_required. Ask for the
    restart with "restart": true.

config.get's tls is what is being served now, never what is stored
for later, and never the key:

"tls": {
  "serving": true,
  "port": 8883,
  "engine": "wolfSSL",
  "certificate": {
    "fingerprint": "3dd049baf3c5e7eaeaea212abd68147b15c891250bf4f82f2c2b1e4974412dfc",
    "subject": "CN=mqtt.example.com",
    "not_after": "2027-01-05T10:00:00Z",
    "names": ["DNS:mqtt.example.com", "IP:203.0.113.7"]
  },
  "key": "set",
  "client_ca": {"fingerprint": "1aa71917...", "subject": "CN=Plant Devices CA", "certificates": 1},
  "client_certificates": "required",
  "certificate_login": true
}
Field
serving Whether there is a TLS listener. False: every other field is empty (certificate and client_ca are null)
certificate.fingerprint SHA-256 of the certificate's DER: 64 lowercase hexadecimal characters - openssl x509 -noout -fingerprint -sha256, lowered, without its colons
certificate.subject The subject, in the certificate's own order
certificate.not_after When it expires, ISO 8601 UTC
certificate.names Its subject alternative names: DNS:, IP:, email:, URI:
client_ca The first CA certificate's fingerprint and subject, and how many certificates the bundle holds; null without one
client_certificates required, optional, or none without a client CA
certificate_login Whether a verified certificate logs in by itself (logins.md)

A password set as an override is stored in the database as written - unlike a
login's secret, a setting has to be readable to be used - and goes into every
backup of it. password-file: keeps it out.


Applying a change: the restart

A change applies when the server next starts. --restart (or
"restart": true) asks for that start now: once the answer has been sent,
the server stops gracefully, exactly as docker stop stops it - devices
are told the server is shutting down, and messages on their way to Kafka are
given their time.

What brings it back is whatever runs it:

  • Docker, with a restart policy of unless-stopped (every compose file
    here has it) or always. A graceful stop exits with code 0, so on-failure does not
    bring it back.
  • systemd, with Restart=always.
  • Nothing, when the server was started by hand: it stays stopped.

"restart": true restarts only when it is needed: if what is stored is
already what the server is running, the answer has "restarting": false and
nothing stops.


Backups

The settings are in the login database, so modbee backup and the daily copy
(logins.md) hold them, and modbee restore puts them
back with the logins.

Last updated 2 hours ago
Was this helpful?
Thanks!