# Job & Plugin: MQTT Topic Permissions

**Audience:** Integrators configuring MQTT topic permissions for job tasks or managed plugins.

This page is a consolidated reference for how the MQTT broker enforces topic permissions for integrator-authored components. For the narrative explanation of how MQTT fits into the broader integration flow, see Step 3 of the [Custom Job Integration Guide ](/keeperpam/endpoint-privilege-manager/integrations/custom-job-guide.md)and Step 2 of the [Custom Plugin Integration Guide](/keeperpam/endpoint-privilege-manager/integrations/custom-plugin-guide.md).

## How Enforcement Works

Topic permissions are declared in the component's JSON file and enforced by the broker at runtime. The enforcement model has four stages:

**Declaration.** A job declares permitted topics in `mqttTopics` on the job root object. A plugin declares them in `metadata.mqttTopics` in the plugin JSON.

**Connection.** When a component connects to the broker, the broker validates the client identity against the launched-process registry and resolves which job or plugin the connection belongs to based on the client ID format.

**Enforcement.** Every publish and subscribe call is checked against the declared allowed lists for that component. Calls to undeclared topics are denied. The MQTT connection itself is not dropped — only the individual operation fails.

**Silent denial.** A denied publish or subscribe does not produce a client-visible error in most cases. If operations appear to succeed at the connection level but messages are not arriving, a missing topic declaration is the first thing to check.

## Jobs

Topic permissions for job tasks are declared on the job root object — not inside the task definition:

```json
"mqttTopics": {
  "allowedPublications": ["KeeperLogger"],
  "allowedSubscriptions": []
}
```

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="209.333251953125">Field</th><th>Purpose</th></tr></thead><tbody><tr><td><code>allowedPublications</code></td><td>Topics the task may publish to. Must include every topic the binary publishes — <code>KeeperLogger</code>, <code>eventTopic</code>, or any custom topic.</td></tr><tr><td><code>allowedSubscriptions</code></td><td>Topics the task may subscribe to. Empty for most job tasks that only publish logs.</td></tr></tbody></table>

When `mqttTopics` is present with at least one entry, the agent injects `KEEPER_JOB_ID` and `KEEPER_JOB_NAME` as environment variables before starting the task. These are required to form the correct MQTT client ID.

If your task does not use MQTT at all, you may omit `mqttTopics` entirely. Doing so also means `KEEPER_JOB_ID` and `KEEPER_JOB_NAME` are not injected.

## Plugins

Topic permissions for managed plugins are declared under `metadata` in the plugin JSON:

```json
"metadata": {
  "mqttTopics": {
    "publish": ["KeeperLogger", "MyPlugin/Responses/+"],
    "subscribe": ["MyPlugin/Commands/+"]
  }
}
```

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="109.99993896484375">Field</th><th>Purpose</th></tr></thead><tbody><tr><td><code>publish</code></td><td>Topics the plugin may publish to.</td></tr><tr><td><code>subscribe</code></td><td>Topics the plugin may subscribe to beyond the primary <code>Subscription.Topic</code>.</td></tr></tbody></table>

The primary `Subscription.Topic` is a separate field from `metadata.mqttTopics.subscribe`. Both are subscribed — declare additional topics in `metadata.mqttTopics.subscribe` alongside the primary subscription, not instead of it.

## Side-by-Side Comparison

