solace-cnt-scripts

Configuration

[!WARNING] Not a supported Solace product. solace-util was created by Solace Professional Services and is supported only by Solace Professional Services – not by Solace Support. For help with this tool, contact your Solace Professional Services representative rather than opening a Solace Support case. This notice covers this tool only, not the Solace PubSub+ Event Broker or the EventBroker Operator that it deploys and operates.

Every solace-util command reads one YAML env file. This document explains how that file is found, how it decides which platform runs, and what every commonly-used key means.

Do not assemble a file from the tables below – start from one the binary writes:

solace-util examples --platform kubernetes -o env/dev.yaml   # or docker, or podman
solace-util examples                              # every key, annotated

The three platform names give a minimal standalone file: only the keys that platform cannot default, declaring only its own section. full is the fully annotated schema – every key the loader accepts and the default each omitted one takes – and is the same text as env/sample.yaml, which is generated from it. This document explains the keys; those are the things you start from.

Choosing the env file

Every command reads one YAML env file, selected with -e/--env. The value is an actual file name, taken literally – no extension is ever inferred, so -e dev and -e dev.yaml name different files:

When no candidate exists the error names every path that was tried.

Decoding is strict: an unknown or misspelled key is a hard error, so typos fail loud instead of being silently ignored. A file that is not YAML at all is reported as such – and if it looks like a legacy bash env file, the error points at solace-util convert (below).

Relative paths resolve against the env file, not the current directory

Every host path in the file – tls.cert, tls.certKey, tls.cas, broker.cliScriptsDir, broker.hostDiagnosticDir, broker.domainCerts.dirs[].path, broker.domainCerts.files{}, docker.composeFile – is resolved against the directory the env file itself was found in, never against the directory the command was run from. So /srv/solace/env/prod.yaml declaring tls.cert: certs/tls.crt means /srv/solace/env/certs/tls.crt, whichever directory you drive it from. An absolute value is left exactly as written.

The reason is not tidiness. A podman Volume= source with no leading separator is read by podman as the name of a named volume, so a relative certificate path made podman create an empty volume and mount it over the real certificate – no error, unit starts, broker starts, and TLS is not what you configured. systemd gives a unit no useful working directory, so there was never a current directory that would have made the relative form work.

Three fields deliberately do not follow the rule:

Field Why
<platform>.container.dataDir Required absolute instead. It is the host side of a bind mount and what broker remove --delete-data empties (the directory itself stays), so quietly changing which directory that points at is not a fix. The default /opt/solace/data already satisfies it
podman.quadletDir Checked, never resolved. The unit must live where systemd scans, so resolving a relative value would invent a location systemd never reads. Both defaults are already absolute
podman.baseDir Optional, and required absolute when set. Deploy and remove delete a legacy private-key file under it, so resolving a relative value would aim that delete at a directory you never named

Host paths are also character-checked, and the rule is deliberately looser than the one for the command fields: a path may carry a backslash and a colon, because C:\certs\tls.crt is a real path and a command token has no business carrying either. (A tilde is not part of that difference: both sides admit an embedded one – an 8.3 short name such as C:\Users\RUNNER~1\... – and both expand a leading one, see below.) What a path may not carry is a $ or a shell metacharacter (both container artifacts write these values verbatim, and compose interpolates $ across the whole document), whitespace (a mount is written source:target:options on one line, so a space cannot be delimited), or a control or invisible character. The check runs on the value as you wrote it, before any resolution.

A leading ~ expands to your own home directory, in every path key and in every command field. ~, or ~/..., or ~\..., expands to os.UserHomeDir() – the home directory of whoever is running this tool – before the relative-path rule above ever sees it, so the expanded value is already absolute and is never joined onto the env file’s directory. A tilde anywhere else in the value is left alone, which is what keeps an 8.3 short name like C:\Users\RUNNER~1\... working. ~someoneelse/... (another user’s home) is not supported and is refused by name rather than guessed at. An unresolvable home directory is an error naming the field, and so is a home directory whose own path carries whitespace or a character the value’s destination cannot carry.

There are no exceptions among the path keys – the three in the table above included. Those three are still never resolved against the env file’s directory, so a relative value is still refused; a ~ one is not relative by the time the check runs. What each is checked for does not change:

podman:
  quadletDir: ~/.config/containers/systemd
  baseDir: ~/solace
  container:
    dataDir: ~/solace/data

In a command field every token expands except the first, which names the binary and must stay a bare allowlisted name (kubectl, oc, docker, podman, …) with no path in it at all – see the command fields. So this works:

kubernetes:
  command: oc --kubeconfig ~/solace/kubecontext

and command: ~/bin/oc ... is refused, naming argv[0] as the reason.

A ~ value is the one thing in this schema that does not travel. Every other host path either is absolute or resolves against the env file’s own directory, so the same file describes the same deployment on any machine; a ~ resolves against whoever runs the tool, so an env file carrying one means something different for each operator – and if their home cannot be resolved, or carries a space, it means a failed load. Write it out in full in any file you share.

Which platform runs

The command tree is flat and identical on every platform – there is no kubernetes, docker or podman subtree to type. The platform is a property of the deployment the env file already describes, so it is resolved from that file rather than repeated on the command line. Resolution happens in this order:

  1. --platform <name>, if given. It accepts the canonical names kubernetes, docker, podman, or the abbreviations kube, dk, pm. Neither k8s nor k8 is accepted, as an abbreviation or as a section name – the platform goes by the product’s own word. It must name a platform section the env file actually declares – passing --platform docker against a file with no docker: section fails loudly rather than running against a platform the file never described.
  2. Otherwise the env file decides, from whichever of the top-level kubernetes:, docker:, and podman: sections it declares:
    • exactly one -> used silently, and named in the preamble (==> platform: docker (from ./dev.yaml))
    • none -> a loud error telling you to add one
    • more than one -> an interactive prompt listing them; a non-interactive run (piped, CI) fails loudly instead and tells you to pass --platform

An env file must declare its platform section even when every setting under it defaults – write docker: {} rather than leaving the section out. This matters because docker and podman have no mandatory field of their own, so without the marker the file would be indistinguishable from a kubernetes one that simply hasn’t set any docker keys.

