Audit logs
Updated 6 October 2026 · applies to 1.0.1 · Community and Commercial
Every change BROKA makes is recorded, and so is every refusal. The trail is append-only, tamper-evident, and it lives in your database — it goes nowhere else unless you forward it. Each record is written as an OpenAuditModel 1.0 event, and the screen reads it off that event, so the row you look at and the file an auditor checks are one document.
What an entry carries
Each record holds when it happened and, where it concerns a connection or an environment, in which environment; who did it — the account, their IP address, their user agent, their session, and whether they held administrative access at that moment; how it arrived — in Commercial, an action an AI assistant took through the MCP server is marked MCP with the tool, the client's name and version and the API token's prefix, and the list filters on it; what they did — the action, the target, and the connection where one applies; why, when a reason was needed; and how it ended — Success, Denied (a permission was missing), Blocked (a policy stopped a request the person was entitled to make) or Failed (the broker or the system refused it).
Two details are deliberate and worth knowing:
- The actor is recorded by id, and the name is resolved beside the trail. The id is what the tamper-evidence chain binds, so an entry can never be quietly re-attributed to somebody else — and because no hash has ever covered the name, a name can be erased on request without breaking a single link. The trade is worth stating: the screen shows the name as it is now rather than as it read on the day.
- A refusal is recorded exactly like a success. A write blocked by an environment policy, a permission denied, a failed sign-in — all of them are entries, recorded against the object they were aimed at, so a queue's, a topic's or a Redis key's history holds the attempts beside what succeeded. A trail that only holds successes cannot answer the question an access review is actually asking. A refusal also says what the request asked for — the principal a denied grant was for, the role a refused assignment named, where a refused offset reset was aimed — and that nothing changed. What it cannot know, such as what a broker that failed a write had already done, it names in the event as unknown, and nothing is filled in.
An entry also carries the request's trace id, what a change changed — both sides where that is safe, a property that holds a credential by name only — and the resource's classification where it has one. It never carries a credential, a secret or a message payload.
Not every entry has a person behind it. When the broker service decrypts a stored credential for its own work — a cluster's list snapshot, an alert check, an alert being sent — no request asked for it, so the service records it as an entry of its own, with itself as the actor: which connection's or channel's credential, of which kind, and which of those jobs needed it, never the value. A job's repeats of the same credential within the hour are not recorded again.
Clicking an entry opens it: the result, the target, the actor and the connection, the address, session and user agent it came from, with Everything from this address and Everything in this session to narrow the list to either, and the whole event exactly as the chain holds it.
Writes that cannot go missing
An audited action and its audit record commit together or not at all. If the record cannot be written, the action is rolled back and the caller is told — there is no fail-open path and no list of actions exempt from the rule, because such a list would be a list of the actions worth attacking.
A failed audit write raises a critical alarm of its own, so "the trail is down" is a signal rather than a silence.
A change on a broker cannot be rolled back, so its record is written when the broker answers, and the caller cannot cancel it: a client hanging up must not be able to destroy the only trace of an irreversible action. A write the broker never answered is recorded as a failure marked outcome unknown, because the broker may have carried it out.
Tamper evidence
Records are chained: each link is a SHA-256 hash of the event, in its RFC 8785 canonical form, and the previous link's hash, kept in a separate table so the audit rows themselves stay strictly append-only. The chain is built by a periodic sweep, in order, under a lock, one chain per installation.
Verify integrity walks the chain over the dates you have chosen and reports the first break it finds, telling you which kind it is — a link that no longer follows its neighbour, a record that has gone missing without a retention record to account for it, content that no longer hashes to what was recorded, or a link sealed under an algorithm it cannot recompute. Alongside the verdict it reports how many links it checked, how many rows are not yet chained, how many links were pruned by retention, and how many timestamps run backwards.
What verification proves, and what it does not:
- It proves that within the range it walked, no covered record was altered, removed or reordered
by anyone who cannot write the database. It does not hold against someone who can: the seal is
an unkeyed SHA-256 chain in that same database, so they can change records and reseal every link
after them. What holds against them is a copy they cannot reach — the evidence pack's
checkpoint.jsonkept outside the installation, or the trail forwarded as it is written — to a file or an HTTP endpoint through the Orchestrator'sAuditForwarding__Modeand, for a file,__Directoryor, for HTTP,__Endpointand__Tokensettings, which the Compose recipe does not set, or, in Commercial, to Kafka or RabbitMQ (see Retention, below). - It cannot prove that nothing was removed from the newest end of the trail: a chain that has been truncated is internally consistent, because nothing inside it records how long it should be.
- It reports the first break, not a count. Past a break every later link mismatches by construction, so a total would be a measure of chain length rather than of tampering.
- The newest records are not yet chained — the sweep runs behind live traffic, and the console says so rather than implying coverage it does not have.
- Timestamps running backwards are reported separately from the verdict, because the usual cause is a clock correction, not tampering.
Reads are audited too
Commercial only. A Community installation records every change and every refusal, and no reads.
Not every read by default — that would be noise measured in orders of magnitude — but the privileged ones: anything that returns message or key contents, returns or decrypts a secret, shows who may do what (users, roles, permissions, ACLs, sessions), or produces an export. The trail itself is one of them: each page of it read on this screen is recorded, with the filters that narrowed it — never what they were set to — so the trail also says who has been reading it. Settings ▸ Security can turn read recording off or extend it to every read. Two kinds of record ignore that setting: a read of contents a data masking rule covers, shown unmasked to someone allowed to see it, is always recorded, naming the rules it skipped and their tags; and a change to a masking rule is recorded with its tags. A read an assistant makes through the MCP server follows the setting like any other. Recorded reads are badged read, which is how the screen's activity filter and the evidence pack's changes-versus-reads split work.
Retention
Retention always runs, and it keeps at most six months — 180 days, which is also the default.
The window can be shortened in Settings ▸ Security; it cannot be switched off. Shortening it is
itself an administrative change: it requires the security permission and a reason, because it
deletes records sooner, and it is recorded. Every pruned range still leaves a chained
audit.trail.prune destruction record.
Anything older than six months exists only in a copy the trail was forwarded to. There are two ways to forward it:
- To a file or an HTTP endpoint, in both editions. This is set in the deployment environment
(
AuditForwarding__*), not in the console. For a file, setAuditForwarding__Mode=fileand name the directory withAuditForwarding__Directory; one newline-delimited JSON file is written per day. That directory must be a volume mounted into the orchestrator container, writable by it — otherwise the copy lives and dies with the container. - To Kafka or RabbitMQ, in the Commercial edition only. The destination is a Kafka or RabbitMQ connection already registered in BROKA, plus a topic on Kafka, or an exchange and a routing key on RabbitMQ. Someone holding the security permission chooses it in Settings ▸ Security; saving and stopping it need a reason and are recorded. A file or HTTP destination set in the environment takes precedence, and while one is set this cannot be chosen. While the Commercial licence withholds commercial writes, this forwarding stops, and it resumes where it stopped.
What leaves is the sealed event, unchanged and in chain order, so the receiver can check the chain itself. Only one destination is used at a time.
While forwarding is off, has stopped, or has lost records, the audit page recommends it to whoever holds the security permission — Kafka or RabbitMQ in Commercial; in Community, a file or an HTTP endpoint, with a note that Kafka and RabbitMQ forwarding is Commercial.
A record that was never forwarded is still deleted at 180 days. BROKA warns seven days before that happens.
Evidence pack
An evidence pack — Export ▸ Evidence pack (zip) — is a single archive for an auditor or a reviewer, for the dates chosen: the entries as CSV views (the whole trail, sign-ins, privileged actions, changes and, in Commercial, reads), the sealed events themselves — the unbroken run of the chain for those dates, in files small enough for the specification's own tool to read whole — with a checkpoint of the chain's head, the access report, the integrity verdict, the retention state, and a manifest describing what is inside — including the limits of what it can claim. Handing over an artefact that overstates itself is worse than handing over nothing.
Export also downloads the filtered range as CSV, or the dates alone as NDJSON. The NDJSON is the sealed chain itself, which the specification's verifier checks, so no other filter narrows it: a chain with entries left out reads to a verifier as a chain that has been tampered with.


