Broker management and delivery configuration
The running PSSP Broker independently authenticates clients, authorizes topic and delivery access, and owns SQLite. WebService and other provisioning applications communicate only with the encrypted management listener.
This implementation deliberately has no Broker clustering, replication, quorum, partition ownership, or cross-node coordination. Run one authoritative low-latency Broker and connect multiple remote worker/client instances to it. Multiple WebService workers are supported; multiple Broker replicas are not.
Security and storage boundary
| Component | What it knows | What it must not do |
|---|---|---|
| PSSP Broker | SQLite path, Argon2id hashes, ACLs, subscription grants, management allow-list, audit actor identity. | Call WebService or interpret application payloads. |
| WebService | Management host/port, its service ID and Ed25519 private key, pinned Broker management public key. | Open SQLite, receive its path, launch the Broker executable, or validate PSSP client passwords. |
| PSSP client/worker | Its one-time client ID, username, password, normal PSSP endpoint, and pinned data-plane Broker public key. | Select unauthorized delivery mode/group or create another principal. |
The Broker takes an exclusive operating-system lock on <sqlite_path>.lock for its lifetime. Offline administration fails while the Broker is active. The setup command is the bootstrap exception and must run before serve.
Complete AES-only TOML example
Use different X25519 private keys for normal PSSP traffic and management traffic. Replace every placeholder and keep the file readable only by the Broker service account.
[listener]
tcp_bind = "0.0.0.0:6688"
udp_bind = "0.0.0.0:6688"
[tls]
enabled = false
[encryption]
enabled = true
static_private_key = "REPLACE_WITH_DATA_PLANE_X25519_PRIVATE_KEY"
[transport]
allow_insecure_tcp_udp = false
[limits]
max_connections = 2000
max_header_bytes = 1024
max_payload_bytes = 65536
max_outbound_queue_bytes = 4194304
idle_timeout_seconds = 120
subscriber_resume_ttl_seconds = 300
[storage]
sqlite_path = "/var/lib/pssp/pssp-broker.sqlite3"
unix_owner_user = "pssp"
unix_owner_group = "pssp"
auth_cache_seconds = 30
auth_cache_entries = 4096
[topic_defaults]
max_bytes = 67108864
max_messages = 10000
max_age_seconds = 600
[management]
enabled = true
bind = "127.0.0.1:6690"
encryption_enabled = true
static_private_key = "REPLACE_WITH_MANAGEMENT_X25519_PRIVATE_KEY"
max_request_bytes = 65536
authentication_timeout_seconds = 10
request_timeout_seconds = 15
[[management.allowed_services]]
service_id = "oxrecorder-webservice"
enabled = true
ed25519_public_key = "REPLACE_WITH_RAW_32_BYTE_ED25519_PUBLIC_KEY_BASE64URL"
permissions = ["principal.provision", "principal.delete"]
principal_kinds = ["device"]
client_id_patterns = ["ox-device-*"]
username_patterns = ["device-*"]
allowed_delivery_modes = ["broadcast"]
allowed_share_groups = []
publish_topic_patterns = [
"/api/devices/*/heartbeat",
"/api/devices/*/audio/chunks",
]
subscribe_topic_patterns = ["/api/devices/*/commands"]
[[management.allowed_services]]
service_id = "worker-deployment-controller"
enabled = true
ed25519_public_key = "REPLACE_WITH_SECOND_RAW_ED25519_PUBLIC_KEY_BASE64URL"
permissions = ["principal.provision", "principal.delete"]
principal_kinds = ["worker"]
client_id_patterns = ["ox-*-worker-*"]
username_patterns = ["worker-*"]
allowed_delivery_modes = ["shared"]
allowed_share_groups = ["audio-workers", "device-sync-workers"]
publish_topic_patterns = ["/api/devices/+/commands"]
subscribe_topic_patterns = [
"/api/devices/+/heartbeat",
"/api/devices/+/audio/chunks",
]
Management field reference
| Field | Meaning |
|---|---|
enabled | Starts the independent management TCP listener. When false, no remote provisioning endpoint exists. |
bind | Management address. Prefer loopback, a private management interface, or a firewall-restricted address; do not expose it publicly. |
encryption_enabled | Must be true. Management traffic always uses X25519/HKDF and AES-256-GCM without TLS. |
static_private_key | Base64url-without-padding 32-byte X25519 private key for management. It is separate from [encryption].static_private_key. |
max_request_bytes | Maximum decrypted JSON management request, 1 to 1,048,576 bytes. |
authentication_timeout_seconds | Time allowed to answer the Broker challenge. |
request_timeout_seconds | Maximum silent interval while an authenticated management connection waits for its next request. |
Allowed-service fields
| Field | Meaning |
|---|---|
service_id | Stable application identity included in the signed challenge and credential audit actor. |
enabled | Emergency allow/deny switch. Unknown or disabled services are rejected before any database work. |
ed25519_public_key | Raw 32-byte Ed25519 public key encoded as base64url without padding. The matching private key remains with the service. Follow the generation steps below; never paste the PEM private key here. |
permissions | principal.provision, principal.delete, principal.list, and/or principal.update. The desktop Access tab needs principal.list to review accounts, principal.provision to add them, principal.update to change them, and principal.delete to remove them. No permission implies denial. |
principal_kinds | Principal kinds the service may create. Management never permits creation of administrator. |
client_id_patterns, username_patterns | Simple glob allow-lists where * matches zero or more characters. Every requested identity must match. |
allowed_delivery_modes | broadcast, shared, or both. The service cannot assign a mode outside this list. |
allowed_share_groups | Exact group names a service may assign. Required when shared is allowed. |
publish_topic_patterns, subscribe_topic_patterns | Topic-scope allow-lists. A segment containing only * matches one concrete segment. Use a literal + when authorizing a worker wildcard filter. A literal # grants an explicitly trusted service all valid PSSP filters. |
Create the Ed25519 key for an allowed service
Each [[management.allowed_services]] entry needs a distinct Ed25519 key pair. The Broker stores the public key in TOML; the management application keeps the private PEM and signs its requests with it.
- Choose a protected location for the service private key. For a local desktop operator, run this from the repository root:
The command createscd broker cargo run -- generate-management-service-key \ --private-key-file config/desktop-operator-ed25519.pemconfig/desktop-operator-ed25519.pemwith owner-only permissions and prints one base64url public key. - Copy only the printed value into the matching service entry:
[[management.allowed_services]] service_id = "desktop-operator" ed25519_public_key = "PASTE_THE_PRINTED_PUBLIC_KEY_HERE" - Keep the PEM with the service that connects to the management listener. Do not commit it, put it in
pssp.toml, or send it to the Broker. To print the public key again later, without displaying the private key, run:cd broker cargo run -- management-service-public-key \ --private-key-file config/desktop-operator-ed25519.pem - If the PEM is replaced, generate a new public key, update
ed25519_public_key, and restart the Broker. The old private key will no longer authenticate.
Generate and protect keys
- Generate the Broker data-plane and management X25519 private keys separately.
pssp-broker generate-aes-only-private-key pssp-broker generate-aes-only-private-key - Derive each public key for pinning. The command accepts a protected file or hidden terminal prompt.
pssp-broker aes-only-public-key --private-key-file /run/secrets/pssp-management-x25519 - Generate one Ed25519 identity for each management application. The command writes a mode-0600 PKCS#8 PEM private key and prints the raw public key for TOML.
pssp-broker generate-management-service-key \ --private-key-file /run/secrets/pssp-webservice-ed25519.pem - Print the public key again later without exposing the private key.
pssp-broker management-service-public-key \ --private-key-file /run/secrets/pssp-webservice-ed25519.pem
Never copy a service private key into Broker TOML. Never put the Broker private keys in WebService environment variables. Distribute only public pins through a trusted deployment channel.
Management authentication sequence
- The Broker sends a fresh random challenge and its management X25519 public key.
- The service verifies the public key equals its pinned value, creates an ephemeral X25519 key and salt, and signs the canonical service ID, challenge, Unix timestamp, ephemeral public key, and salt with Ed25519.
- The Broker finds the exact enabled
service_id, verifies the signature with configured public key, checks ±30 seconds clock skew, and derives a session key with X25519/HKDF-SHA256. - Both sides use sequenced AES-256-GCM frames. Replayed, reordered, modified, unsigned, unknown, or disabled-service requests are rejected.
- For every request the Broker rechecks permission, principal kind, identity pattern, topics, delivery mode, and share group before its internal credential store opens a transaction.
Successful provisioning creates or rotates the principal atomically, replaces ACLs and delivery grants, stores only an Argon2id password hash, records the authenticated service_id in audit, and returns the generated plaintext password once.
Provisioning request and scope examples
{
"operation": "provisionPrincipal",
"clientId": "ox-device-OXRC-001",
"username": "device-OXRC-001",
"principalKind": "device",
"passwordBytes": 32,
"publish": [
"/api/devices/OXRC-001/heartbeat",
"/api/devices/OXRC-001/audio/chunks"
],
"subscribe": ["/api/devices/OXRC-001/commands"],
"deliveryGrants": [{
"topicFilter": "/api/devices/OXRC-001/commands",
"deliveryMode": "broadcast",
"shareGroup": ""
}]
}
The WebService service above can create only device identities with those exact device topic shapes and broadcast command delivery. It cannot create an administrator, grant #, create a worker, join audio-workers, or delete an identity outside its configured patterns.
If an older management client omits deliveryGrants, the Broker creates broadcast grants for its subscribe ACLs. Shared-worker provisioning must always send explicit delivery grants. If a normal PSSP client omits shareGroup and the principal has exactly one grant for that filter, the Broker applies that grant automatically; the client cannot override it.
{
"operation": "provisionPrincipal",
"clientId": "ox-audio-worker-a",
"username": "worker-a",
"principalKind": "worker",
"publish": [],
"subscribe": ["/api/devices/+/audio/chunks"],
"deliveryGrants": [{
"topicFilter": "/api/devices/+/audio/chunks",
"deliveryMode": "shared",
"shareGroup": "audio-workers"
}]
}
workerID, shared workers, and broadcast clients
clientId authenticates and authorizes a principal. workerID is the durable ACK/resume identity supplied in SUBSCRIBE. The connection ID identifies only the current TCP session. shareGroup identifies competing workers, but the Broker accepts it only when an exact persisted delivery grant authorizes it.
| Connection | workerID | Grant | Result for one message |
|---|---|---|---|
| A | audio-worker-a | shared / audio-workers | Eligible shared worker. |
| B | audio-worker-b | shared / audio-workers | Eligible shared worker. |
| C | audio-worker-c | shared / audio-workers | Eligible shared worker. |
| D | viewer-d | broadcast | Always receives its independent copy. |
For A/B/C/D, the Broker sends each message to exactly one of A, B, or C by local round-robin and also sends a copy to D. A/B/C share one group cursor. If the selected worker disconnects before MSGACK, the pending QoS 1 message is reassigned to another connected group member. D owns a cursor keyed by its workerID and can reconnect with from: "resume".
Multiple broadcast worker IDs each receive a copy; different shared groups each receive one copy. A worker cannot omit its configured group to gain broadcast access, and a viewer cannot name a worker group. Delivery is at least once, so workers must make processing idempotent. A slow or full outbound client queue does not block publisher acknowledgement or other subscribers.
The Broker temporarily accepts the old subscriberId header as an input alias. Updated clients send workerID; new integrations must use workerID.
WebService configuration
PSSP_DEVICE_PROVISIONING_ENABLED=true
PSSP_MANAGEMENT_HOST=127.0.0.1
PSSP_MANAGEMENT_PORT=6690
PSSP_MANAGEMENT_SERVICE_ID=oxrecorder-webservice
PSSP_MANAGEMENT_PRIVATE_KEY_FILE=/run/secrets/pssp-webservice-ed25519.pem
PSSP_MANAGEMENT_BROKER_PUBLIC_KEY=PINNED_MANAGEMENT_X25519_PUBLIC_KEY
PSSP_MANAGEMENT_TIMEOUT_MS=15000
PSSP_DEVICE_CLIENT_ID_PREFIX=ox-device
PSSP_DEVICE_USERNAME_PREFIX=device
After POST /api/me/devices completes registration, WebService requests one device principal from the Broker. The response contains pssp_client_id, pssp_username, and the one-time pssp_password. WebService returns them to the device but does not store the plaintext password. Device deletion requests deletePrincipal through the same authenticated listener.
The old PSSP_BROKER_BINARY, PSSP_BROKER_CONFIG, and PSSP_BROKER_WORKING_DIRECTORY settings are removed. They must not be restored.
Initialization, migration, and startup
- Back up the existing SQLite file and stop the old Broker.
- Add
[management]and explicit allowed services to the protected TOML. Generate all keys and install service private keys separately. - Run setup once if this is a new database.
pssp-broker setup --config /etc/pssp/pssp.toml \ --bootstrap-client-id pssp-admin --bootstrap-username admin - Start
pssp-broker serve. Schema version 1 databases migrate to version 2 by creating structured delivery grants for existing subscriptions in broadcast mode. - Configure WebService with management endpoint and keys, then restart it. Confirm it has no SQLite/Broker binary/config path.
- Provision worker principals through a separately authorized deployment controller. Give every worker instance a unique
workerIDand the same approvedshareGroup.
The supplied offline credential CLI remains useful before service startup. Once the Broker holds its SQLite lock, direct commands fail and applications must use the authenticated management listener. Treat operating-system access capable of stopping the Broker or reading its protected files as host-administrator access.
Security failures and troubleshooting
| Error/observation | Check |
|---|---|
| Management connection rejected immediately | Service ID exists and is enabled; Ed25519 public/private keys match; clocks differ by no more than 30 seconds. |
pssp_management_broker_public_key_pin_mismatch | The WebService public pin is not derived from management.static_private_key. Do not bypass the check. |
management_not_authorized | Permission, principal kind, identity glob, topic pattern, delivery mode, or share group is outside the allowed-service scope. |
subscription_delivery_not_authorized | The authenticated principal lacks an exact structured grant for requested filter plus broadcast/shared group. |
| Offline CLI says Broker owns SQLite | The service is running as designed. Use the management endpoint, or stop it only for approved maintenance/setup. |
| Only one of A/B/C receives data | Expected for the same shared group. D needs an independent broadcast grant to receive a separate copy. |
| Duplicate processing after failure | Expected at-least-once behavior. Deduplicate by topic sequence/application message ID before committing side effects. |
| High audio latency | Do not add Broker replicas. Inspect worker queue depth, payload processing, network path, and per-session outbound capacity on the single Broker. |