The tree itself is the union of every platform’s commands, and it renders the same way regardless of which platform an env file names – --help and shell completion never load one, so a command cannot appear or disappear depending on a file they have not read. A command that does not apply to the platform a file resolves to says so in its help text (for example “(kubernetes only)”) and refuses at run time with a named error rather than silently doing nothing.

The keys

Minimum required (Kubernetes):

Key Purpose
image.repo Broker image repository: a lowercase path, optionally led by a registry host (solace/solace-pubsub-standard, localhost:5000/solace/broker)
image.tag Image tag: 1-128 letters, digits, _, . or -, not starting with . or -. A digest is not supported. Written quoted into the CR, so 10.10 stays a string rather than becoming the number 10.1
kubernetes.name Broker / custom-resource name
kubernetes.namespace Target namespace
kubernetes.storage.msgNodeSize Message-node PVC size, a Kubernetes quantity (30Gi; no sign or exponent). Mandatory unless customVolumeMount covers every node, which leaves nothing to provision
kubernetes.storage.customVolumeMount.<primary\|backup\|monitor> Mount an EXISTING PersistentVolumeClaim for that node instead of provisioning one. Mutually exclusive with kubernetes.storage.class – naming both is refused, since the CRD does not say which wins. All nodes in the redundancy group or none. broker remove --delete-data never deletes these: the volume may hold data that predates this broker, so it is reported and left for you

semp.adminPass is not required on Kubernetes, unlike docker and podman: without it the CR references kubernetes.adminSecret, or names no admin Secret and lets the operator generate one – see The admin Secret. The admin username is always admin, the broker’s own name for the built-in account, and there is no key to change it.

Common optional knobs:

