Skip to content

fix(#728): scope the camel-core jmx.acl to Karaf's role conventions - #737

Open
oscerd wants to merge 3 commits into
apache:mainfrom
oscerd:fix/728-jmx-acl-scope
Open

fix(#728): scope the camel-core jmx.acl to Karaf's role conventions#737
oscerd wants to merge 3 commits into
apache:mainfrom
oscerd:fix/728-jmx-acl-scope

Conversation

@oscerd

@oscerd oscerd commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #728

What

The camel-core feature seeded jmx.acl.org.apache.camel with a single
* = * entry. Karaf resolves ACL PIDs most-specific-first, so this domain-level
PID is consulted before the root jmx.acl PID and replaces Karaf's stock
per-operation mapping for every MBean in the org.apache.camel domain — so
read-only introspection and mutating operations are treated identically.

How

Karaf falls through to the next-less-specific PID when no rule in the current
PID matches
the operation. That means the domain file only needs to list the
operations that should differ from the stock jmx.acl defaults, exactly as
Karaf's own jmx.acl.org.apache.karaf.bundle.cfg does (it lists lifecycle only
and lets get*/list* fall through).

So the entry becomes:

start*   = manager
stop*    = manager
suspend* = manager
resume*  = manager
reset*   = manager

which leaves:

  • get* / list* / is* at the stock viewer mapping
  • lifecycle at manager, matching how Karaf maps bundle lifecycle
  • everything else — sendBody*, requestBody*, createEndpoint,
    removeEndpoints, addOrUpdateRoutes*, and dump* (which resolves property
    placeholders) — at the stock admin mapping

The comment that was removed was wrong

The entry carried <!-- allow camel to access its own mbeans for karaf commands and other needs -->. That justification does not hold:

  • KarafMBeanServerGuard is installed as a Proxy around the MBeanServer
    passed to JMXConnectorServerFactory.newJMXConnectorServer(...)
    (ConnectorServerFactory:293-295), so it applies to remote JMX connections
    only
    .
  • The camel:* commands read CamelContext straight from the OSGi service
    registry (CamelCommandSupport.getCamelContexts()).
  • The one command that does touch the MBeanServer, ContextInflight, gets it
    from agent.getMBeanServer() in-VM — which bypasses the guard regardless of
    what any ACL says.

So this config never had any effect on the shell commands; it only ever applied
to remote JMX principals.

Verification

Rather than reasoning about the semantics, I ran the new mapping through
Karaf 4.4.8's own ACLConfigurationParser, driving it the way
KarafMBeanServerGuard.getRequiredRoles does (domain PID first, fall through to
the stock root jmx.acl on NO_MATCH):

operation                    | OLD (* = *)            | NEW
-----------------------------+------------------------+-----------------------
getCamelId                   | viewer ALLOWED [*]     | viewer ALLOWED [viewer]
listRoutes                   | viewer ALLOWED [*]     | viewer ALLOWED [viewer]
isStarted                    | viewer ALLOWED [*]     | viewer ALLOWED [viewer]
start                        | viewer ALLOWED [*]     | viewer denied [manager]
stop                         | viewer ALLOWED [*]     | viewer denied [manager]
suspend                      | viewer ALLOWED [*]     | viewer denied [manager]
resume                       | viewer ALLOWED [*]     | viewer denied [manager]
resetStatistics              | viewer ALLOWED [*]     | viewer denied [manager]
sendBody                     | viewer ALLOWED [*]     | viewer denied [admin]
sendStringBody               | viewer ALLOWED [*]     | viewer denied [admin]
requestBody                  | viewer ALLOWED [*]     | viewer denied [admin]
createEndpoint               | viewer ALLOWED [*]     | viewer denied [admin]
removeEndpoints              | viewer ALLOWED [*]     | viewer denied [admin]
addOrUpdateRoutesFromXml     | viewer ALLOWED [*]     | viewer denied [admin]
dumpRoutesAsXml              | viewer ALLOWED [*]     | viewer denied [admin]
setStatisticsLevel           | viewer ALLOWED [*]     | viewer denied [admin]

RESULT: all expectations hold

(The [*] role in the OLD column is why every row reads ALLOWED: Karaf's
JaasHelper.currentUserHasRole treats a required role of * as satisfied
before it looks at any principal.)

camel-features.xml is still well-formed XML.

Upgrade note

Karaf seeds a <config> only when the file does not already exist, so an
existing installation keeps its current
etc/jmx.acl.org.apache.camel.cfg on upgrade. Added an operator bullet to the
security model telling operators to check that file.

Open question for reviewers

The tiering above is a judgement call. The alternative is to drop the
<config> block entirely and let the whole domain fall through to Karaf's
defaults — simpler, but then route lifecycle needs admin over remote JMX,
which is inconsistent with Karaf treating bundle lifecycle as manager. Happy
to switch if you prefer strict fall-through.


Claude Code on behalf of Andrea Cosentino

…ions

The camel-core feature seeded jmx.acl.org.apache.camel with a single
"* = *" entry. Karaf resolves ACL PIDs most specific first, so that
domain level PID is consulted before the root jmx.acl PID and replaces
Karaf's stock per-operation mapping for every Camel MBean: read-only
introspection and mutating operations end up treated identically.

Replace it with the deviations only. Karaf falls through to the next PID
when no rule in the current one matches, so listing just the lifecycle
operations leaves get*/list*/is* at the stock viewer mapping and
everything else - sendBody*, requestBody*, createEndpoint,
removeEndpoints, addOrUpdateRoutes*, and dump*, which resolves property
placeholders - at the stock admin mapping. Lifecycle goes to manager,
matching how Karaf maps bundle lifecycle in its own
jmx.acl.org.apache.karaf.bundle.

The comment being removed said the entry was there to let the karaf
commands reach the Camel MBeans. That does not hold: KarafMBeanServerGuard
is installed as a proxy on the MBeanServer handed to the JMX connector
(ConnectorServerFactory:293-295), so it applies to remote connections
only. The camel:* commands read CamelContext from the OSGi service
registry, and the one command that touches the MBeanServer
(ContextInflight) does so in-VM, bypassing the guard either way.

Verified against Karaf 4.4.8's own ACLConfigurationParser rather than by
inspection: read-only stays viewer, lifecycle resolves to manager, and
the injection/mutation/dump operations resolve to admin.

Karaf seeds a config only when absent, so an existing installation keeps
its current file on upgrade; documented as an operator check in the
security model.
@oscerd
oscerd requested review from essobedo and jbonofre August 24, 2026 20:02

@jbonofre jbonofre left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing a new pass.

Comment thread features/src/main/feature/camel-features.xml
oscerd and others added 2 commits August 26, 2026 18:00
Co-authored-by: JB Onofré <jbonofre@apache.org>
…are safe to expose

Builds on the committed suggestion. browse* = viewer is kept as reviewed; the
dump* wildcard is replaced by an explicit list, because as a wildcard it grants
more than intended and fails open.

Reading the MBeans is what settled it:

- dumpRoutesAsXml has four overloads and dumpRoutesAsYaml five, plus the route
  level twins. The first boolean is resolvePlaceholders in every family, and
  every no-arg form passes false - verified from the bytecode, dumpRoutesAsXml()
  calls dumpRoutesAsXml(false, true). So the no-arg dumps are safe to expose and
  the boolean overloads are the ones that can print resolved credentials.
- dump* also matches ManagedBacklogTracerMBean.dumpAllTracedMessagesAsXml and
  dumpTracedMessagesAsXml, which return traced message bodies, and
  ManagedDumpRoutesStrategyMBean.dumpRoutes(String), which writes files.

Pinning arguments instead (dumpRoutesAsXml(boolean)[true] = admin) would cover
today's overloads but fails open: an overload added by a future Camel matches no
argument rule, falls back to the dump* wildcard and is granted to viewer. Since
repackaging each new Camel release is what this repository does, that is routine
rather than hypothetical. Listing the safe operations fails the other way - an
unrecognised operation stays at Karaf's admin default until someone reviews it.

Verified by running the mapping through Karaf 4.4.8's ACLConfigurationParser,
driven the way KarafMBeanServerGuard.getRequiredRoles drives it: domain PID
first, fall through to the stock root jmx.acl on NO_MATCH. 24 expectations, all
holding, including a hypothetical dumpRoutesAsXml(b,b,b,b) landing on admin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@oscerd

oscerd commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@jbonofre before this lands — one line in it contradicts the rationale the same PR documents, and I would rather raise it now than after the merge.

The security-model.adoc bullet added here states the principle:

the overloads that take a resolvePlaceholders flag, and the backlog tracer/debugger operations that return traced message bodies, are left at Karaf's admin default, so an operation added by a future Camel release is denied rather than granted until it is reviewed

The dump* side implements that faithfully. There is no dump* wildcard, so dumpTracedMessagesAsXml, dumpTracedMessagesAsJSon and dumpTracedMessages on ManagedBacklogTracerMBean / ManagedBacklogDebuggerMBean all fall through to admin, and so does every (boolean…) dump overload. That is the right shape, and it fails closed on overloads Camel adds later.

Four lines above it, though:

browse* = viewer

browse* reaches the same class of data the bullet says is withheld, just off browsable endpoints rather than the tracer. From ManagedBrowsableEndpointMBean:

operation what a viewer gets
browseMessageBody(Integer index) the message body, no flag to suppress it
browseExchange(Integer index) the exchange
browseAllMessagesAsXml(Boolean includeBody) caller passes includeBody=true
browseMessageAsXml(Integer, Boolean includeBody) same
browseRangeMessagesAsXml(Integer, Integer, Boolean) same

So on any browsable endpoint in the container — seda, mock, JMS browse, file — a principal holding only viewer can read live message payloads. That is business data in flight, and it is a wider disclosure than the resolved property placeholders the rest of this commit closes. It is also a wildcard, so it fails open: a browse* operation added by a future Camel release is granted to viewer without review, which is precisely the property the bullet says the mapping avoids.

Worth noting it was not in the original issue either — #728 was about the * = * blanket, and the read-only set under discussion was list* / get* / is* / non-placeholder dump*.

Two consistent ways out, both fine by me:

  1. Drop browse* = viewer. It falls through to admin, matching the tracer reasoning exactly, and the documented principle holds as written. My preference, on the grounds that the wildcard fails open.
  2. Keep it and amend the bullet to say that message bodies are exposed to viewer through browse*, so operators reading the security model are not misled about what the shipped mapping grants.

Everything else in the PR I checked against the real MBean interfaces: all 15 dump* signatures match actual operations, no typos, no dead rules, and dumpRoutesAsXml() delegates to dumpRoutesAsXml(false, true) so the no-arg forms genuinely do not resolve placeholders.


Claude Code on behalf of Andrea Cosentino

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

camel-core feature: scope the shipped jmx.acl configuration to Karaf's role conventions

2 participants