<table data-header-hidden><thead><tr><th width="202.333251953125"></th><th width="236.4444580078125"></th><th></th></tr></thead><tbody><tr><td></td><td>Jobs (<code>Jobs/*.json</code>)</td><td>Plugins (<code>Plugins/*.json</code>)</td></tr><tr><td>Declaration location</td><td><code>mqttTopics</code> on the job root</td><td><code>metadata.mqttTopics</code> inside the plugin JSON</td></tr><tr><td>Publish field name</td><td><code>allowedPublications</code></td><td><code>publish</code></td></tr><tr><td>Subscribe field name</td><td><code>allowedSubscriptions</code></td><td><code>subscribe</code></td></tr><tr><td>Primary subscription</td><td>Not applicable</td><td>Declared separately as <code>Subscription.Topic</code></td></tr><tr><td>MQTT client ID format</td><td><code>{KEEPER_JOB_ID}_{Token}_{Pid}</code></td><td><code>{PluginName}_{Pid}</code> or <code>{PluginName}_{UserName}_{Pid}</code></td></tr><tr><td>Environment variables</td><td><code>KEEPER_JOB_ID</code>, <code>KEEPER_JOB_NAME</code> injected when <code>mqttTopics</code> is set</td><td>Not injected via environment; plugin reads its own identity from its <code>id</code></td></tr></tbody></table>

## MQTT Client IDs

The broker uses the client ID format to associate a connection with a job or plugin and apply the correct permission set. Using the wrong format causes permission checks to fail even when the topic declarations are correct.

**For job tasks:**

```
{KEEPER_JOB_ID}_{ExecutableToken}_{ProcessId}
```

* `KEEPER_JOB_ID` comes from the environment variable the agent injects
* `ExecutableToken` is a short, stable name for your binary — no underscores, no path characters
* `ProcessId` is the current OS process ID as a decimal integer
* The job `id` must not contain underscores — the broker splits on `_` to extract the job ID segment

Example: `my-scanner_SecretScanner_48292`

**For managed plugins:**

```
{PluginName}_{ProcessId}                      (service account)
{PluginName}_{UserName}_{ProcessId}           (user session)
```

* `PluginName` should align with the plugin `id`
* Do not use the job triple format for a plugin — the broker applies different permission logic based on client ID format

## Wildcards

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="106.333251953125">Pattern</th><th width="275.333251953125">Scope</th><th>When to Use</th></tr></thead><tbody><tr><td><code>+</code></td><td>Matches exactly one topic level</td><td>Command and response patterns where subtopics vary by type: <code>Commands/+</code> matches <code>Commands/scan</code> and <code>Commands/reset</code> but not <code>Commands/a/b</code></td></tr><tr><td><code>#</code></td><td>Matches all levels below a prefix</td><td>Broad monitoring or logging patterns — use sparingly and only where required</td></tr></tbody></table>

Prefer explicit topic strings in production. Wildcards are convenient during development but reduce what the broker can enforce. For a bounded command set, list each topic explicitly:

```json
"publish": [
  "KeeperLogger",
  "MyPlugin/Responses/scan",
  "MyPlugin/Responses/reset"
]
```

## KeeperLogger

`KeeperLogger` is the well-known topic for structured log delivery. Any job task or plugin that publishes structured log messages must include it in the publish list. The payload must follow the `RequestMessage` JSON format — see [Step 5](https://claude.ai/integration/custom-job-guide#step-5-publish-log-messages-to-keeperlogger) in the Custom Job Integration Guide for the complete format and a code example.

Do not publish to product-internal topics (policy, API, and similar) unless your administrator has explicitly extended permissions for your component.

## Common Failures

<table data-header-hidden="false" data-header-sticky><thead><tr><th>Symptom</th><th>Cause</th><th>Fix</th></tr></thead><tbody><tr><td>Publish silently fails</td><td>Topic not in <code>allowedPublications</code> / <code>publish</code></td><td>Add the topic to the correct declared list</td></tr><tr><td>Subscribe silently fails</td><td>Topic not in <code>allowedSubscriptions</code> / <code>subscribe</code></td><td>Add the topic to the correct declared list</td></tr><tr><td><code>eventTopic</code> publishes denied</td><td><code>eventTopic</code> string not present in <code>allowedPublications</code></td><td>Add the exact <code>eventTopic</code> string to <code>allowedPublications</code> — the strings must match exactly</td></tr><tr><td>Permission check fails despite correct declarations</td><td>Wrong client ID format — job triple used for plugin or vice versa</td><td>Confirm client ID format matches the component type</td></tr><tr><td>MQTT connection refused entirely</td><td>Process not in launched-process registry</td><td>Ensure binary is started by the agent (job runner or orchestrator), not manually; add retry on startup</td></tr></tbody></table>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.keeper.io/keeperpam/endpoint-privilege-manager/reference/job-and-plugin-mqtt-topic-permissions.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