Key Default Purpose
redundancy.<primary\|backup\|monitor>.name this host’s hostname (standalone containers only) The broker’s routername, and the container’s hostname. On docker/podman in HA all three are mandatory: each is the key of that node’s entry in the group table every host renders (redundancy_group_node_<name>_connectvia), and a host knows its own name and no other machine’s, so one it filled in itself would build a table the other two disagree with. In standalone it is optional – there is one node, it is always this host, so the host’s own OS hostname is used when the key is omitted, and the run says so. Not read on Kubernetes, where the operator names the pods after kubernetes.name
redundancy.enabled false true = HA group (primary+backup+monitor); false = single standalone broker. HA provisions three brokers, so it must be asked for explicitly. The group’s members live under redundancy.<primary\|backup\|monitor>.<name\|addr> and the key they authenticate with under redundancy.psk
replication.* – A DR pair: two SEPARATE brokers, each message-VPN active at one site and standby at the other. Not redundancy, which is the three nodes of one HA group – a replicated deployment usually has both, an HA group at each site. Omit the section entirely unless this broker replicates; see Replication
image.registry docker.io Registry prefix for the image reference: a host with an optional :port and path (registry.example.com:5000, ghcr.io/solace). All three image.* parts are held to the image-reference grammar at load, since each is written as-is into the quadlet unit, the compose file and the CR, where a newline or a space would add keys of its own
kubernetes.storage.class cluster default StorageClass for the broker PVCs
kubernetes.storage.monNodeSize 5Gi Monitor-node PVC size, a Kubernetes quantity like msgNodeSize. The monitor holds no message spool, so it needs far less than msgNodeSize
kubernetes.serviceAccount none ServiceAccount the broker pods run as. Unset lets the operator use the namespace default
kubernetes.updateStrategy automatedRolling automatedRolling or manualPodRestart
kubernetes.command kubectl Cluster CLI (legacy KUBE). A scalar is split on whitespace, so it can be a drop-in (oc) or a profile (kubectl --kubeconfig <file>). Restricted – see The command fields are executable content
docker.command / podman.command docker / podman Container CLI (legacy CONTAINER_RUNTIME), same forms and the same restrictions as kubernetes.command
docker.compose <command> compose The compose invocation. Set it to docker-compose on a host carrying only the standalone v1 binary; same forms and restrictions as docker.command, plus the one permitted compose subcommand
<docker\|podman>.container.healthCheck.enabled false Adds an engine health check polling the broker’s own /health-check/readiness on port 5550 every 5s, so docker ps and podman’s auto-restart see readiness rather than liveness. Needs broker 10.26 or later and a version-numbered image.tag; set healthCheck.cmd to supply your own probe instead (which skips the version check). A custom probe runs inside the container under no-new-privileges, so it cannot use su, sudo or a setuid helper – see What the broker cannot gain. Its interval, timeout and startPeriod are Go durations (5s, 1m30s): startPeriod may be 0s, the other two must be above zero. Container-only by design – on Kubernetes the operator already probes the pods
kubernetes.tlsServerSecret – Name of the TLS Secret the broker uses; a Secret to reference – named here, or derived – is what enables the CR’s TLS block. Optional: unset derives <kubernetes.name>-tls when tls.cert/tls.certKey are set, and names nothing (no tls block) when they are not – the same three states as kubernetes.imagePullSecret. Lives under kubernetes.* because it names a Kubernetes Secret object – the cert/key files themselves stay platform-neutral under tls.*. Naming it does not mean building it: with tls.cert/tls.certKey set, this tool builds the Secret and removes it on teardown; without them the Secret must already exist and is only referenced – see Bring your own TLS Secret
tls.cert / tls.certKey – The server certificate and its private key, as two separate host files. Inseparable on every platform: setting one without the other is refused at load, in both directions. On docker and podman the broker reads the certificate as ONE file containing the key, the certificate, then the tls.cas chain, and this tool builds that file from them. Kubernetes takes them as two keys in a Secret and lets the operator assemble them – a Secret carrying only tls.crt is one the broker cannot start a listener over
tls.cas – The CA chain the broker presents after its server certificate – intermediates first, in the order listed. Part of the server certificate on every platform: appended to the docker/podman secret, to the Kubernetes TLS Secret’s tls.crt, and to what broker configure server-certs loads, and covered by the certificate’s digest, so a renewed CA is noticed like a renewed certificate. Not trusted CAs: those are broker.domainCerts, which broker configure domain-certs loads
<docker\|podman>.container.name solace The container’s name, and the stem of every derived name (the podman unit and service, the host-side secret names). Held to the engines’ own grammar: it must start with a letter or digit, then letters, digits, ., _ or -. A name that YAML would read as a boolean or number (yes, off, 0123) is legal here and quoted in the generated compose file, so it stays the string you wrote. On docker it is also the compose project name, lowercased with anything outside [a-z0-9_-] folded to -, since compose’s grammar is narrower than the engines’ – override with COMPOSE_PROJECT_NAME (operations.md)
podman.rootless false Which podman this file deploys. true = the unit under $XDG_CONFIG_HOME/containers/systemd loaded by systemctl --user, container.runUser defaulting to 1000:0, and ownership applied through podman unshare. Declared, then checked against the account you run as: every podman command except generate refuses when they disagree – true under sudo, or false without it – so a mistyped sudo is a refusal rather than a second, rootful deployment. generate is exempt because one file must render one artifact from any account. See Rootless podman prerequisites
podman.baseDir – Optional and unused; absolute when set. Earlier builds wrote the server-certificate bundle (private key included) here as a host file; the certificate now rides podman’s secret store like every other credential, so nothing is written here. When set, broker deploy and broker remove delete the old <container.name>-tls-servercertificate.pem in it, so an upgraded host is not left holding the key. Leave it set until every host has been redeployed once, then remove it
kubernetes.imagePullSecret – Name of the image-pull Secret the CR references. Optional: unset derives <kubernetes.name>-image-pull when registry credentials are configured, and names nothing (no pullSecrets block) when they are not. Naming it does not mean building it, the same rule kubernetes.tlsServerSecret follows above. With image.user/image.pass set (or their *Env equivalents), this tool builds the Secret, applies it alongside the operator’s fixed-name regcred, and removes both on teardown. Without them, a configured name must already exist – created by hand or by a cluster admin – and is only referenced, never built or deleted. The credentials themselves stay under image.*; docker and podman use them for <command> login, having no operator and no regcred
kubernetes.imagePullPolicy – Always | IfNotPresent | Never; unset keeps the CR’s own IfNotPresent
kubernetes.adminSecret – Name of the Secret holding the broker’s admin credentials, which the CR’s adminCredentialsSecret references. Optional: unset derives <kubernetes.name>-admin when semp.adminPass is set, and names nothing when it is not – the operator then generates <kubernetes.name>-pubsubplus-admin-creds. Naming it does not mean building it, the rule the TLS and image-pull Secrets follow. See The admin Secret
kubernetes.operator.namespace pubsubplus-operator-system Namespace the cluster-scoped EventBroker Operator is installed to and addressed in. Two rules, neither of which asks the cluster: use this when set, otherwise the fixed default operator deploy installs to. So every operator command resolves the SAME namespace, and this tool never searches a cluster to find one – an unanchored name match could resolve to another team’s operator. Stays optional; most deployments never set it
kubernetes.operator.image solace/pubsubplus-eventbroker-operator:1.4.2 The operator image operator deploy installs, with image.registry prefixed when set: a lowercase repository path with an optional :tag and @sha256: digest
kubernetes.operator.watchNamespaces "" Comma-separated namespaces the operator watches, each a DNS-1123 label. operator deploy unions it with what the installed operator already watches, and adds kubernetes.namespace unless watchBrokerNs: false; false with an empty list watches every namespace
kubernetes.operator.cpu / mem 500m / 512Mi The operator container’s limits, Kubernetes quantities (no sign or exponent). These four operator keys are substituted into the operator bundle as-is, so each is checked at load
semp.additionalUsers – Extra CLI (management) users, each {username, accessLevel, password\|passwordEnv} with accessLevel one of none, read-only, mesh-manager, read-write, admin. Created at boot on every platform. The username must start with a letter or _ and be 1-32 characters (the broker’s own rule); on Kubernetes it may not contain . or - either, because the credentials ride the pod environment there and the kubelet drops variables whose names are not identifiers. See Extra CLI users differ by platform
semp.adminPassEnv (and every other *Env) – Name of an environment variable holding the secret, instead of the value itself. See Secrets
timezone – Broker timezone, all platforms (the CR’s timezone and the containers’ TZ). Omitted keeps the image default
broker.cliScriptsDir cli Host folder broker perform cli-script and shell-script read scripts from – the only place they look. A script is named by its bare file name and must exist here; a path is refused and the current directory is never searched. Relative to the env file’s directory, so for env/dev.yaml the default is env/cli
broker.hostDiagnosticDir diag-configs Host folder broker perform gather-diagnostics writes its bundle to
broker.productKeys – The Solace licence keys broker configure product-keys applies. Strings, not paths
broker.domainCerts – CA certificates broker configure domain-certs loads: dirs, a list of directories walked one level deep, plus files, explicit CA-NAME: full host path entries. Neither is defaulted, so an env file configuring neither is a no-op. A files key is checked at LOAD against the same charset and 64-character cap a directory-derived name meets by construction, and an entry with no path is refused there too. The directory WALK waits for the command, since a directory absent from this machine must not fail a deploy that never touches certificates. See operations.md
kubernetes.securityContext – runAsUser/fsGroup for the pod. Omitted entirely when unset. Every id here and in containerSecurity is a whole number from 0 to 2147483647 in plain decimal digits, quoted or not – anything else is refused at load, because the value is written into the CR unquoted, where 010 would be read as 8 and a newline as extra YAML. "0" means the operator’s default (1000001 for users, 1000002 for groups), or on OpenShift an id the SCC assigns; it never means root
kubernetes.containerSecurity – runAsUser/runAsGroup for the broker container, with the id rule above. readOnlyRootFilesystem was removed: a read-only root filesystem is not supported, so the key is refused by name (see the removed keys). The CR has no privileged or allowPrivilegeEscalation field, so a rendered CR shows neither; the bundled operator 1.4.2 pins privileged: false, allowPrivilegeEscalation: false, runAsNonRoot: true, capabilities: drop: [ALL] and seccompProfile: RuntimeDefault on the broker container itself – which makes the operator version part of the security posture, see container-security.md
kubernetes.ports – (the operator’s default) The broker Service’s ports, each name=containerPort[:servicePort][/proto] (tcp-smf=55555:55556, tls-smf=55443/TCP; TCP or UDP, default TCP). Unset, the CR carries no service.ports, so the installed operator’s own default list applies – for the bundled operator, the 17 ports solace-util examples shows commented out, SSH on 2222 among them. A list you set REPLACES that default whole, so keep tcp-ssh if you want CLI over SSH; an empty list is the same as unset. Upgrading this tool changes nothing on the broker or its Service for an env file without the key: earlier builds wrote that same list into the CR, so the first broker deploy after the upgrade reports the CR configured and the operator’s default refills identical ports
scaling.* see Scaling Broker sizing, applied on every platform – the CR’s spec.systemScaling on Kubernetes, container environment variables on docker and podman
scaling.maxConnections 100 (Kubernetes) / 1000 (container) The Solace scaling tier. Fixes how many cores the broker gets and defaults its memory on every platform – see Scaling tiers
<docker\|podman>.container.mem the tier’s memory Container memory limit, in docker’s and podman’s own b\|k\|m\|g suffix (not Kubernetes’ Mi/Gi). Podman’s unit carries it as PodmanArgs=--memory=, not quadlet’s Memory= key, which needs podman 5.5
<docker\|podman>.container.cpuset 0-(cores-1) from the tier WHICH host cpus the MESSAGING nodes may use – cpuset: in the compose file, PodmanArgs=--cpuset-cpus= in the quadlet unit. A list or range of cpu ids (0-3, 0,2,4) naming EXACTLY as many cpus as the tier sizes the broker for; which ones is yours
<docker\|podman>.container.monitorCpuset 0 WHICH host cpu the HA monitor runs on. Exactly one id: the monitor gets a fixed 1 cpu and 2g whatever the tier, since it arbitrates quorum and carries no spool
<docker\|podman>.container.ulimits.core -1 The core-dump size limit: -1 for unlimited – Solace’s recommendation, so a crash can be diagnosed – or a size in bytes as a plain whole number. A dump is a copy of the broker’s memory, credentials included, so 0 is the setting for a host where none may reach disk; whether one is written at all is still the host’s kernel.core_pattern. Rendered as core: in the compose file and as both Ulimit=core= and LimitCORE= in the quadlet unit. The one tunable limit: nofile and memlock stay fixed
<docker\|podman>.network.mode bridge bridge or host. bridge publishes network.ports and nothing else. It puts the broker on the engine’s own private network: a bridge on docker and rootful podman, while the quadlet states no Network= line, so rootless podman uses its rootless default (pasta, or slirp4netns before podman 5.0). Earlier builds defaulted to host, so a file that relied on that must now say mode: host or list network.ports – see the upgrade note. host is an opt-in widening (container-security.md, rule 1): the container shares the host’s network namespace and every broker listener binds directly on the host’s interfaces – SEMP on 8080 (and 1943 with TLS), SMF on 55555, SSH on 2222 and the rest of the image’s defaults – so what can reach the broker is decided by the host firewall, not by this file. Either way the container listens only on unprivileged ports (8008/1443/1943 rather than 80/443/843), which is what lets it run as a non-root user
<docker\|podman>.network.ports – (publishes nothing) What bridge publishes, passed through exactly as written and never defaulted: with no list nothing is published on the host’s interfaces, so other machines cannot reach the broker – though containers on the same engine network, and on a rootful engine the host itself, still can at the container’s own address. This tool reaches its own broker through <runtime> exec, which needs no published port; only an HA primary’s calls to its backup do. Each entry is host:container, optionally bound to one address (127.0.0.1:8080:8080, [::1]:8080:8080) and suffixed /tcp or /udp; either side may be a lo-hi range, and a container range needs a host range of the same size. Checked in either mode, since each entry is written into the quadlet unit as-is. An HA member on bridge publishes its redundancy ports itself – 8300-8302, 8741 and 55555 – and, for broker perform redundancy-test, the backup’s SEMP port (8080, or 1943 with TLS) must be reachable from the primary (operations.md). Binding a port to 127.0.0.1 keeps it off the network

