You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by jb...@apache.org on 2026/10/16 11:47:20 UTC

(activemq) branch main updated: Tighten default Jolokia access policy and bind the web console to loopback (#2639)

This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq.git


The following commit(s) were added to refs/heads/main by this push:
     new 58c1f024e2 Tighten default Jolokia access policy and bind the web console to loopback (#2639)
58c1f024e2 is described below

commit 58c1f024e2ab1f6b884f07981f42b33f468e46ac
Author: JB Onofré <jb...@apache.org>
AuthorDate: Fri Oct 16 12:47:14 2026 +0100

    Tighten default Jolokia access policy and bind the web console to loopback (#2639)
    
    - jolokia-access.xml: list the allowed read-only operations explicitly
      instead of a wildcard, list the allowed origins for strict checking, and
      let denied operation names match a name sent with its signature
    - jetty-spring.properties: bind the http/https connectors to 127.0.0.1
    - JettyServerBean: -Djetty.host overrides the connector bind address
    - add tests evaluating the shipped Jolokia policy and Jetty configuration
---
 .../activemq/spring/jetty/JettyServerBeanTest.java |  20 ++
 .../test/resources/conf/jetty-spring.properties    |   2 +
 .../activemq/spring/jetty/JettyServerBean.java     |  11 +
 assembly/src/release/conf/jetty-spring.properties  |   6 +
 assembly/src/release/conf/jolokia-access.xml       | 255 +++++++++++++++------
 .../activemq/config/JettyBindAddressTest.java      |  65 ++++++
 .../activemq/config/JolokiaAccessPolicyTest.java   | 211 +++++++++++++++++
 7 files changed, 502 insertions(+), 68 deletions(-)

diff --git a/activemq-web-console/src/test/java/org/apache/activemq/spring/jetty/JettyServerBeanTest.java b/activemq-web-console/src/test/java/org/apache/activemq/spring/jetty/JettyServerBeanTest.java
index 6efe6709ae..ad5855bff1 100644
--- a/activemq-web-console/src/test/java/org/apache/activemq/spring/jetty/JettyServerBeanTest.java
+++ b/activemq-web-console/src/test/java/org/apache/activemq/spring/jetty/JettyServerBeanTest.java
@@ -55,6 +55,26 @@ public class JettyServerBeanTest {
                 HttpResponse.BodyHandlers.ofString());
         assertEquals("Web console must require authentication", 401, response.statusCode());
 
+        // jetty.http.host from jetty-spring.properties: without it Jetty binds every interface
+        assertEquals("127.0.0.1", ((ServerConnector) server.getConnectors()[0]).getHost());
+
         context.stop();
     }
+
+    @Test
+    public void testJettyHostSystemPropertyOverridesBindAddress() throws Exception {
+        System.setProperty(JettyServerBean.SYSTEM_PROPERTY_HOST, "localhost");
+        try {
+            var context = new ClassPathXmlApplicationContext("conf/jetty-spring.xml");
+            context.start();
+            try {
+                var jettyServer = (JettyServerBean) context.getBean("jettyServer");
+                assertEquals("localhost", ((ServerConnector) jettyServer.getServer().getConnectors()[0]).getHost());
+            } finally {
+                context.stop();
+            }
+        } finally {
+            System.clearProperty(JettyServerBean.SYSTEM_PROPERTY_HOST);
+        }
+    }
 }
diff --git a/activemq-web-console/src/test/resources/conf/jetty-spring.properties b/activemq-web-console/src/test/resources/conf/jetty-spring.properties
index c1b0b15b0c..4c0d2f9ebc 100644
--- a/activemq-web-console/src/test/resources/conf/jetty-spring.properties
+++ b/activemq-web-console/src/test/resources/conf/jetty-spring.properties
@@ -22,6 +22,8 @@ jettyXmlFiles=jetty-bytebufferpool.xml,jetty-threadpool.xml,jetty-scheduler.xml,
 jettyHttpXmlFiles=jetty-http.xml
 httpEnabled=true
 httpsEnabled=false
+# Loopback only, as in the shipped jetty-spring.properties.
+jetty.http.host=127.0.0.1
 # Ephemeral port for tests to avoid collisions.
 jetty.http.port=0
 jetty.dos.leakingBucketTracker.maxRequestsPerSecond=100
diff --git a/activemq-web/src/main/java/org/apache/activemq/spring/jetty/JettyServerBean.java b/activemq-web/src/main/java/org/apache/activemq/spring/jetty/JettyServerBean.java
index 586aee538a..c0d689894a 100644
--- a/activemq-web/src/main/java/org/apache/activemq/spring/jetty/JettyServerBean.java
+++ b/activemq-web/src/main/java/org/apache/activemq/spring/jetty/JettyServerBean.java
@@ -50,6 +50,9 @@ public class JettyServerBean implements InitializingBean, DisposableBean {
     public static final String PROPERTY_HTTP_ENABLED = "httpEnabled";
     public static final String PROPERTY_HTTPS_ENABLED = "httpsEnabled";
     public static final String PROPERTY_EXTRA_XML_FILES = "jettyExtraXmlFiles";
+    public static final String PROPERTY_HTTP_HOST = "jetty.http.host";
+    public static final String PROPERTY_HTTPS_HOST = "jetty.ssl.host";
+    public static final String SYSTEM_PROPERTY_HOST = "jetty.host";
 
     public static final int PROPERTY_XML_FILES_LIMIT = 128;
     public static final String PROPERTY_XML_FILES_SEPARATOR = ",";
@@ -161,6 +164,14 @@ public class JettyServerBean implements InitializingBean, DisposableBean {
             jettyProperties.putIfAbsent("jetty.home", jettyProperties.get("activemq.home"));
             jettyProperties.putIfAbsent("jetty.base", jettyProperties.get("activemq.base"));
 
+            // -Djetty.host overrides the bind address of the http and https connectors set in
+            // jetty-spring.properties (jetty.http.host / jetty.ssl.host), e.g. for the docker image.
+            var jettyHost = System.getProperty(SYSTEM_PROPERTY_HOST);
+            if (jettyHost != null && !jettyHost.isBlank()) {
+                jettyProperties.put(PROPERTY_HTTP_HOST, jettyHost);
+                jettyProperties.put(PROPERTY_HTTPS_HOST, jettyHost);
+            }
+
             // Now lets tie it all together
             idMap = configure(xmls, jettyProperties);
         }
diff --git a/assembly/src/release/conf/jetty-spring.properties b/assembly/src/release/conf/jetty-spring.properties
index 23ab44e491..26962252b5 100644
--- a/assembly/src/release/conf/jetty-spring.properties
+++ b/assembly/src/release/conf/jetty-spring.properties
@@ -32,6 +32,12 @@ jettyHttpXmlFiles=jetty-http.xml
 jettyHttpsXmlFiles=jetty-ssl.xml,jetty-ssl-context.xml,jetty-https.xml,jetty-secure-redirect.xml
 httpEnabled=true
 httpsEnabled=false
+# Bind address of the http / https connectors. Loopback only by default: without a
+# value Jetty binds every interface. To expose the console set an interface address
+# (or 0.0.0.0) here and widen the client allow-list in jetty/jetty-security.xml and
+# the <allow-origin> list in jolokia-access.xml. -Djetty.host=<address> overrides both.
+jetty.http.host=127.0.0.1
+jetty.ssl.host=127.0.0.1
 jetty.http.port=8161
 jetty.ssl.port=8443
 jetty.httpConfig.securePort=8443
diff --git a/assembly/src/release/conf/jolokia-access.xml b/assembly/src/release/conf/jolokia-access.xml
index 6844c31460..03469fe40d 100644
--- a/assembly/src/release/conf/jolokia-access.xml
+++ b/assembly/src/release/conf/jolokia-access.xml
@@ -22,10 +22,22 @@
     <method>post</method>
   </http>
 
-  <!-- Enforce that an Origin/Referer header is present to prevent CSRF.
-       Add explicit <allow-origin> entries below if the console is served from
-       a different host. -->
+  <!-- CSRF protection: with <strict-checking/> every request must carry an
+       Origin (or Referer) header matching one of the <allow-origin> entries.
+       The list must not be empty: without any entry Jolokia accepts every
+       origin and only rejects requests that have no such header at all.
+       The entries below are the console's own loopback addresses on the default
+       http / https ports. Add the exact scheme://host:port the console is
+       reached through when the bind address, port or host name is changed, and
+       avoid a leading wildcard. Non-browser clients must send one of these
+       values as their Origin header. -->
   <cors>
+    <allow-origin>http://localhost:8161</allow-origin>
+    <allow-origin>http://127.0.0.1:8161</allow-origin>
+    <allow-origin>http://[::1]:8161</allow-origin>
+    <allow-origin>https://localhost:8443</allow-origin>
+    <allow-origin>https://127.0.0.1:8443</allow-origin>
+    <allow-origin>https://[::1]:8443</allow-origin>
     <strict-checking/>
   </cors>
 
@@ -37,13 +49,117 @@
     <command>search</command>
   </commands>
 
+  <!--
+       How Jolokia combines <commands>, <allow> and <deny>: the two lists are
+       never applied to the same request.
+         - A command type listed in <commands> (read) is permitted everywhere
+           except where <deny> matches. <allow> is not consulted.
+         - A command type NOT listed in <commands> (write, exec) is refused
+           everywhere except where <allow> matches. <deny> is not consulted.
+       <allow> is therefore the only thing limiting write and exec, so each
+       entry must name the exact operations it opens. A wildcard such as
+       <operation>*</operation> or <attribute>*</attribute> on the broker
+       MBeans re-enables every operation / attribute write, including the ones
+       listed in <deny> below.
+
+       Operation names are matched as plain strings against what the client
+       sent, which may carry a signature, e.g. "browse(java.lang.String)". Both
+       spellings are listed for each operation.
+
+       To open an additional management operation, add it to <allow>. Do not add
+       exec or write to <commands>: that permits everything <deny> does not name,
+       on every MBean in the JVM.
+  -->
   <allow>
-    <!-- Allow all operations for the broker itself -->
+    <!-- Broker: read-only queries -->
     <mbean>
-      <name>org.apache.activemq:*</name>
-      <attribute>*</attribute>
-      <operation>*</operation>
+      <name>org.apache.activemq:type=Broker,brokerName=*</name>
+      <operation>queryQueues</operation>
+      <operation>queryQueues(java.lang.String,int,int)</operation>
+      <operation>queryTopics</operation>
+      <operation>queryTopics(java.lang.String,int,int)</operation>
+      <operation>browseQueue</operation>
+      <operation>browseQueue(java.lang.String)</operation>
+      <operation>getTransportConnectorByType</operation>
+      <operation>getTransportConnectorByType(java.lang.String)</operation>
+    </mbean>
+
+    <!-- Destinations: browsing and cursor state -->
+    <mbean>
+      <name>org.apache.activemq:type=Broker,brokerName=*,destinationType=*,destinationName=*</name>
+      <operation>browse</operation>
+      <operation>browse()</operation>
+      <operation>browse(java.lang.String)</operation>
+      <operation>browseAsTable</operation>
+      <operation>browseAsTable()</operation>
+      <operation>browseAsTable(java.lang.String)</operation>
+      <operation>getMessage</operation>
+      <operation>getMessage(java.lang.String)</operation>
+      <operation>cursorSize</operation>
+      <operation>cursorSize()</operation>
+      <operation>doesCursorHaveMessagesBuffered</operation>
+      <operation>doesCursorHaveMessagesBuffered()</operation>
+      <operation>doesCursorHaveSpace</operation>
+      <operation>doesCursorHaveSpace()</operation>
+    </mbean>
+
+    <!-- Subscriptions: browsing, cursor state and selector matching -->
+    <mbean>
+      <name>org.apache.activemq:type=Broker,brokerName=*,destinationType=*,destinationName=*,endpoint=Consumer,clientId=*,consumerId=*</name>
+      <operation>browse</operation>
+      <operation>browse()</operation>
+      <operation>browseAsTable</operation>
+      <operation>browseAsTable()</operation>
+      <operation>cursorSize</operation>
+      <operation>cursorSize()</operation>
+      <operation>doesCursorHaveMessagesBuffered</operation>
+      <operation>doesCursorHaveMessagesBuffered()</operation>
+      <operation>doesCursorHaveSpace</operation>
+      <operation>doesCursorHaveSpace()</operation>
+      <operation>isMatchingQueue</operation>
+      <operation>isMatchingQueue(java.lang.String)</operation>
+      <operation>isMatchingTopic</operation>
+      <operation>isMatchingTopic(java.lang.String)</operation>
+    </mbean>
+
+    <!-- Health checks -->
+    <mbean>
+      <name>org.apache.activemq:type=Broker,brokerName=*,service=Health</name>
+      <operation>health</operation>
+      <operation>health()</operation>
+      <operation>healthList</operation>
+      <operation>healthList()</operation>
+      <operation>healthStatus</operation>
+      <operation>healthStatus()</operation>
+    </mbean>
+
+    <!-- JobScheduler: listing scheduled jobs -->
+    <mbean>
+      <name>org.apache.activemq:type=Broker,brokerName=*,service=JobScheduler,name=*</name>
+      <operation>getAllJobs</operation>
+      <operation>getAllJobs(boolean)</operation>
+      <operation>getAllJobs(java.lang.String,java.lang.String)</operation>
+      <operation>getAllJobs(java.lang.String,java.lang.String,boolean)</operation>
+      <operation>getNextScheduleJobs</operation>
+      <operation>getNextScheduleJobs(boolean)</operation>
+      <operation>getExecutionCount</operation>
+      <operation>getExecutionCount(java.lang.String)</operation>
+    </mbean>
+
+    <!-- Log4JConfiguration: reading a logger level -->
+    <mbean>
+      <name>org.apache.activemq:type=Broker,brokerName=*,service=Log4JConfiguration</name>
+      <operation>getLogLevel</operation>
+      <operation>getLogLevel(java.lang.String)</operation>
     </mbean>
+
+    <!-- Transport connectors: connection count -->
+    <mbean>
+      <name>org.apache.activemq:type=Broker,brokerName=*,connector=clientConnectors,connectorName=*</name>
+      <operation>connectionCount</operation>
+      <operation>connectionCount()</operation>
+    </mbean>
+
     <!-- Allow all operations for Jolokia Config -->
     <mbean>
       <name>jolokia:type=Config</name>
@@ -51,7 +167,10 @@
     </mbean>
   </allow>
 
-  <!-- deny all operations or getting attributes from these mbeans -->
+  <!-- deny all operations or getting attributes from these mbeans.
+       Attribute entries apply to reads. Operation entries only take effect if
+       exec is added to <commands>; they end with a wildcard so that they also
+       match a name sent with its signature. -->
   <deny>
     <!-- ActiveMQ broker: deny destructive / privileged operations.
          Safe read-only getters, browse*, and statistics queries remain allowed
@@ -59,74 +178,74 @@
     <mbean>
       <name>org.apache.activemq:type=Broker,brokerName=*</name>
       <!-- JVM and broker lifecycle -->
-      <operation>terminateJVM</operation>
-      <operation>stop</operation>
-      <operation>stopGracefully</operation>
-      <operation>restart</operation>
-      <operation>gc</operation>
+      <operation>terminateJVM*</operation>
+      <operation>stop*</operation>
+      <operation>stopGracefully*</operation>
+      <operation>restart*</operation>
+      <operation>gc*</operation>
       <!-- Topology and destination management -->
-      <operation>addConnector</operation>
-      <operation>removeConnector</operation>
-      <operation>addNetworkConnector</operation>
-      <operation>removeNetworkConnector</operation>
-      <operation>addQueue</operation>
-      <operation>removeQueue</operation>
-      <operation>addTopic</operation>
-      <operation>removeTopic</operation>
-      <operation>createDurableSubscriber</operation>
-      <operation>destroyDurableSubscriber</operation>
+      <operation>addConnector*</operation>
+      <operation>removeConnector*</operation>
+      <operation>addNetworkConnector*</operation>
+      <operation>removeNetworkConnector*</operation>
+      <operation>addQueue*</operation>
+      <operation>removeQueue*</operation>
+      <operation>addTopic*</operation>
+      <operation>removeTopic*</operation>
+      <operation>createDurableSubscriber*</operation>
+      <operation>destroyDurableSubscriber*</operation>
       <!-- Runtime configuration mutation -->
-      <operation>reloadLog4jProperties</operation>
-      <operation>setMemoryLimit</operation>
-      <operation>setStoreLimit</operation>
-      <operation>setTempLimit</operation>
-      <operation>setJobSchedulerStoreLimit</operation>
-      <operation>setMaxUncommittedCount</operation>
+      <operation>reloadLog4jProperties*</operation>
+      <operation>setMemoryLimit*</operation>
+      <operation>setStoreLimit*</operation>
+      <operation>setTempLimit*</operation>
+      <operation>setJobSchedulerStoreLimit*</operation>
+      <operation>setMaxUncommittedCount*</operation>
     </mbean>
 
     <!-- Destinations: deny message data manipulation and injection.
          browse / browseAsTable / counters remain allowed. -->
     <mbean>
       <name>org.apache.activemq:type=Broker,brokerName=*,destinationType=*,destinationName=*</name>
-      <operation>purge</operation>
-      <operation>removeMessage</operation>
-      <operation>removeMatchingMessages</operation>
-      <operation>copyMessageTo</operation>
-      <operation>copyMatchingMessagesTo</operation>
-      <operation>moveMessageTo</operation>
-      <operation>moveMatchingMessagesTo</operation>
-      <operation>retryMessage</operation>
-      <operation>retryMessages</operation>
-      <operation>removeMessageGroup</operation>
-      <operation>removeAllMessageGroups</operation>
-      <operation>sendTextMessage</operation>
-      <operation>sendTextMessageWithProperties</operation>
-      <operation>pause</operation>
-      <operation>resume</operation>
+      <operation>purge*</operation>
+      <operation>removeMessage*</operation>
+      <operation>removeMatchingMessages*</operation>
+      <operation>copyMessageTo*</operation>
+      <operation>copyMatchingMessagesTo*</operation>
+      <operation>moveMessageTo*</operation>
+      <operation>moveMatchingMessagesTo*</operation>
+      <operation>retryMessage*</operation>
+      <operation>retryMessages*</operation>
+      <operation>removeMessageGroup*</operation>
+      <operation>removeAllMessageGroups*</operation>
+      <operation>sendTextMessage*</operation>
+      <operation>sendTextMessageWithProperties*</operation>
+      <operation>pause*</operation>
+      <operation>resume*</operation>
     </mbean>
 
     <!-- Durable subscriptions: deny destroy and selector tampering -->
     <mbean>
       <name>org.apache.activemq:type=Broker,brokerName=*,destinationType=*,destinationName=*,endpoint=Consumer,clientId=*,consumerId=*</name>
-      <operation>destroy</operation>
-      <operation>removeMessage</operation>
-      <operation>setSelector</operation>
+      <operation>destroy*</operation>
+      <operation>removeMessage*</operation>
+      <operation>setSelector*</operation>
     </mbean>
 
     <!-- JobScheduler: deny job removal -->
     <mbean>
       <name>org.apache.activemq:type=Broker,brokerName=*,service=JobScheduler,name=*</name>
-      <operation>removeJob</operation>
-      <operation>removeAllJobs</operation>
-      <operation>removeAllJobsAtScheduledTime</operation>
+      <operation>removeJob*</operation>
+      <operation>removeAllJobs*</operation>
+      <operation>removeAllJobsAtScheduledTime*</operation>
     </mbean>
 
     <!-- Log4JConfiguration MBean: deny runtime log level / config changes -->
     <mbean>
       <name>org.apache.activemq:type=Broker,brokerName=*,service=Log4JConfiguration</name>
-      <operation>setRootLogLevel</operation>
-      <operation>setLogLevel</operation>
-      <operation>reloadLog4jProperties</operation>
+      <operation>setRootLogLevel*</operation>
+      <operation>setLogLevel*</operation>
+      <operation>reloadLog4jProperties*</operation>
     </mbean>
 
     <!-- NetworkConnector: deny credential exposure and config mutation -->
@@ -134,21 +253,21 @@
       <name>org.apache.activemq:type=Broker,brokerName=*,connector=networkConnectors,networkConnectorName=*</name>
       <attribute>Password</attribute>
       <attribute>RemotePassword</attribute>
-      <operation>setUserName</operation>
-      <operation>setPassword</operation>
-      <operation>setRemoteUserName</operation>
-      <operation>setRemotePassword</operation>
-      <operation>setBridgeTempDestinations</operation>
-      <operation>setConduitSubscriptions</operation>
-      <operation>setDispatchAsync</operation>
-      <operation>setDynamicOnly</operation>
-      <operation>setMessageTTL</operation>
-      <operation>setConsumerTTL</operation>
-      <operation>setPrefetchSize</operation>
-      <operation>setAdvisoryPrefetchSize</operation>
-      <operation>setDecreaseNetworkConsumerPriority</operation>
-      <operation>setSuppressDuplicateQueueSubscriptions</operation>
-      <operation>setSuppressDuplicateTopicSubscriptions</operation>
+      <operation>setUserName*</operation>
+      <operation>setPassword*</operation>
+      <operation>setRemoteUserName*</operation>
+      <operation>setRemotePassword*</operation>
+      <operation>setBridgeTempDestinations*</operation>
+      <operation>setConduitSubscriptions*</operation>
+      <operation>setDispatchAsync*</operation>
+      <operation>setDynamicOnly*</operation>
+      <operation>setMessageTTL*</operation>
+      <operation>setConsumerTTL*</operation>
+      <operation>setPrefetchSize*</operation>
+      <operation>setAdvisoryPrefetchSize*</operation>
+      <operation>setDecreaseNetworkConsumerPriority*</operation>
+      <operation>setSuppressDuplicateQueueSubscriptions*</operation>
+      <operation>setSuppressDuplicateTopicSubscriptions*</operation>
     </mbean>
 
     <!-- MLet allows remote class loading: well-known JMX/Jolokia RCE vector -->
diff --git a/assembly/src/test/java/org/apache/activemq/config/JettyBindAddressTest.java b/assembly/src/test/java/org/apache/activemq/config/JettyBindAddressTest.java
new file mode 100644
index 0000000000..97619cc132
--- /dev/null
+++ b/assembly/src/test/java/org/apache/activemq/config/JettyBindAddressTest.java
@@ -0,0 +1,65 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.config;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.activemq.spring.jetty.JettyServerBean;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.junit.Test;
+
+/**
+ * Applies the shipped Jetty XML files with the shipped jetty-spring.properties, without
+ * starting the server. A connector without a host binds every interface, so the web
+ * console relies on these properties to stay on loopback.
+ */
+public class JettyBindAddressTest {
+
+    private static final Path CONF = Path.of("src/release/conf");
+
+    @Test
+    public void consoleConnectorsBindToLoopbackByDefault() throws Exception {
+        Map<String, String> properties = new HashMap<>();
+        Properties shipped = new Properties();
+        try (InputStream in = new FileInputStream(CONF.resolve("jetty-spring.properties").toFile())) {
+            shipped.load(in);
+        }
+        shipped.forEach((key, value) -> properties.put(String.valueOf(key), String.valueOf(value)));
+
+        List<Resource> xmls = new ArrayList<>();
+        for (String xml : new String[] {"jetty-bytebufferpool.xml", "jetty-threadpool.xml", "jetty-scheduler.xml",
+                "jetty-http-config.xml", "jetty.xml", "jetty-http.xml", "jetty-ssl.xml"}) {
+            xmls.add(ResourceFactory.root().newResource(CONF.resolve("jetty").resolve(xml)));
+        }
+
+        Map<String, Object> ids = new JettyServerBean().configure(xmls, properties);
+
+        assertEquals("127.0.0.1", ((ServerConnector) ids.get("httpConnector")).getHost());
+        assertEquals("127.0.0.1", ((ServerConnector) ids.get("sslConnector")).getHost());
+    }
+}
diff --git a/assembly/src/test/java/org/apache/activemq/config/JolokiaAccessPolicyTest.java b/assembly/src/test/java/org/apache/activemq/config/JolokiaAccessPolicyTest.java
new file mode 100644
index 0000000000..2876e9fe4e
--- /dev/null
+++ b/assembly/src/test/java/org/apache/activemq/config/JolokiaAccessPolicyTest.java
@@ -0,0 +1,211 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.management.ObjectName;
+
+import org.apache.activemq.broker.jmx.AbortSlowConsumerStrategyViewMBean;
+import org.apache.activemq.broker.jmx.BrokerViewMBean;
+import org.apache.activemq.broker.jmx.ConnectionViewMBean;
+import org.apache.activemq.broker.jmx.ConnectorViewMBean;
+import org.apache.activemq.broker.jmx.DurableSubscriptionViewMBean;
+import org.apache.activemq.broker.jmx.HealthViewMBean;
+import org.apache.activemq.broker.jmx.JobSchedulerViewMBean;
+import org.apache.activemq.broker.jmx.Log4JConfigViewMBean;
+import org.apache.activemq.broker.jmx.NetworkBridgeViewMBean;
+import org.apache.activemq.broker.jmx.NetworkConnectorViewMBean;
+import org.apache.activemq.broker.jmx.PersistenceAdapterViewMBean;
+import org.apache.activemq.broker.jmx.ProducerViewMBean;
+import org.apache.activemq.broker.jmx.QueueViewMBean;
+import org.apache.activemq.broker.jmx.RecoveredXATransactionViewMBean;
+import org.apache.activemq.broker.jmx.TopicSubscriptionViewMBean;
+import org.apache.activemq.broker.jmx.TopicViewMBean;
+import org.apache.activemq.broker.jmx.VirtualDestinationSelectorCacheViewMBean;
+import org.apache.activemq.plugin.jmx.RuntimeConfigurationViewMBean;
+import org.apache.activemq.security.SecurityAdminMBean;
+import org.apache.activemq.transport.TransportLoggerControlMBean;
+import org.apache.activemq.transport.TransportLoggerViewMBean;
+import org.jolokia.server.core.restrictor.policy.PolicyRestrictor;
+import org.jolokia.server.core.util.HttpMethod;
+import org.jolokia.server.core.util.RequestType;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Evaluates the shipped jolokia-access.xml with Jolokia's own policy restrictor.
+ * Jolokia consults &lt;allow&gt; only for command types missing from &lt;commands&gt; and
+ * &lt;deny&gt; only for the ones present, so reading the file is not enough to tell
+ * what it permits.
+ */
+public class JolokiaAccessPolicyTest {
+
+    private static final String BROKER = "org.apache.activemq:type=Broker,brokerName=localhost";
+    private static final String QUEUE = BROKER + ",destinationType=Queue,destinationName=TEST";
+    private static final String TOPIC = BROKER + ",destinationType=Topic,destinationName=TEST";
+    private static final String SUBSCRIPTION = QUEUE + ",endpoint=Consumer,clientId=client,consumerId=consumer";
+    private static final String CONNECTOR = BROKER + ",connector=clientConnectors,connectorName=openwire";
+    private static final String NETWORK_CONNECTOR = BROKER + ",connector=networkConnectors,networkConnectorName=nc";
+
+    // The only operations reachable through Jolokia by default: none of them changes broker state.
+    private static final Set<String> READ_ONLY_OPERATIONS = Set.of(
+            "queryQueues", "queryTopics", "browseQueue", "getTransportConnectorByType",
+            "browse", "browseAsTable", "getMessage",
+            "cursorSize", "doesCursorHaveMessagesBuffered", "doesCursorHaveSpace",
+            "isMatchingQueue", "isMatchingTopic",
+            "health", "healthList", "healthStatus",
+            "getAllJobs", "getNextScheduleJobs", "getExecutionCount",
+            "getLogLevel", "connectionCount");
+
+    private static final Map<String, Class<?>> MBEANS = new LinkedHashMap<>();
+    static {
+        MBEANS.put(BROKER, BrokerViewMBean.class);
+        MBEANS.put(QUEUE, QueueViewMBean.class);
+        MBEANS.put(TOPIC, TopicViewMBean.class);
+        MBEANS.put(SUBSCRIPTION, DurableSubscriptionViewMBean.class);
+        MBEANS.put(TOPIC + ",endpoint=Consumer,clientId=client,consumerId=consumer", TopicSubscriptionViewMBean.class);
+        MBEANS.put(QUEUE + ",endpoint=Producer,clientId=client,producerId=producer", ProducerViewMBean.class);
+        MBEANS.put(BROKER + ",service=Health", HealthViewMBean.class);
+        MBEANS.put(BROKER + ",service=JobScheduler,name=JMS", JobSchedulerViewMBean.class);
+        MBEANS.put(BROKER + ",service=Log4JConfiguration", Log4JConfigViewMBean.class);
+        MBEANS.put(BROKER + ",service=PersistenceAdapter,instanceName=KahaDB", PersistenceAdapterViewMBean.class);
+        MBEANS.put(BROKER + ",service=SlowConsumerStrategy,instanceName=strategy", AbortSlowConsumerStrategyViewMBean.class);
+        MBEANS.put(BROKER + ",service=RuntimeConfiguration,name=XBean", RuntimeConfigurationViewMBean.class);
+        MBEANS.put(BROKER + ",service=plugin,virtualDestinationSelectoCache=cache", VirtualDestinationSelectorCacheViewMBean.class);
+        MBEANS.put(BROKER + ",transactionType=RecoveredXaTransaction,xid=xid", RecoveredXATransactionViewMBean.class);
+        MBEANS.put(CONNECTOR, ConnectorViewMBean.class);
+        MBEANS.put(CONNECTOR + ",connectionViewType=clientId,connectionName=client", ConnectionViewMBean.class);
+        MBEANS.put(NETWORK_CONNECTOR, NetworkConnectorViewMBean.class);
+        MBEANS.put(NETWORK_CONNECTOR + ",networkBridge=bridge", NetworkBridgeViewMBean.class);
+        MBEANS.put("org.apache.activemq:type=Broker,brokerName=localhost,service=SecurityAdmin", SecurityAdminMBean.class);
+        MBEANS.put("org.apache.activemq:type=Broker,brokerName=localhost,service=TransportLoggerControl", TransportLoggerControlMBean.class);
+        MBEANS.put("org.apache.activemq:type=Broker,brokerName=localhost,service=TransportLogger", TransportLoggerViewMBean.class);
+    }
+
+    private PolicyRestrictor restrictor;
+
+    @Before
+    public void loadPolicy() throws Exception {
+        try (InputStream in = new FileInputStream("src/release/conf/jolokia-access.xml")) {
+            restrictor = new PolicyRestrictor(in);
+        }
+    }
+
+    @Test
+    public void onlyReadCommandsAreEnabled() {
+        assertTrue(restrictor.isTypeAllowed(RequestType.READ));
+        assertTrue(restrictor.isTypeAllowed(RequestType.LIST));
+        assertTrue(restrictor.isTypeAllowed(RequestType.SEARCH));
+        assertTrue(restrictor.isTypeAllowed(RequestType.VERSION));
+        assertFalse(restrictor.isTypeAllowed(RequestType.WRITE));
+        assertFalse(restrictor.isTypeAllowed(RequestType.EXEC));
+        assertTrue(restrictor.isHttpMethodAllowed(HttpMethod.POST));
+        assertFalse(restrictor.isHttpMethodAllowed(HttpMethod.GET));
+    }
+
+    @Test
+    public void onlyReadOnlyOperationsCanBeExecuted() throws Exception {
+        int allowed = 0;
+        for (Map.Entry<String, Class<?>> mbean : MBEANS.entrySet()) {
+            ObjectName name = new ObjectName(mbean.getKey());
+            for (Method method : mbean.getValue().getMethods()) {
+                if (isAttribute(method)) {
+                    continue;
+                }
+                boolean expected = READ_ONLY_OPERATIONS.contains(method.getName());
+                // a client may send the bare name or the name with its signature
+                for (String operation : new String[] {method.getName(), signature(method)}) {
+                    assertEquals(operation + " on " + name, expected, restrictor.isOperationAllowed(name, operation));
+                }
+                if (expected) {
+                    allowed++;
+                }
+            }
+        }
+        assertTrue("no read-only operation was checked", allowed > 0);
+    }
+
+    @Test
+    public void operationsOutsideTheBrokerDomainCannotBeExecuted() throws Exception {
+        assertFalse(restrictor.isOperationAllowed(new ObjectName("java.lang:type=Memory"), "gc"));
+        assertFalse(restrictor.isOperationAllowed(new ObjectName("java.lang:type=Threading"), "dumpAllThreads"));
+        assertFalse(restrictor.isOperationAllowed(new ObjectName("com.sun.management:type=DiagnosticCommand"), "vmSystemProperties"));
+        assertFalse(restrictor.isOperationAllowed(new ObjectName("com.sun.management:type=HotSpotDiagnostic"), "dumpHeap"));
+        assertFalse(restrictor.isOperationAllowed(new ObjectName("javax.management.loading:type=MLet"), "getMBeansFromURL"));
+    }
+
+    @Test
+    public void attributesCannotBeWritten() throws Exception {
+        assertFalse(restrictor.isAttributeWriteAllowed(new ObjectName(BROKER), "MemoryLimit"));
+        assertFalse(restrictor.isAttributeWriteAllowed(new ObjectName(QUEUE), "MaxPageSize"));
+        assertFalse(restrictor.isAttributeWriteAllowed(new ObjectName(NETWORK_CONNECTOR), "Password"));
+        assertFalse(restrictor.isAttributeWriteAllowed(new ObjectName("java.lang:type=Memory"), "Verbose"));
+    }
+
+    @Test
+    public void attributesCanBeReadExceptSensitiveOnes() throws Exception {
+        assertTrue(restrictor.isAttributeReadAllowed(new ObjectName(BROKER), "BrokerVersion"));
+        assertTrue(restrictor.isAttributeReadAllowed(new ObjectName(QUEUE), "QueueSize"));
+        assertFalse(restrictor.isAttributeReadAllowed(new ObjectName(NETWORK_CONNECTOR), "Password"));
+        assertFalse(restrictor.isAttributeReadAllowed(new ObjectName(NETWORK_CONNECTOR), "RemotePassword"));
+        assertFalse(restrictor.isAttributeReadAllowed(new ObjectName("java.lang:type=Runtime"), "SystemProperties"));
+    }
+
+    @Test
+    public void onlyTheConsoleOriginsPassTheOriginCheck() {
+        for (String origin : new String[] {"http://localhost:8161", "http://127.0.0.1:8161", "http://[::1]:8161",
+                "https://localhost:8443", "https://127.0.0.1:8443", "https://[::1]:8443"}) {
+            assertTrue(origin, restrictor.isOriginAllowed(origin, true));
+        }
+        for (String origin : new String[] {"https://example.org", "http://localhost:9999",
+                "http://localhost.example.org:8161", "http://localhost:8161.example.org",
+                "https://example.org/http://localhost:8161"}) {
+            assertFalse(origin, restrictor.isOriginAllowed(origin, true));
+            assertFalse(origin, restrictor.isOriginAllowed(origin, false));
+        }
+        // neither an Origin nor a Referer header
+        assertFalse(restrictor.isOriginAllowed(null, true));
+    }
+
+    // JMX exposes getters and setters as attributes, everything else as operations
+    private static boolean isAttribute(Method method) {
+        String name = method.getName();
+        int parameters = method.getParameterCount();
+        Class<?> returnType = method.getReturnType();
+        return (name.startsWith("get") && parameters == 0 && returnType != void.class)
+                || (name.startsWith("is") && parameters == 0 && returnType == boolean.class)
+                || (name.startsWith("set") && parameters == 1 && returnType == void.class);
+    }
+
+    private static String signature(Method method) {
+        return method.getName() + Arrays.stream(method.getParameterTypes())
+                .map(Class::getName)
+                .collect(Collectors.joining(",", "(", ")"));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@activemq.apache.org
For additional commands, e-mail: commits-help@activemq.apache.org
For further information, visit: https://activemq.apache.org/contact