Security model
lockmq lets an AI agent inspect a RabbitMQ broker without holding its credentials or connecting to it. This page says what it protects, how, and where it stops.
It summarises docs/security-model.md, which is the reference.
Principles
- Two processes, one key holder. The console holds the credentials and the only broker connections; the clients (CLI, MCP server) hold neither.
- The console is the enforcement point. Skills, rules and tool descriptions only guide the agent.
- The AI tightens, the human loosens. Any change that widens what the agent can do takes effect only after you confirm it in the console.
- Fail closed. An unknown operation, field, body format or rule is refused or treated as the most restrictive case.
- Defence in depth. The broker user’s own permissions back the allowlist where RabbitMQ allows it.
- Results are data, not instructions. Everything read from the broker is framed as untrusted.
- Reading a message is not always free. An operation that can change what a consumer receives (
peek) needs its own tier, set by you, and your approval every time, with its side effects on the screen.
Boundaries
| Boundary | Enforced by |
|---|---|
| Agent ↔ console | a separate OS account (lockmq install); a Unix socket in a directory only the clients group reaches; the kernel’s peer credentials checked on every connection; the console refuses to start without the separated setup |
| Console ↔ human | approval screens in the console’s own terminal; on production the typed profile name; a 5-minute timeout means denied |
| Console ↔ broker | an allowlist of operations and endpoints; one broker user; TLS verify-full by default; an optional ssh tunnel opened by the console |
| Policy | config files read every 2 s; loosenings held until confirmed; the approved policy stored in the console account’s state |
What lockmq sends to the broker
- Management API:
GETon overview, nodes, feature flags, queues, one queue and its bindings, exchanges, bindings, policies, operator policies, consumers, connections, channels, whoami and runtime parameters; plus, under tierpeekonly,POST /api/queues/{vhost}/{name}/getwithackmode = "ack_requeue_true". - Stream protocol: an unnamed consumer (no stored offset, no single-active-consumer group), metadata and
QueryOffset. - AMQP 0-9-1:
basic.qos,basic.consumewithx-stream-offseton a stream, manual acks,basic.cancel.
Never: /api/definitions, /api/users* (password hashes), other users’ /api/permissions*, any other non-GET endpoint, basic.get, basic.publish, a declare, a delete, a purge, store_offset, ack_requeue_false or reject_requeue_false. Every management URL path segment is escaped.
The broker user
Recommended: tag monitoring, permissions configure "^$", write "^$", and read on what lockmq may read. With configure and write empty, the broker refuses publish, declare, delete and policy changes from that user (tested on RabbitMQ 3.13 and 4.1).
The read permission also allows a purge
RabbitMQ authorises queue.purge (and the management API’s DELETE …/contents) with read alone. lockmq never purges, but the broker user cannot back that up: grant read on the streams only (^(order-events|payment-events)$), and on queues only for profiles with tier peek.
A monitoring user cannot read runtime parameters nor its own permissions: policies says the parameters are not visible, and the console’s privilege audit relies on the user’s tags only. At start and at each reconnection the console reads whoami: a user tagged administrator or policymaker is refused on production and needs a typed continue elsewhere.
Credentials
Asked at console start (echo off), read from the console account’s OS keychain, or read from Vault at every connection (the console never handles the Vault token; a lease is renewed by reconnecting before it ends). They never appear in argv, the environment, a file, the socket, the audit log or an error: errors are generic, URIs are built without userinfo (AMQP and stream logins go through SASL), and every error text that leaves the console is scrubbed of known secrets, URI userinfo and Authorization headers. URIs inside runtime parameters (shovels, federation) lose their userinfo before they reach a client.
Message content
- Approval. Every
readandpeekshows a screen: the queue or stream, the starting point and count, what moves (nothing, for a stream read), the masking in force, the agent’s intent. Auto mode (non-production only) approvesread, neverpeek. - Masking. JSON bodies are masked per leaf:
redact-unknown(the production default) redacts every string no allow rule names;detectuses mask rules, field-name heuristics and value detectors. Headers and properties follow rules and heuristics. Bodies that are binary, compressed, invalid JSON, deeper than 64 levels or larger than four timesmax_body_bytesbecome<bytes:N sha256:H type:T>; so does a body without content type that looks like JSON but does not parse (underredact-unknown), and any body the broker cut short. Clear output: Ctrl-O prints results in clear in the console only; unmasked output to the agent needs:unmaskin the console and your approval of every such request. - Size. At most
read_max(≤ 200) orpeek_max(≤ 20) messages, each body cut atmax_body_bytes, the answer cut atmax_output_bytes. - Audit. One JSON line per event in the console account’s
~/.local/state/lockmq/audit.log(mode 0600), with the operation, vhost, target, message count, the agent’s intent and the client’s uid. Never a message body, header or property value.
Threats
| Threat | Mitigation |
|---|---|
| Agent changes broker state | operation allowlist; GET only plus the get of peek; no publish, declare, purge, store_offset or destructive ack mode; broker user with empty configure/write |
| Peek dead-letters or drops a message | off by default; tier per profile set by the human; delivery limit read first, refusal near it; side effects on the screen; never auto-approved |
| Agent turns peek on | read → peek is a loosening: no effect until confirmed in the console; no IPC method loosens |
| A read moves a real consumer | unnamed stream consumer; never store_offset |
| Password hashes or URI passwords leak | those endpoints never called; userinfo stripped from every URI |
| PII in bodies of unknown shape | redact-unknown on production; detectors; proposals of mask rules |
| Prompt injection in payloads | results framed as untrusted data |
| Broker overload | paginated lists with selected columns, one request at a time, request_timeout, read_max |
| Agent reads the console’s terminal or types into it | separate account and session; same-user mode refused |
Known limits
- Masking is best effort: a personal value in a free-text field that no rule, heuristic or detector catches comes back in clear. Use
redact-unknownand allow rules on production. - RabbitMQ allows a purge to any user with
readon the queue: the allowlist is the only barrier. - The approval screen can only show what the broker reports before the read: unacked messages are invisible to a peek, and a peek cannot know the next message’s delivery count before taking it.
- Through an ssh tunnel streams are read over AMQP; offsets and lag need the stream protocol and are unavailable.
- The audit log is append-only by convention (
chattr +aon Linux makes it so for the console account too); there is no hash chain.
Reporting a vulnerability
Privately, through GitHub security advisories, not in a public issue. See SECURITY.md.