The broker.* keys sit at the top level rather than under kubernetes.* because every platform runs these same post-deployment steps identically. cliScriptsDir, hostDiagnosticDir and every domainCerts path are host paths and resolve against the env file’s directory (above) – cliScriptsDir is also the only folder the script commands read, so the file name you give cli-script or shell-script is looked up there and nowhere else; productKeys holds licence strings and is never treated as a path.

Bring your own TLS Secret

kubernetes.tlsServerSecret and tls.cert/tls.certKey answer different questions – what the Secret is CALLED, and what it is built FROM. Which of them you set decides who owns the Secret, and whether there is one at all:

tls.cert + tls.certKey kubernetes.tlsServerSecret What happens
set set, or unset to derive <kubernetes.name>-tls This tool builds the kubernetes.io/tls Secret from those files under that name, broker generate prints it ahead of the CR, broker deploy applies it (offering to restart a running broker when the certificate changed), broker configure server-certs rotates it and hot-swaps the certificate over the CLI with no restart, and broker remove deletes it
unset set The Secret must already exist – created by hand, by cert-manager, or by anything else. The CR references it by name and nothing here reads, applies, rotates or deletes it. broker remove leaves it alone, and the namespace gate counts it as someone else’s
unset unset No TLS Secret and no tls block in the CR: the broker serves no TLS from a Secret

Set the pair or neither: one without the other is refused, because the Secret carries both keys and a Secret with only tls.crt in it is one the broker cannot start a listener over. Supplying the files without naming the Secret is not refused: the name is derived, so the certificate always has a Secret to land in and a tls block to reach the broker through.

broker validate states which of them it is, and whether the name was derived, so a missing Secret is not first discovered by a pod that will not mount.

tls.certPassphrase is docker/podman only. The CRD’s spec.tls carries only serverTlsConfigSecret, certFilename, certKeyFilename and enabled – there is no passphrase field and no Secret key the operator reads one from. On Kubernetes, supply an unencrypted key or decrypt it into the Secret yourself; broker validate warns when the key is set.

The admin Secret

The broker CR’s adminCredentialsSecret names the Secret holding the broker’s admin password. semp.adminPass and kubernetes.adminSecret decide which one it is, and who owns it – the three states the TLS and image-pull Secrets have:

semp.adminPass kubernetes.adminSecret What happens
set set, or unset to derive <kubernetes.name>-admin This tool builds the Secret under that name, with the monitor password and pre-shared key when those are set; broker generate prints it, broker deploy applies it and broker remove deletes it
unset set The Secret must already exist with the key username_admin_password. The CR references it and nothing here builds, applies or deletes it
unset unset The CR names no admin Secret. The operator generates <kubernetes.name>-pubsubplus-admin-creds with a random password, owns it, and deletes it with the broker

Without semp.adminPass, broker perform semp-login-check reads the password back from whichever Secret the broker uses, which needs get secrets in the namespace.

Three combinations are refused at load:

The broker reads its admin password once, on a fresh data volume. Solace applies the password key only when a broker boots with no database; changing the Secret later changes nothing in the broker. That matters most in the last state. broker remove keeps the data by default but takes the operator’s generated Secret with the broker, so a later deploy onto that data would get a new random password the broker ignores – in HA the standby and monitor pods would never become Ready, and nothing would hold the working password. So:

Upgrading moves an unset name. Earlier builds defaulted kubernetes.adminSecret to solace-admin-secret. An env file that sets semp.adminPass and no name now derives <kubernetes.name>-admin: the next deploy builds that Secret and the CR switches to it – under updateStrategy: automatedRolling the operator rolls the pods, while manualPodRestart waits for broker restart – and solace-admin-secret stays behind, where it keeps the namespace from being offered for deletion. Set kubernetes.adminSecret: solace-admin-secret to keep the old name, or delete the old Secret by hand after the redeploy.

Scaling

Every key under scaling applies to every platform. Only the delivery differs: Kubernetes writes them into the broker CR’s spec.systemScaling, while docker and podman pass them to the container as environment variables under the broker’s own setting names. One env file therefore sizes the same broker whichever platform runs it.

Every setting is settable under either spelling. The scaling key column below is the schema’s own friendly name; the destination column is the broker setting this tool emits for it, which is also a legal key in the env file – system_scaling_maxconnectioncount: 1000 works exactly like maxConnections: 1000. Both write the same value. Setting the same setting under both spellings at once fails to load, naming both keys – a file that could hold a contradiction is worse than one that refuses it.

scaling key Broker setting (container env var / CR field)
maxConnections system_scaling_maxconnectioncount
maxQueueMessages system_scaling_maxqueuemessagecount
maxKafkaBridge system_scaling_maxkafkabridgecount
maxKafkaConnections system_scaling_maxkafkabrokerconnectioncount
maxBridges system_scaling_maxbridgecount
maxSubscriptions system_scaling_maxsubscriptioncount
maxGuaranteedMsgMB system_scaling_maxguaranteedmessagesize
maxSpoolUsageMB messagespool_maxspoolusage (now the same name on every platform)

Every destination name above is the name BOTH platforms use: the key inside the CR’s spec.systemScaling, and the environment variable on docker and podman. That is a constraint rather than a coincidence, and it has a limit worth knowing – a systemd Environment= name may contain only letters, digits and underscores, so a broker setting spelled with a hyphen cannot reach a container this way and could not be added to the table above as it stands.

A key that is neither a friendly name nor a destination name – a typo, or a real broker setting this tool does not map (system_scaling_maxtransactedsessioncount, say) – fails to load exactly as an unknown key does anywhere else in this schema. cpu and messagingNodeCpu are refused by name for a different reason: scaling.cpu is fixed by the maxConnections tier and derived, so there is no key for it under either spelling (see Scaling tiers, below). <docker|podman>.container.cpuset is not an exception to that: it says WHICH cpus, not how many, and it takes its own default from the same tier.

Defaults are identical across platforms except maxConnections (100 on Kubernetes, 1000 on containers) and maxSpoolUsageMB (10000 on Kubernetes, 100000 on containers).

Scaling tiers

scaling.maxConnections is the Solace scaling tier, and it decides how much CPU the broker gets on all three platforms. The amount is not configurable: sizing a broker by connection count and then sizing its CPU independently is how a 200k-connection broker ends up on two cores. The two platforms express it differently – Kubernetes gets a CPU limit in the CR, docker and podman get a cpuset, the host cpus the container may run on – and which cpus those are is yours to change. Memory is the tier’s default and stays yours to override; storage is untouched by the tier.

scaling.maxConnections CPU cores (fixed) Container cpuset default Memory default (Kubernetes / container)
100 (Kubernetes default) 2 0-1 3410Mi / 3410m
1000 (container default) 2 0-1 6898Mi / 6898m
10000 4 0-3 12435Mi / 12435m
100000 8 0-7 30925Mi / 30925m
200000 12 0-11 52581Mi / 52581m

The value must be exactly one of those five. A value between tiers is rejected rather than rounded, because Solace publishes no sizing for it. Override memory with kubernetes.msgNode.mem (a Kubernetes quantity, Mi/Gi) or <docker|podman>.container.mem (docker’s and podman’s own b|k|m|g suffix – the engines reject Mi, so the two spellings are not interchangeable and the loader says so). Override which cpus with <docker|podman>.container.cpuset.

Docker and podman carry the tier’s caps in the generated compose file (cpuset:, mem_limit:) and quadlet unit (PodmanArgs=--cpuset-cpus=, PodmanArgs=--memory=), including a rootless quadlet, whose user slice gets the cpuset controller from the Delegate= line in the drop-in its rlimits already need. Either way an existing container deployment needs a full redeploy – not just a restart – to pick up a changed cap.

The monitor host is sized separately, and not from the tier at all: it arbitrates quorum, carries no message spool and routes no traffic, so it gets a fixed 1 cpu and 2g on docker and podman however large the tier is. Only WHICH cpu is yours to set (<docker|podman>.container.monitorCpuset, default 0); the count is fixed. Kubernetes is unaffected – the operator sizes the monitor pod there, and this tool sets only its monitorNodeStorageSize.

The container nofile, memlock and shared-memory limits are not tunable: the broker needs one specific value for each, so this tool emits them and checks the host can grant them – see The limits the container actually gets. core is the exception: <docker|podman>.container.ulimits.core defaults to unlimited and can be lowered, down to 0 for no dumps at all. A rootless podman host has further prerequisites, all checked and refused rather than fixed: see Rootless podman prerequisites. The mode itself is declared, then checked against who runs the command.

Secrets

Every secret field takes either the value itself or – through a sibling *Env key – the name of an environment variable to read it from. Setting both is an error, and so is naming a variable that is unset or empty: the load fails naming the key and the variable rather than deploying a broker with a blank password.

Value key Reference key
semp.adminPass semp.adminPassEnv
semp.monitorPass semp.monitorPassEnv
semp.additionalUsers[].password semp.additionalUsers[].passwordEnv
tls.certPassphrase tls.certPassphraseEnv
image.pass image.passEnv
redundancy.psk redundancy.pskEnv
replication.sites[].via.semp.pass replication.sites[].via.semp.passEnv
semp:
  adminPassEnv: SOLACE_ADMIN_PASS   # export SOLACE_ADMIN_PASS before any command

With the *Env form the env file carries no secret and is safe to commit and share. A value is otherwise used verbatim on every platform – a $VAR or ${VAR} inside one is a literal password, never expanded.

The pre-shared key is mandatory on docker and podman when redundancy.enabled is true and optional on Kubernetes, and it is refused at load when it is missing where it is required – the error carries the openssl rand -base64 32 command. Nothing in this tool generates it. An earlier version made one on a first HA deploy and rewrote the env file; that is gone, because it only ever ran on one host, the value still had to be copied to the other two by hand, and a deploy that edits the file it was handed is a surprise on a file that may be version-controlled. Nothing distributes a key across three container hosts, so each host’s env file must carry the same value or the group cannot form. On Kubernetes the operator generates and distributes one itself when the key is empty, and the CR’s spec.preSharedAuthKeySecret is then omitted entirely; set it and the value is written as the preshared_auth_key entry of the admin Secret this tool builds from semp.adminPass – the same Secret the admin credentials live in – and the CR points at it. So on Kubernetes a key needs semp.adminPass, and one without it is refused at load.

semp.monitorPass follows the same rule on Kubernetes, including needing semp.adminPass. Set, it is written as the username_monitor_password entry of the admin Secret this tool builds and the CR’s spec.monitoringCredentialsSecret points there. Unset, the field is omitted and the operator generates <kubernetes.name>-pubsubplus-monitor-creds with a random password of its own – naming the admin Secret without that entry would point the broker’s monitor user at a file that is not there. Nothing in this tool logs in as the monitor user, and it never enables the operator’s Prometheus exporter, that user’s only consumer. Switching between the two on a live broker changes the CR spec, which the operator applies like any other spec change – a rolling restart under updateStrategy: automatedRolling. Upgrading this tool is such a switch for an env file with no semp.monitorPass: earlier builds named kubernetes.adminSecret for the monitor user regardless, so the first broker deploy after the upgrade drops the field and rolls the pods, with no edit to the env file. Schedule that deploy, or set semp.monitorPass first to keep the CR as it was – and, if the env file names no kubernetes.adminSecret, set kubernetes.adminSecret: solace-admin-secret too, since that name moves as well (see The admin Secret).

The tool never echoes a secret. Values piped to a command on stdin show as <<< (N bytes on stdin) under -v/--verbose, values passed to a child process’s environment as NAME=***, and validate/broker status report only whether each one is set. The one exception is explicit and Kubernetes-only: there a Secret manifest IS the artifact, so broker generate and operator generate print the values they would apply. On docker and podman neither prints a secret at all – the artifact references them by name and only broker deploy handles the values. See Rendering without applying for where each secret lands at rest.

Replication

replication: describes a DR pair: two separate brokers, each message-VPN active at one site and standby at the other. It is not redundancy:, which is the three nodes of one HA group – a replicated deployment usually has both, an HA group at each site. Omit the whole section unless this broker replicates.

Two commands read it. broker configure data-replication converges THIS broker to it – the mate’s addresses, which VPNs replicate, and each one’s role – and never contacts the mate. broker perform data-replication verifies both brokers and moves roles across the pair. See Data replication for what each does and when to run it.

The block is byte-identical at both sites. Nothing in it is written from one broker’s point of view: there is no mate: key and no “my role”, both of which would have to be reversed in the other site’s file. Each broker reads its own show router-name instead, finds itself among sites[].routerNames, and whichever entry is not itself is its mate. So the same text is pasted into both env files, and a failover is one edit – change one activeAt – rather than two files kept in step.

replication:
  sites:                                   # exactly 2
    - virtualRouterName: "v:sg1"           # quote it: the colon is a YAML indicator
      routerNames: [sg1, sg1b]             # every node of this site's HA group
      endpoints:
        - { host: 10.160.132.1, port: 55443, transport: ssl }
      via:                                 # optional; read only from the OTHER site
        kubernetes: { command: kubectl --context sg, namespace: solace-sg, name: solace }
    - virtualRouterName: "v:dr1"
      routerNames: [dr1]
      endpoints:
        - { host: 10.150.132.1, port: 55443, transport: ssl }
      via:
        semp: { host: 10.150.132.1, port: 1943, tls: true, passEnv: SOLACE_DR_ADMIN_PASS }
  vpns:
    - { name: ORDERS,   activeAt: "v:sg1" }
    - { name: PAYMENTS, activeAt: "v:dr1" }
Key Purpose
replication.sites Exactly 2 entries. Replication is a pair; one site or three is refused at load
sites[].virtualRouterName The site’s key: what vpns[].activeAt references, and the literal operand the mate is given as replication mate virtual-router-name. Mandatory and never derived – the file states the exact string the broker CLI will be handed, so nothing in the mate-address path is inferred. The two sites’ values must differ. Quote it: a bare v:sg1 does parse – a colon ends a plain scalar only when a blank follows it – but quoting a value whose whole point is a literal colon leaves nothing to reason about, and it is what the annotated sample teaches
sites[].routerNames What this site’s brokers answer to, matched against their own show router-name so a broker can find itself in this file. A list, because a site is usually an HA group and the backup node reports its own name – list every node. Non-empty, and no name may appear under both sites
sites[].endpoints How the other broker dials this one. At least one, at most 2 per transport. This tool never dials them: they are rendered into the mate’s own CLI lines
endpoints[].host / .port The address, port 1-65535. Against an appliance mate every endpoint must carry the same host and a distinct transport: that grammar has one connect-via address and one non-repeatable connect-port per transport, so a second host or a second port of the same transport is refused when the lines are rendered rather than silently dropped
endpoints[].transport plainText (what an omitted transport means), compressed or ssl. encrypted is the routing name for the same thing and is refused by name, pointing at ssl
sites[].via How this tool reaches that site when it is the mate. Exactly one child, kubernetes: or semp: – the key present IS the mechanism, so a via cannot name one thing and configure another. Read only from the OTHER site’s entry: a broker takes its own access from this file’s kubernetes:/docker:/podman: section like every other command
via.kubernetes.command The cluster CLI this tool runs to reach that site. Carry the cluster in it (kubectl --context dr) rather than beside it. Restricted – see The command fields are executable content
via.kubernetes.namespace / .name The mate’s namespace, and its PubSubPlusEventBroker name, which its pod is named after
via.semp.host / .port The mate’s SEMP address. Not derived from endpoints: replication runs over the message backbone, so a reachable replication endpoint proves nothing about SEMP reachability
via.semp.tls / .insecure https rather than http, and whether to skip certificate verification for a self-signed mate. Declared, never inferred from this broker’s own posture – this hop is a WAN rather than a rack
via.semp.pass / .passEnv / .passSecret Exactly one of the three supplies the mate’s admin password, never this deployment’s semp.adminPass – a DR site is a different broker, and reusing this one’s password would fail at best and hide the mistake if the two happened to match. passSecret is {namespace, name, key} of a Kubernetes Secret, read with that site’s own via.kubernetes.command when it has one and this file’s kubernetes.command otherwise. The username is always admin, and there is no prompt for the password
replication.vpns[].name Listing a VPN enables replication for it at both sites. A VPN replicating on the broker but absent from this list has its replication shut down by broker configure data-replication – the file is authoritative. Each name appears once
replication.vpns[].activeAt Which site holds the active role for that VPN; the other is standby. It names a virtualRouterName, not a router name

via is optional, and a file whose sites declare none is valid. Only broker perform data-replication needs it, and it checks both sites’ blocks in its own preflight before anything is written; requiring it at load would refuse a file that configures replication perfectly well with the local-only command. A via: key with nothing under it decodes to the same value as an absent one, so it is accepted here too and the switch command is what reports a site it cannot reach.

Everything else is checked at load: two sites, a virtualRouterName on each and no duplicate, non-empty non-overlapping routerNames, at least one endpoint with a valid port and transport and no more than two per transport, VPN names that appear once, and an activeAt naming a declared site. A partially written block is an error rather than a half-configured switchover waiting to happen.

The command fields are executable content

kubernetes.command, docker.command, podman.command, docker.compose and each replication.sites[].via.kubernetes.command name a binary this tool runs on your machine. Env files travel – repositories, pull requests, shared archives – so the person who wrote one is routinely not the person who runs it. Treat an env file the way you would treat a script someone sent you: read the command fields before running anything with it.

To make that review short, the fields are restricted. A command is accepted only when:

  1. Every token is inert and visible. No control characters, no whitespace inside a single argument (any Unicode whitespace, not just the ASCII space), no invisible formatting characters (zero-width spaces and joiners, bidirectional overrides), no quotes, no backslash, no backtick, and none of $ ; | & < > ( ) * ? [ ] { } # !. Nothing is ever passed through a shell, so these are not injections – but tokens end up in logs and in the -v/--verbose exec trace, and a token you cannot see is one you cannot review. A Windows path in a flag value therefore needs forward slashes: --kubeconfig C:/Users/you/.kube/config.
  2. A leading ~ is expanded, not refused – in every token but the first, to the home directory of whoever runs the tool, exactly as in a path key. So command: oc --kubeconfig ~/solace/kubecontext works. A tilde anywhere else in a token is an ordinary character (an 8.3 short name such as C:/Users/RUNNER~1/.kube/config is what expansion itself produces on Windows), and a ~ that is still leading when the guard runs is refused – nothing downstream would expand it.
  3. The binary is a bare name from the allowlist. No / or \ anywhere in it, and no leading ~ either – it is the one token that is not expanded, because a path would run a file the env file chose – such as a ./kubectl unpacked beside it – rather than the one on your PATH. One optional .exe is stripped, then the name must be:

    Platform Allowed
    Kubernetes kubectl, oc
    Docker docker, docker-compose, nerdctl
    Podman podman

    A replication site’s via.kubernetes.command is always held to the Kubernetes row whatever platform this end runs on: the mate may sit in a cluster while the local broker runs on docker, and the binary being run is a cluster CLI either way.

  4. Nothing after it is a bare word. Flags and their values are fine (kubectl --context prod -n solace); a bare word is not, because this tool appends its own subcommand and a word in that position would run ahead of it. kubectl delete in a config is exactly the attack. The literal -- is refused for the same reason.

    One acknowledged gap, in that last rule. The check cannot know how many values a flag takes – that would mean carrying a table of every flag of every allowed CLI, which would rot as those CLIs change – so the token after any flag is accepted as that flag’s value. After a flag that takes no value, that token is not a value at all, and it lands in subcommand position after all:

    kubernetes.command: kubectl --insecure-skip-tls-verify delete   # accepted; runs `delete`
    docker.compose: docker-compose --verbose down                   # the container equivalent
    

    This is a known limit, not an oversight, and it is why the guarantee is stated narrowly: argv[0] and every bare token are checked; the contents of a flag value are not. It costs an attacker nothing more than the access they already need – anyone who can edit the env file can also point a perfectly legitimate kubectl at your production cluster, which no check in this file can detect. Both are review problems: read an env file’s command fields the way you would read a script it ships.

Anything else – a wrapper such as microk8s kubectl or lima nerdctl, a site-specific shim – runs only when you approve it, per invocation:

solace-util broker deploy --allow-command microk8s   # kubernetes env file wrapping kubectl in microk8s
solace-util broker deploy --allow-command lima       # docker/podman env file wrapping the runtime in lima

--allow-command is repeatable, takes a bare name (never a path), and exists only as a command-line flag. There is deliberately no env-file key, environment variable, or any other way for a config to widen its own allowlist: the authority to run something unusual belongs to the person who can see what they are approving. It is rejected on any generate command, where nothing executes.

Privilege escalation is never approvable, by the config or by you: sudo, sudoedit, doas, su, pkexec, run0, systemd-run, machinectl, setpriv, capsh, unshare, nsenter, runas and gsudo are refused as --allow-command values, in any casing (Sudo and SUDO.exe too – Windows and a default macOS filesystem resolve those to the same binary, so matching them exactly would have let a capital letter through a floor that is supposed to stop everyone). The allowlist in rule 2 above is the opposite: it stays case-sensitive on purpose, because KUBECTL is a genuinely different file where filesystems say it is, and a positive match that folded case would approve a binary nobody listed. This is not a ban on running as root – rootful podman needs it. It is about where you elevate. A command: sudo podman elevates every command this tool issues, for the whole life of an env file, decided by whoever wrote that file. Elevate the tool instead, at the moment you run it, so the privilege belongs to one invocation you chose:

sudo solace-util broker deploy -e prod.yaml   # yes (prod.yaml is a podman env file)
# command: sudo podman  in the env file      # never

The same check runs twice – once when the env file is loaded, and again immediately before any command line is built – from a single implementation, so a hostile file is inert even on a path that skipped validation.

What this does not protect against. Two things are out of scope, and no amount of parsing would fix either:

Rendering executes nothing: every generate command only ever calls the templating package, never an external command, so pointing one at an env file you did not write cannot run anything. Note that it still loads the file, so one whose command field breaks the rules above fails there rather than printing an artifact – which is itself the answer you wanted about that file. To read a command field without loading anything at all, open the file.

Keys that were renamed

An env file written for an earlier build still decodes these keys, and the load then fails naming the replacement. That is deliberate: a bare “unknown field” error would tell you the key is wrong without telling you what to write instead.

Old key Now
kubernetes.runtime kubernetes.command
docker.runtime docker.command
podman.runtime podman.command
broker.cliScriptsFolder broker.cliScriptsDir
broker.diagDir broker.hostDiagnosticDir
broker.domainCerts.folder broker.domainCerts.dirs (see below)

The three command renames make the platform blocks agree with replication.sites[].via.kubernetes.command, which always spelled it that way.

broker.domainCerts.folder changed shape as well as name, so the error says so. dirs is a list of directories to walk, each entry either a plain path or a mapping with path and an optional comma-separated fileExt (default .cer, .crt, .pem); files now takes a full host path as its value rather than a bare filename that used to be joined onto folder. A certificate loaded from dirs is named <last directory element>_<filename>, which is what files exists to override when you want a name of your own or one file out of a directory of many.

Some keys were removed rather than renamed. Each still decodes and then fails to load naming why, for the same reason the renames do:

Removed key Why, and what to do instead
kubernetes.msgNode.cpu Broker CPU is fixed by the scaling tier – see Scaling tiers
scaling.maxPool Named the same broker setting as scaling.maxSpoolUsageMB; use that
<docker\|podman>.container.shmSize /dev/shm is fixed at 2g, which is what the broker needs
<docker\|podman>.container.ulimits.nofile, .ulimits.memlock Fixed at 2448:1048576 and -1. The host is checked against them – see The limits the container actually gets. ulimits.core was NOT removed: it is live, see The keys
kubernetes.containerSecurity.readOnlyRootFilesystem A read-only root filesystem is not supported on any platform, so the key is refused whether it says true or false; the operator’s default, a writable root, applies. runAsUser and runAsGroup are unaffected

Migrating from the bash env files (solace-util convert)

The pre-Go scripts kept their configuration in shell files under bash/env/, sourced by 000-env.sh. solace-util convert turns one into the YAML this CLI reads:

solace-util convert bash/env/prod -o prod.yaml                 # kubernetes flavour
solace-util convert bash/docker-podman/env/prod -o prod.yaml   # docker/podman flavour
solace-util validate -e prod.yaml

The output carries every secret from the source file verbatim – treat it like the source, and never commit it. (Switch the values to their *Env reference keys afterwards and it becomes safe to commit; see Secrets.) SOLBK_USR_SECRET converts to kubernetes.adminSecret (SOLBK_ADM_SECRET is accepted as an alias for the same key; when both are set and disagree, the canonical SOLBK_USR_SECRET wins with a warning; when neither is set, solace-admin-secret is written out – the bash tool’s default, which the legacy broker was built under, where this schema would derive <kubernetes.name>-admin), SOLBK_SVR_SECRET to kubernetes.tlsServerSecret, IMAGEREPO_SECRET to kubernetes.imagePullSecret (on a docker/podman conversion those two are dropped with a warning naming that kubernetes-only home – they name Kubernetes Secret objects, which have no container equivalent), and each SOLBK_USR_PASS entry to an semp.additionalUsers entry with accessLevel: none – the bash flow set no level, so the converter picks the least privileged one and says so; raise it per user as needed.

kubernetes.msgNode.cpu and scaling.maxPool fail to load, each naming its replacement. Broker CPU is fixed by the scaling tier rather than set by hand, and maxPool would name the same broker setting as scaling.maxSpoolUsageMB – one concept under two platform-specific keys, which the scaling block keeps under a single name today. That is not the same thing as the dual-spelling alias every other scaling setting now gets (see Scaling): maxPool and maxSpoolUsageMB had no defined winner if a file set both, where an alias pair does – setting both spellings of one setting fails to load, naming both. kubernetes.msgNode.mem is unaffected. Docker always deploys through compose, so there is no docker.mode key to choose a mode with; a file still carrying it fails strict decoding as an unknown field. See Scaling.