This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new 6d23795cb5 Harden Jolokia extension security defaults
6d23795cb5 is described below
commit 6d23795cb5ac615a016ba8af577b2e1ffa50eeb2
Author: James Netherton <[email protected]>
AuthorDate: Tue Aug 4 19:24:41 2026 +0100
Harden Jolokia extension security defaults
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../modules/ROOT/pages/migration-guide/3.39.0.adoc | 37 +++++
docs/modules/ROOT/pages/migration-guide/index.adoc | 1 +
.../ROOT/pages/reference/extensions/jolokia.adoc | 121 +++++++++++++-
.../jolokia/deployment/JolokiaProcessor.java | 14 +-
.../CamelJolokiaRestrictorFilePolicyTest.java | 80 +++++++++
...CamelJolokiaRestrictorPolicyDelegationTest.java | 158 ++++++++++++++++++
...elJolokiaRestrictorRemoteAccessEnabledTest.java | 76 +++++++++
.../jolokia/CamelJolokiaRestrictorTest.java | 179 +++++++++++++++++++++
.../jolokia/JolokiaDefaultHostLocalhostTest.java | 47 ++++++
.../JolokiaKubernetesClientAuthDisabledTest.java | 43 +++++
.../JolokiaKubernetesNoPrincipalWarningTest.java | 59 +++++++
.../jolokia/JolokiaKubernetesTlsFailureTest.java | 42 +++++
.../JolokiaKubernetesTlsLoopbackFallbackTest.java | 42 +++++
.../camel/quarkus/jolokia/JolokiaRecorderTest.java | 95 +++++++++++
.../jolokia/JolokiaRemoteAccessDefaultTest.java} | 37 ++---
.../jolokia/JolokiaRemoteAccessEnabledTest.java} | 38 ++---
extensions/jolokia/runtime/src/main/doc/usage.adoc | 99 +++++++++++-
.../jolokia/CamelQuarkusJolokiaLogHandler.java | 2 +-
.../camel/quarkus/jolokia/JolokiaRecorder.java | 32 +++-
.../jolokia/config/JolokiaRuntimeConfig.java | 16 +-
.../jolokia/restrictor/CamelJolokiaRestrictor.java | 108 ++++++++++++-
.../quarkus/jolokia/util/JolokiaHostUtils.java | 66 ++++++++
.../component/jolokia/it/CustomRestrictor.java | 4 +-
.../jolokia/it/JolokiaRemoteAccessAllowedIT.java} | 17 +-
.../jolokia/it/JolokiaRemoteAccessAllowedTest.java | 62 +++++++
.../quarkus/component/jolokia/it/JolokiaTest.java | 24 +++
26 files changed, 1412 insertions(+), 87 deletions(-)
diff --git a/docs/modules/ROOT/pages/migration-guide/3.39.0.adoc
b/docs/modules/ROOT/pages/migration-guide/3.39.0.adoc
new file mode 100644
index 0000000000..a4462f9a7e
--- /dev/null
+++ b/docs/modules/ROOT/pages/migration-guide/3.39.0.adoc
@@ -0,0 +1,37 @@
+= Camel Quarkus 3.39.0 Migration Guide
+
+The following guide outlines how to adapt your code to changes that were made
in Camel Quarkus 3.39.0.
+
+== Jolokia extension changes
+
+=== Default bind address changed to localhost
+
+The Jolokia agent HTTP server now binds to `localhost` by default in
production mode (previously `0.0.0.0`). Dev and test modes already defaulted to
`localhost`. Remote dev mode and WSL environments continue to default to
`0.0.0.0`. This means Jolokia is only accessible from the local machine in
production unless explicitly configured otherwise.
+
+=== Remote access blocked by default
+
+The default Camel Jolokia restrictor now blocks connections from non-loopback
(remote) addresses. This provides defense-in-depth: even if the server host is
configured to bind to all interfaces, remote requests are rejected unless the
`remote-access-allowed` property is explicitly set to `true`.
+
+=== Cross-origin requests (CORS) blocked by default
+
+The default Camel Jolokia restrictor now denies all cross-origin requests from
non-loopback origins. If you rely on browser-based monitoring tools connecting
cross-origin, configure a
https://jolokia.org/reference/html/manual/security.html[Jolokia access policy]
(`jolokia-access.xml`) with a `<cors>` section, or use a custom restrictor. See
the xref:reference/extensions/jolokia.adoc[Jolokia extension documentation] for
details.
+
+=== CamelJolokiaRestrictor no longer extends AllowAllRestrictor
+
+`CamelJolokiaRestrictor` now implements `Restrictor` directly instead of
extending Jolokia's `AllowAllRestrictor`. If you have a custom restrictor that
extends `AllowAllRestrictor`, consider switching to `extends
CamelJolokiaRestrictor` to inherit the secure defaults (remote access control,
CORS blocking, MBean domain filtering, `jolokia-access.xml` policy delegation).
+
+=== Jolokia access policy (jolokia-access.xml) delegation
+
+The default Camel Jolokia restrictor now loads `jolokia-access.xml` from the
classpath if present and delegates remote access, CORS, HTTP method, and
request type checks to it for non-loopback requests. This allows fine-grained
access control alongside the Camel MBean domain filtering. The policy location
can be configured via
`quarkus.camel.jolokia.additional-properties."policyLocation"`.
+
+=== Required configuration for Kubernetes remote management
+
+If you use tools such as https://hawt.io/[Hawtio] or
https://github.com/hawtio/hawtio-online[hawtio-online] to connect to Jolokia
running on Kubernetes or OpenShift, you must add the following configuration to
`application.properties`.
+
+[source]
+----
+quarkus.camel.jolokia.server.host=0.0.0.0
+quarkus.camel.jolokia.remote-access-allowed=true
+----
+
+This is safe when combined with SSL client authentication (enabled by default
in Kubernetes environments), which ensures only clients presenting a valid
certificate can connect. Refer to the
xref:reference/extensions/jolokia.adoc[Jolokia extension documentation] for
full details.
diff --git a/docs/modules/ROOT/pages/migration-guide/index.adoc
b/docs/modules/ROOT/pages/migration-guide/index.adoc
index 6a4f52a81f..cf05efe3e0 100644
--- a/docs/modules/ROOT/pages/migration-guide/index.adoc
+++ b/docs/modules/ROOT/pages/migration-guide/index.adoc
@@ -4,6 +4,7 @@ We do frequent releases, a release almost every month, and even
though we strive
Listed here are guides on how to migrate between major versions and anything
of significance to watch for when upgrading from minor versions.
+* xref:migration-guide/3.39.0.adoc[Camel Quarkus 3.38.x to Camel Quarkus
3.39.0 migration guide]
* xref:migration-guide/3.38.0.adoc[Camel Quarkus 3.36.x to Camel Quarkus
3.38.0 migration guide]
* xref:migration-guide/3.36.0.adoc[Camel Quarkus 3.35.x to Camel Quarkus
3.36.0 migration guide]
* xref:migration-guide/3.35.0.adoc[Camel Quarkus 3.33.x to Camel Quarkus
3.35.0 migration guide]
diff --git a/docs/modules/ROOT/pages/reference/extensions/jolokia.adoc
b/docs/modules/ROOT/pages/reference/extensions/jolokia.adoc
index b39c8db3ec..501b9ca3a9 100644
--- a/docs/modules/ROOT/pages/reference/extensions/jolokia.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/jolokia.adoc
@@ -44,11 +44,11 @@ This extension adds https://jolokia.org/[Jolokia] support
to your application.
[id="extensions-jolokia-usage-jolokia-http-endpoints"]
=== Jolokia HTTP endpoints
-In prod mode, Jolokia is accessible at the following URLs.
+Jolokia is accessible at the following URL.
-* http://0.0.0.0:8778/jolokia/
+* http://localhost:8778/jolokia/
-In dev and test modes Jolokia is bound only to `localhost`.
+By default, the Jolokia agent HTTP server binds to `localhost`. Remote dev
mode and WSL environments default to `0.0.0.0`.
If you want to disable Jolokia entirely, then add the following configuration
to `application.properties`.
@@ -105,11 +105,11 @@ quarkus.camel.jolokia.register-camel-restrictor=false
[id="extensions-jolokia-usage-create-a-custom-restrictor"]
==== Create a custom restrictor
-You can create your own restrictor class and register it with Jolokia.
+You can create your own restrictor class and register it with Jolokia.
Extending `CamelJolokiaRestrictor` inherits the secure defaults (remote access
control, CORS blocking, MBean domain filtering).
[source,java]
----
-public class CustomRestrictor extends AllowAllRestrictor {
+public class CustomRestrictor extends CamelJolokiaRestrictor {
// Override methods to apply custom restrictions
}
----
@@ -149,13 +149,106 @@ This functionality can be disabled by adding the
following configuration to `app
quarkus.camel.jolokia.kubernetes.client-authentication-enabled=false
----
-Note that if you choose to use
https://github.com/hawtio/hawtio-online[hawtio-online] to connect to your
running application, then you must configure the Jolokia client principal.
+[id="extensions-jolokia-usage-configuring-jolokia-for-remote-management-tools"]
+==== Configuring Jolokia for remote management tools
+
+Tools such as https://hawt.io/[Hawtio] and
https://github.com/hawtio/hawtio-online[hawtio-online] connect to Jolokia via
the pod IP address, which is a non-loopback address. By default, the Jolokia
agent binds to `localhost` and the default restrictor blocks connections from
non-loopback addresses.
+
+To allow remote management tools to connect to Jolokia in Kubernetes, add the
following configuration to `application.properties`.
+
+[source]
+----
+quarkus.camel.jolokia.server.host=0.0.0.0
+quarkus.camel.jolokia.remote-access-allowed=true
+----
+
+This is safe when combined with SSL client authentication (enabled by default
in Kubernetes), which ensures that only clients presenting a valid certificate
signed by the service CA can connect. If you use hawtio-online, you must also
configure the Jolokia client principal.
[source]
----
quarkus.camel.jolokia.kubernetes.client-principal=cn=hawtio-online.hawtio.svc
----
+[id="extensions-jolokia-usage-security"]
+=== Security
+
+NOTE: Even when bound to `localhost`, Jolokia does not require authentication
by default. On shared hosts or in container environments where localhost may be
reachable from other containers, consider using a custom restrictor or Jolokia
access policy to restrict access further.
+
+[id="extensions-jolokia-usage-network-binding"]
+==== Network binding
+
+By default, the Jolokia agent HTTP server binds to `localhost`, making it
accessible only from the local machine. If you need to expose Jolokia to remote
hosts, you must explicitly configure the bind address and enable remote access.
+
+[source]
+----
+quarkus.camel.jolokia.server.host=0.0.0.0
+quarkus.camel.jolokia.remote-access-allowed=true
+----
+
+[id="extensions-jolokia-usage-remote-access-control"]
+==== Remote access control
+
+The default Camel Jolokia restrictor blocks connections from non-loopback
addresses. This is controlled by the
`quarkus.camel.jolokia.remote-access-allowed` property (default `false`). When
set to `false`, only connections from loopback addresses (e.g. `127.0.0.1`,
`::1`) are accepted, regardless of the server bind address.
+
+[id="extensions-jolokia-usage-cross-origin-requests-cors"]
+==== Cross-origin requests (CORS)
+
+The default Camel Jolokia restrictor denies all cross-origin requests. If you
need to allow specific origins (e.g. for a browser-based monitoring tool
connecting cross-origin), you can use a
https://jolokia.org/reference/html/manual/security.html[Jolokia access policy]
(`jolokia-access.xml`) with a `<cors>` section, or configure a custom
restrictor that overrides `isOriginAllowed()`.
+
+[id="extensions-jolokia-usage-jolokia-access-policy"]
+==== Jolokia access policy
+
+Jolokia supports fine-grained access control via an XML policy file
(`jolokia-access.xml`). This can restrict access by IP address, CORS origin,
HTTP method, and MBean operation.
+
+The default Camel restrictor automatically loads `jolokia-access.xml` from the
classpath if present. Place the file in `src/main/resources/jolokia-access.xml`
to use it alongside the Camel restrictor. The Camel restrictor always allows
loopback connections. For non-loopback requests, it delegates remote access,
CORS, allowed request types (`<commands>`), and HTTP method (`<http>`) checks
to the policy file, while continuing to enforce MBean domain filtering.
+
+For example, the following `jolokia-access.xml` allows access from the
`10.0.0.0/8` subnet, CORS requests from a monitoring tool, and restricts
Jolokia to read-only operations over HTTP GET.
+
+[source,xml]
+----
+<?xml version="1.0" encoding="UTF-8"?>
+<restrict>
+ <remote>
+ <host>10.0.0.0/8</host>
+ </remote>
+ <cors>
+ <allow-origin>http://monitoring.example.com</allow-origin>
+ </cors>
+ <commands>
+ <command>read</command>
+ <command>list</command>
+ <command>version</command>
+ <command>search</command>
+ </commands>
+ <http>
+ <method>get</method>
+ </http>
+</restrict>
+----
+
+Refer to the https://jolokia.org/reference/html/manual/security.html[Jolokia
security documentation] for the full policy file format.
+
+To load the policy file from a different location, such as a file path mounted
from a Kubernetes ConfigMap, configure the `policyLocation` property.
+
+[source]
+----
+quarkus.camel.jolokia.additional-properties."policyLocation"=file:/etc/jolokia/jolokia-access.xml
+----
+
+As an alternative, you can disable the default Camel restrictor entirely and
let Jolokia manage the policy file directly. This gives full control to
`jolokia-access.xml` (including MBean-level rules) but loses the Camel MBean
domain filtering.
+
+[source]
+----
+quarkus.camel.jolokia.register-camel-restrictor=false
+----
+
+If you are building a native executable, the policy file must be explicitly
included as a native resource.
+
+[source]
+----
+quarkus.native.resources.includes=jolokia-access.xml
+----
+
[id="extensions-jolokia-camel-quarkus-limitations"]
== Camel Quarkus limitations
@@ -212,8 +305,8 @@ This can be done via `@Inject CamelQuarkusJolokiaServer`
and then invoking the `
a|
[[quarkus-camel-jolokia-server-host]]`link:#quarkus-camel-jolokia-server-host[quarkus.camel.jolokia.server.host]`
The host address to which the Jolokia agent HTTP server should bind.
-When unspecified, the default is localhost for dev and test mode.
-In prod mode the default is to bind to all interfaces at 0.0.0.0.
+When unspecified, the default is localhost in all modes except remote dev,
+where it defaults to 0.0.0.0.
| `string`
|
@@ -271,6 +364,18 @@ following MBean domains.
Note that this option has no effect if
`quarkus.camel.jolokia.additional-properties."restrictorClass"` is set.
| `boolean`
| `true`
+
+a|
[[quarkus-camel-jolokia-remote-access-allowed]]`link:#quarkus-camel-jolokia-remote-access-allowed[quarkus.camel.jolokia.remote-access-allowed]`
+
+When `true`, the default Camel Jolokia restrictor allows connections from
non-loopback (remote) addresses.
+When `false` (the default), only connections from loopback addresses (e.g.
127.0.0.1, ::1) are permitted.
+This provides defense-in-depth: even if the server host is configured to bind
to all interfaces,
+remote requests are rejected unless this property is explicitly set to `true`.
+
+This option only takes effect when `register-camel-restrictor` is `true` and a
custom restrictor class
+is not configured via
`quarkus.camel.jolokia.additional-properties."restrictorClass"`.
+| `boolean`
+| `false`
|===
[.configuration-legend]
diff --git
a/extensions/jolokia/deployment/src/main/java/org/apache/camel/quarkus/jolokia/deployment/JolokiaProcessor.java
b/extensions/jolokia/deployment/src/main/java/org/apache/camel/quarkus/jolokia/deployment/JolokiaProcessor.java
index 79e5a0285a..43c598f426 100644
---
a/extensions/jolokia/deployment/src/main/java/org/apache/camel/quarkus/jolokia/deployment/JolokiaProcessor.java
+++
b/extensions/jolokia/deployment/src/main/java/org/apache/camel/quarkus/jolokia/deployment/JolokiaProcessor.java
@@ -61,6 +61,7 @@ import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.StringHelper;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
+import org.jboss.jandex.IndexView;
import org.jolokia.core.api.LogHandler;
import org.jolokia.server.core.service.api.Restrictor;
import org.jolokia.server.core.service.impl.QuietLogHandler;
@@ -161,8 +162,10 @@ public class JolokiaProcessor {
.methods(true)
.build());
+ IndexView index = combinedIndex.getIndex();
+
// Register custom (non-OSGi) Jolokia Restrictor impls for reflection
- Set<String> jolokiaRestrictorClasses = combinedIndex.getIndex()
+ Set<String> jolokiaRestrictorClasses = index
.getAllKnownImplementations(Restrictor.class)
.stream()
.map(ClassInfo::name)
@@ -170,11 +173,18 @@ public class JolokiaProcessor {
.filter(className ->
!className.startsWith("org.jolokia.server.core.osgi"))
.collect(Collectors.toSet());
+ // Register the builtin CamelJolokiaRestrictor and subclasses
jolokiaRestrictorClasses.add(CamelJolokiaRestrictor.class.getName());
+ index.getAllKnownSubclasses(CamelJolokiaRestrictor.class)
+ .stream()
+ .map(ClassInfo::name)
+ .map(DotName::toString)
+ .forEach(jolokiaRestrictorClasses::add);
+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(jolokiaRestrictorClasses.toArray(new
String[0])).build());
// Register custom LogHandler classes for reflection
- Set<String> jolokiaLogHandlerClasses = combinedIndex.getIndex()
+ Set<String> jolokiaLogHandlerClasses = index
.getAllKnownImplementations(LogHandler.class)
.stream()
.map(ClassInfo::name)
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorFilePolicyTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorFilePolicyTest.java
new file mode 100644
index 0000000000..88d1005997
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorFilePolicyTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import io.quarkus.test.QuarkusUnitTest;
+import org.apache.camel.quarkus.jolokia.restrictor.CamelJolokiaRestrictor;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CamelJolokiaRestrictorFilePolicyTest {
+
+ private static final String JOLOKIA_ACCESS_XML = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <restrict>
+ <remote>
+ <host>10.0.0.0/8</host>
+ </remote>
+ </restrict>
+ """;
+
+ static final File POLICY_FILE;
+
+ static {
+ try {
+ POLICY_FILE = Files.createTempFile("jolokia-access",
".xml").toFile();
+ POLICY_FILE.deleteOnExit();
+ Files.writeString(POLICY_FILE.toPath(), JOLOKIA_ACCESS_XML);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .overrideConfigKey("quarkus.camel.jolokia.server.port", "0")
+
.overrideConfigKey("quarkus.camel.jolokia.additional-properties.policyLocation",
+ POLICY_FILE.toURI().toString());
+
+ @Test
+ void filePolicyAllowsConfiguredSubnet() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isRemoteAccessAllowed("10.0.0.1"));
+ assertTrue(restrictor.isRemoteAccessAllowed("10.255.255.255"));
+ }
+
+ @Test
+ void filePolicyDeniesOtherAddresses() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isRemoteAccessAllowed("192.168.1.1"));
+ assertFalse(restrictor.isRemoteAccessAllowed("172.16.0.1"));
+ }
+
+ @Test
+ void loopbackStillAllowedRegardlessOfPolicy() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isRemoteAccessAllowed("127.0.0.1"));
+ assertTrue(restrictor.isRemoteAccessAllowed("::1"));
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorPolicyDelegationTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorPolicyDelegationTest.java
new file mode 100644
index 0000000000..22d9324dc5
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorPolicyDelegationTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import io.quarkus.test.QuarkusUnitTest;
+import org.apache.camel.quarkus.jolokia.restrictor.CamelJolokiaRestrictor;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.jolokia.server.core.util.HttpMethod;
+import org.jolokia.server.core.util.RequestType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CamelJolokiaRestrictorPolicyDelegationTest {
+
+ private static final String JOLOKIA_ACCESS_XML = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <restrict>
+ <remote>
+ <host>10.0.0.0/8</host>
+ </remote>
+ <cors>
+ <allow-origin>http://example.host.com</allow-origin>
+ </cors>
+ <commands>
+ <command>list</command>
+ <command>version</command>
+ <command>search</command>
+ </commands>
+ <http>
+ <method>get</method>
+ </http>
+ </restrict>
+ """;
+
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .overrideConfigKey("quarkus.camel.jolokia.server.port", "0")
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addAsResource(new StringAsset(JOLOKIA_ACCESS_XML),
"jolokia-access.xml"));
+
+ @Test
+ void loopbackAlwaysAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isRemoteAccessAllowed("127.0.0.1"));
+ assertTrue(restrictor.isRemoteAccessAllowed("::1"));
+ }
+
+ @Test
+ void policyAllowedSubnetAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isRemoteAccessAllowed("10.0.0.1"));
+ assertTrue(restrictor.isRemoteAccessAllowed("10.255.255.255"));
+ }
+
+ @Test
+ void nonPolicyRemoteStillDenied() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isRemoteAccessAllowed("192.168.1.1"));
+ assertFalse(restrictor.isRemoteAccessAllowed("172.16.0.1"));
+ }
+
+ @Test
+ void policyAllowedCorsOriginAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed("http://example.host.com",
false));
+ }
+
+ @Test
+ void nonPolicyCorsOriginDenied() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isOriginAllowed("http://untrusted.example",
false));
+ }
+
+ @Test
+ void nullOriginStillAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed(null, false));
+ assertTrue(restrictor.isOriginAllowed(null, true));
+ }
+
+ @Test
+ void loopbackOriginStillAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed("http://localhost:8080", false));
+ assertTrue(restrictor.isOriginAllowed("http://127.0.0.1:9090", false));
+ }
+
+ @Test
+ void policyAllowedRequestTypesAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isTypeAllowed(RequestType.LIST));
+ assertTrue(restrictor.isTypeAllowed(RequestType.VERSION));
+ assertTrue(restrictor.isTypeAllowed(RequestType.SEARCH));
+ }
+
+ @Test
+ void policyDeniedRequestTypesDenied() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isTypeAllowed(RequestType.READ));
+ assertFalse(restrictor.isTypeAllowed(RequestType.WRITE));
+ assertFalse(restrictor.isTypeAllowed(RequestType.EXEC));
+ }
+
+ @Test
+ void policyDeniedReadBlocksAttributeRead() throws
MalformedObjectNameException {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ ObjectName camelMBean = new
ObjectName("org.apache.camel:context=camel-1,type=context,name=\"camel-1\"");
+ assertFalse(restrictor.isAttributeReadAllowed(camelMBean, "Uptime"));
+ }
+
+ @Test
+ void policyDeniedWriteBlocksAttributeWrite() throws
MalformedObjectNameException {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ ObjectName camelMBean = new
ObjectName("org.apache.camel:context=camel-1,type=context,name=\"camel-1\"");
+ assertFalse(restrictor.isAttributeWriteAllowed(camelMBean, "Tracing"));
+ }
+
+ @Test
+ void policyDeniedExecBlocksOperation() throws MalformedObjectNameException
{
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ ObjectName camelMBean = new
ObjectName("org.apache.camel:context=camel-1,type=context,name=\"camel-1\"");
+ assertFalse(restrictor.isOperationAllowed(camelMBean, "getUptime"));
+ }
+
+ @Test
+ void policyAllowedHttpMethodAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isHttpMethodAllowed(HttpMethod.GET));
+ }
+
+ @Test
+ void policyDeniedHttpMethodDenied() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isHttpMethodAllowed(HttpMethod.POST));
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorRemoteAccessEnabledTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorRemoteAccessEnabledTest.java
new file mode 100644
index 0000000000..af213c184a
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorRemoteAccessEnabledTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import io.quarkus.test.QuarkusUnitTest;
+import org.apache.camel.quarkus.jolokia.restrictor.CamelJolokiaRestrictor;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CamelJolokiaRestrictorRemoteAccessEnabledTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication()
+ .overrideConfigKey("quarkus.camel.jolokia.remote-access-allowed",
"true");
+
+ @Test
+ void allRemoteAddressesAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isRemoteAccessAllowed("192.168.1.1"));
+ assertTrue(restrictor.isRemoteAccessAllowed("10.0.0.1"));
+ assertTrue(restrictor.isRemoteAccessAllowed("172.16.0.1"));
+ }
+
+ @Test
+ void loopbackStillAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isRemoteAccessAllowed("127.0.0.1"));
+ assertTrue(restrictor.isRemoteAccessAllowed("::1"));
+ }
+
+ @Test
+ void allOriginsAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed("http://untrusted.example",
false));
+ assertTrue(restrictor.isOriginAllowed("http://192.168.1.1:8080",
false));
+ assertTrue(restrictor.isOriginAllowed("http://example.com", true));
+ }
+
+ @Test
+ void nullOriginAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed(null, false));
+ assertTrue(restrictor.isOriginAllowed(null, true));
+ }
+
+ @Test
+ void mbeanDomainFilteringStillEnforced() throws
MalformedObjectNameException {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ ObjectName camelMBean = new
ObjectName("org.apache.camel:context=camel-1,type=context,name=\"camel-1\"");
+ assertTrue(restrictor.isOperationAllowed(camelMBean, "getUptime"));
+
+ ObjectName disallowedMBean = new ObjectName("com.example:type=Test");
+ assertFalse(restrictor.isOperationAllowed(disallowedMBean,
"doSomething"));
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorTest.java
new file mode 100644
index 0000000000..66f88b03d3
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/CamelJolokiaRestrictorTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import io.quarkus.test.QuarkusUnitTest;
+import org.apache.camel.quarkus.jolokia.restrictor.CamelJolokiaRestrictor;
+import org.jolokia.server.core.util.HttpMethod;
+import org.jolokia.server.core.util.RequestType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CamelJolokiaRestrictorTest {
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication();
+
+ @Test
+ void ipv4LoopbackAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isRemoteAccessAllowed("127.0.0.1"));
+ }
+
+ @Test
+ void ipv6LoopbackAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isRemoteAccessAllowed("::1"));
+ }
+
+ @Test
+ void nonLoopbackRejectedByDefault() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isRemoteAccessAllowed("192.168.1.1"));
+ }
+
+ @Test
+ void allInterfacesRejectedByDefault() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isRemoteAccessAllowed("0.0.0.0"));
+ }
+
+ @Test
+ void privateNetworkRejectedByDefault() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isRemoteAccessAllowed("10.0.0.1"));
+ }
+
+ @Test
+ void nullArgsRejected() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isRemoteAccessAllowed((String[]) null));
+ }
+
+ @Test
+ void emptyArgsRejected() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isRemoteAccessAllowed());
+ }
+
+ @Test
+ void remoteOriginRejectedByDefault() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isOriginAllowed("http://untrusted.example",
false));
+ assertFalse(restrictor.isOriginAllowed("http://192.168.1.1:8080",
false));
+ }
+
+ @Test
+ void loopbackOriginAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed("http://localhost:8080", false));
+ assertTrue(restrictor.isOriginAllowed("http://127.0.0.1:9090", false));
+ }
+
+ @Test
+ void noOriginHeaderAllowedWithoutStrictCheck() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed(null, false));
+ }
+
+ @Test
+ void noOriginHeaderAllowedWithStrictCheck() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed(null, true));
+ }
+
+ @Test
+ void loopbackOriginAllowedWithStrictCheck() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isOriginAllowed("http://localhost:8080", true));
+ assertTrue(restrictor.isOriginAllowed("http://127.0.0.1:9090", true));
+ }
+
+ @Test
+ void remoteOriginRejectedWithStrictCheck() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isOriginAllowed("http://untrusted.example",
true));
+ }
+
+ @Test
+ void malformedOriginRejected() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isOriginAllowed("not a valid uri", false));
+ }
+
+ @Test
+ void httpGetAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isHttpMethodAllowed(HttpMethod.GET));
+ }
+
+ @Test
+ void httpPostAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isHttpMethodAllowed(HttpMethod.POST));
+ }
+
+ @Test
+ void requestTypesAllowed() {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isTypeAllowed(RequestType.READ));
+ assertTrue(restrictor.isTypeAllowed(RequestType.EXEC));
+ assertTrue(restrictor.isTypeAllowed(RequestType.LIST));
+ assertTrue(restrictor.isTypeAllowed(RequestType.SEARCH));
+ assertTrue(restrictor.isTypeAllowed(RequestType.VERSION));
+ }
+
+ @Test
+ void allowedDomainReadWriteExecAllowed() throws
MalformedObjectNameException {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ ObjectName camelMBean = new
ObjectName("org.apache.camel:context=camel-1,type=context,name=\"camel-1\"");
+ assertTrue(restrictor.isAttributeReadAllowed(camelMBean, "Uptime"));
+ assertTrue(restrictor.isAttributeWriteAllowed(camelMBean, "Tracing"));
+ assertTrue(restrictor.isOperationAllowed(camelMBean,
"sendStringBody"));
+ }
+
+ @Test
+ void disallowedDomainReadWriteExecDenied() throws
MalformedObjectNameException {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ ObjectName disallowed = new ObjectName("com.example:type=Test");
+ assertFalse(restrictor.isAttributeReadAllowed(disallowed, "Value"));
+ assertFalse(restrictor.isAttributeWriteAllowed(disallowed, "Value"));
+ assertFalse(restrictor.isOperationAllowed(disallowed, "doSomething"));
+ }
+
+ @Test
+ void allowedDomainNotHidden() throws MalformedObjectNameException {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertFalse(restrictor.isObjectNameHidden(
+ new
ObjectName("org.apache.camel:context=camel-1,type=context,name=\"camel-1\"")));
+ assertFalse(restrictor.isObjectNameHidden(new
ObjectName("java.lang:type=Runtime")));
+ assertFalse(restrictor.isObjectNameHidden(new
ObjectName("java.nio:type=BufferPool,name=direct")));
+ }
+
+ @Test
+ void disallowedDomainIsHidden() throws MalformedObjectNameException {
+ CamelJolokiaRestrictor restrictor = new CamelJolokiaRestrictor();
+ assertTrue(restrictor.isObjectNameHidden(new
ObjectName("com.example:type=Test")));
+ assertTrue(restrictor.isObjectNameHidden(new
ObjectName("java.util.logging:type=Logging")));
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaDefaultHostLocalhostTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaDefaultHostLocalhostTest.java
new file mode 100644
index 0000000000..4b417c4c9d
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaDefaultHostLocalhostTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import org.apache.camel.quarkus.jolokia.util.JolokiaHostUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class JolokiaDefaultHostLocalhostTest {
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication();
+
+ @Test
+ void defaultHostIsLoopback() {
+ RestAssured.port = 8778;
+ String url = RestAssured.get("/jolokia/")
+ .then()
+ .statusCode(200)
+ .extract()
+ .body()
+ .jsonPath()
+ .getString("value.details.url");
+
+ String host = url.replaceFirst("https?://",
"").replaceFirst("[:\\[].*", "");
+ assertTrue(JolokiaHostUtils.isLoopbackAddress(host),
+ "Default host should resolve to a loopback address, but got: "
+ host);
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesClientAuthDisabledTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesClientAuthDisabledTest.java
new file mode 100644
index 0000000000..7ca25eaafe
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesClientAuthDisabledTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.hamcrest.Matchers.equalTo;
+
+class JolokiaKubernetesClientAuthDisabledTest {
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication()
+ .overrideConfigKey("kubernetes.service.host", "fake-host")
+
.overrideConfigKey("quarkus.camel.jolokia.kubernetes.service-ca-cert",
"/non/existent/ca.crt")
+
.overrideConfigKey("quarkus.camel.jolokia.kubernetes.client-authentication-enabled",
"false")
+ .overrideConfigKey("quarkus.camel.jolokia.server.host", "0.0.0.0");
+
+ @Test
+ void applicationStartsWhenClientAuthDisabled() {
+ RestAssured.port = 8778;
+ RestAssured.get("/jolokia/")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo(200));
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesNoPrincipalWarningTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesNoPrincipalWarningTest.java
new file mode 100644
index 0000000000..d06b65c5d5
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesNoPrincipalWarningTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.logging.Level;
+
+import io.quarkus.test.QuarkusUnitTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class JolokiaKubernetesNoPrincipalWarningTest {
+
+ static final File CA_CERT;
+
+ static {
+ try {
+ CA_CERT = Files.createTempFile("fake-ca", ".crt").toFile();
+ CA_CERT.deleteOnExit();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication()
+ .overrideConfigKey("kubernetes.service.host", "fake-host")
+
.overrideConfigKey("quarkus.camel.jolokia.kubernetes.service-ca-cert",
CA_CERT.getAbsolutePath())
+ .setLogRecordPredicate(record ->
record.getLevel().equals(Level.WARNING))
+ .assertLogRecords(records -> assertTrue(
+ records.stream().anyMatch(r -> r.getMessage().contains("no
client principal is configured")),
+ "Expected a warning about missing client principal"));
+
+ @Test
+ void applicationStartsWithClientPrincipalWarning() {
+ // The assertLogRecords callback above verifies the warning was logged.
+ // The Jolokia server is configured with HTTPS (due to the fake CA
cert),
+ // so we cannot make a plain HTTP request — app startup alone is
sufficient.
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesTlsFailureTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesTlsFailureTest.java
new file mode 100644
index 0000000000..533ce1984e
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesTlsFailureTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import io.quarkus.test.QuarkusUnitTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class JolokiaKubernetesTlsFailureTest {
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication()
+ .overrideConfigKey("kubernetes.service.host", "fake-host")
+
.overrideConfigKey("quarkus.camel.jolokia.kubernetes.service-ca-cert",
"/non/existent/ca.crt")
+ .overrideConfigKey("quarkus.camel.jolokia.server.host", "0.0.0.0")
+ .assertException(t -> {
+ if (t.getMessage() == null
+ || !t.getMessage().contains("Kubernetes SSL client
authentication is enabled")) {
+ throw new AssertionError(
+ "Expected RuntimeException about Kubernetes SSL
client authentication failure, got: " + t, t);
+ }
+ });
+
+ @Test
+ void applicationShouldFailToStart() {
+ // The application should not start — the assertException above
validates the failure
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesTlsLoopbackFallbackTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesTlsLoopbackFallbackTest.java
new file mode 100644
index 0000000000..a2a13e1c42
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaKubernetesTlsLoopbackFallbackTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.hamcrest.Matchers.equalTo;
+
+class JolokiaKubernetesTlsLoopbackFallbackTest {
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication()
+ .overrideConfigKey("kubernetes.service.host", "fake-host")
+
.overrideConfigKey("quarkus.camel.jolokia.kubernetes.service-ca-cert",
"/non/existent/ca.crt")
+ .overrideConfigKey("quarkus.camel.jolokia.server.host",
"localhost");
+
+ @Test
+ void applicationStartsWithWarningWhenCaCertMissingAndHostIsLoopback() {
+ RestAssured.port = 8778;
+ RestAssured.get("/jolokia/")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo(200));
+ }
+}
diff --git
a/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRecorderTest.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRecorderTest.java
new file mode 100644
index 0000000000..10138213dc
--- /dev/null
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRecorderTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.camel.quarkus.jolokia;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class JolokiaRecorderTest {
+
+ @Test
+ void localhostIsLoopback() {
+ assertTrue(JolokiaRecorder.isLoopbackHost("localhost"));
+ }
+
+ @Test
+ void ipv4LoopbackIsLoopback() {
+ assertTrue(JolokiaRecorder.isLoopbackHost("127.0.0.1"));
+ }
+
+ @Test
+ void ipv6LoopbackIsLoopback() {
+ assertTrue(JolokiaRecorder.isLoopbackHost("::1"));
+ }
+
+ @Test
+ void allInterfacesIsNotLoopback() {
+ assertFalse(JolokiaRecorder.isLoopbackHost("0.0.0.0"));
+ }
+
+ @Test
+ void nonLoopbackIpIsNotLoopback() {
+ assertFalse(JolokiaRecorder.isLoopbackHost("192.168.1.1"));
+ }
+
+ @Test
+ void ipv4NonLoopbackRangeIsNotLoopback() {
+ assertFalse(JolokiaRecorder.isLoopbackHost("10.0.0.1"));
+ }
+
+ @Test
+ void ipv6BracketWrappedIsLoopback() {
+ assertTrue(JolokiaRecorder.isLoopbackHost("[::1]"));
+ }
+
+ @Test
+ void localhostLocaldomainIsLoopback() {
+ assertTrue(JolokiaRecorder.isLoopbackHost("localhost.localdomain"));
+ }
+
+ @Test
+ void ipv4FullLoopbackRangeIsLoopback() {
+ assertTrue(JolokiaRecorder.isLoopbackHost("127.255.255.255"));
+ }
+
+ @Test
+ void ipv6LongFormLoopbackIsLoopback() {
+ assertTrue(JolokiaRecorder.isLoopbackHost("0:0:0:0:0:0:0:1"));
+ }
+
+ @Test
+ void nullIsNotLoopback() {
+ assertFalse(JolokiaRecorder.isLoopbackHost(null));
+ }
+
+ @Test
+ void emptyStringIsNotLoopback() {
+ assertFalse(JolokiaRecorder.isLoopbackHost(""));
+ }
+
+ @Test
+ void ipv4LoopbackWithExtraOctetsIsNotLoopback() {
+
assertFalse(JolokiaRecorder.isLoopbackHost("127.0.0.1.untrusted.example"));
+ }
+
+ @Test
+ void ipv4LoopbackOctetOverflowIsNotLoopback() {
+ assertFalse(JolokiaRecorder.isLoopbackHost("127.0.0.256"));
+ }
+}
diff --git
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRemoteAccessDefaultTest.java
similarity index 58%
copy from
extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
copy to
extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRemoteAccessDefaultTest.java
index d70297a2f2..d5784f4b0a 100644
---
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRemoteAccessDefaultTest.java
@@ -16,29 +16,24 @@
*/
package org.apache.camel.quarkus.jolokia;
-import org.jboss.logging.Logger;
-import org.jolokia.core.api.LogHandler;
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
-final class CamelQuarkusJolokiaLogHandler implements LogHandler {
- private static final Logger LOG =
Logger.getLogger(CamelQuarkusJolokiaLogHandler.class);
+import static org.hamcrest.Matchers.equalTo;
- @Override
- public void debug(String s) {
- LOG.debug(s);
- }
-
- @Override
- public void info(String s) {
- LOG.info(s);
- }
-
- @Override
- public void error(String s, Throwable throwable) {
- LOG.error(s);
- }
+class JolokiaRemoteAccessDefaultTest {
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication();
- @Override
- public boolean isDebug() {
- return LOG.isDebugEnabled();
+ @Test
+ void loopbackConnectionAllowedByDefault() {
+ RestAssured.port = 8778;
+ RestAssured.get("/jolokia/")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo(200));
}
}
diff --git
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRemoteAccessEnabledTest.java
similarity index 55%
copy from
extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
copy to
extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRemoteAccessEnabledTest.java
index d70297a2f2..bba2ef859d 100644
---
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
+++
b/extensions/jolokia/deployment/src/test/java/org/apache/camel/quarkus/jolokia/JolokiaRemoteAccessEnabledTest.java
@@ -16,29 +16,25 @@
*/
package org.apache.camel.quarkus.jolokia;
-import org.jboss.logging.Logger;
-import org.jolokia.core.api.LogHandler;
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
-final class CamelQuarkusJolokiaLogHandler implements LogHandler {
- private static final Logger LOG =
Logger.getLogger(CamelQuarkusJolokiaLogHandler.class);
+import static org.hamcrest.Matchers.equalTo;
- @Override
- public void debug(String s) {
- LOG.debug(s);
- }
-
- @Override
- public void info(String s) {
- LOG.info(s);
- }
-
- @Override
- public void error(String s, Throwable throwable) {
- LOG.error(s);
- }
+class JolokiaRemoteAccessEnabledTest {
+ @RegisterExtension
+ static final QuarkusUnitTest CONFIG = new QuarkusUnitTest()
+ .withEmptyApplication()
+ .overrideConfigKey("quarkus.camel.jolokia.remote-access-allowed",
"true");
- @Override
- public boolean isDebug() {
- return LOG.isDebugEnabled();
+ @Test
+ void connectionAllowedWhenRemoteAccessEnabled() {
+ RestAssured.port = 8778;
+ RestAssured.get("/jolokia/")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo(200));
}
}
diff --git a/extensions/jolokia/runtime/src/main/doc/usage.adoc
b/extensions/jolokia/runtime/src/main/doc/usage.adoc
index 1299ab8e0a..44f1dfbe3c 100644
--- a/extensions/jolokia/runtime/src/main/doc/usage.adoc
+++ b/extensions/jolokia/runtime/src/main/doc/usage.adoc
@@ -2,11 +2,11 @@ This extension adds https://jolokia.org/[Jolokia] support to
your application.
=== Jolokia HTTP endpoints
-In prod mode, Jolokia is accessible at the following URLs.
+Jolokia is accessible at the following URL.
-* http://0.0.0.0:8778/jolokia/
+* http://localhost:8778/jolokia/
-In dev and test modes Jolokia is bound only to `localhost`.
+By default, the Jolokia agent HTTP server binds to `localhost`. Remote dev
mode and WSL environments default to `0.0.0.0`.
If you want to disable Jolokia entirely, then add the following configuration
to `application.properties`.
@@ -58,11 +58,11 @@ quarkus.camel.jolokia.register-camel-restrictor=false
==== Create a custom restrictor
-You can create your own restrictor class and register it with Jolokia.
+You can create your own restrictor class and register it with Jolokia.
Extending `CamelJolokiaRestrictor` inherits the secure defaults (remote access
control, CORS blocking, MBean domain filtering).
[source,java]
----
-public class CustomRestrictor extends AllowAllRestrictor {
+public class CustomRestrictor extends CamelJolokiaRestrictor {
// Override methods to apply custom restrictions
}
----
@@ -99,9 +99,96 @@ This functionality can be disabled by adding the following
configuration to `app
quarkus.camel.jolokia.kubernetes.client-authentication-enabled=false
----
-Note that if you choose to use
https://github.com/hawtio/hawtio-online[hawtio-online] to connect to your
running application, then you must configure the Jolokia client principal.
+==== Configuring Jolokia for remote management tools
+
+Tools such as https://hawt.io/[Hawtio] and
https://github.com/hawtio/hawtio-online[hawtio-online] connect to Jolokia via
the pod IP address, which is a non-loopback address. By default, the Jolokia
agent binds to `localhost` and the default restrictor blocks connections from
non-loopback addresses.
+
+To allow remote management tools to connect to Jolokia in Kubernetes, add the
following configuration to `application.properties`.
+
+[source]
+----
+quarkus.camel.jolokia.server.host=0.0.0.0
+quarkus.camel.jolokia.remote-access-allowed=true
+----
+
+This is safe when combined with SSL client authentication (enabled by default
in Kubernetes), which ensures that only clients presenting a valid certificate
signed by the service CA can connect. If you use hawtio-online, you must also
configure the Jolokia client principal.
[source]
----
quarkus.camel.jolokia.kubernetes.client-principal=cn=hawtio-online.hawtio.svc
----
+
+=== Security
+
+NOTE: Even when bound to `localhost`, Jolokia does not require authentication
by default. On shared hosts or in container environments where localhost may be
reachable from other containers, consider using a custom restrictor or Jolokia
access policy to restrict access further.
+
+==== Network binding
+
+By default, the Jolokia agent HTTP server binds to `localhost`, making it
accessible only from the local machine. If you need to expose Jolokia to remote
hosts, you must explicitly configure the bind address and enable remote access.
+
+[source]
+----
+quarkus.camel.jolokia.server.host=0.0.0.0
+quarkus.camel.jolokia.remote-access-allowed=true
+----
+
+==== Remote access control
+
+The default Camel Jolokia restrictor blocks connections from non-loopback
addresses. This is controlled by the
`quarkus.camel.jolokia.remote-access-allowed` property (default `false`). When
set to `false`, only connections from loopback addresses (e.g. `127.0.0.1`,
`::1`) are accepted, regardless of the server bind address.
+
+==== Cross-origin requests (CORS)
+
+The default Camel Jolokia restrictor denies all cross-origin requests. If you
need to allow specific origins (e.g. for a browser-based monitoring tool
connecting cross-origin), you can use a
https://jolokia.org/reference/html/manual/security.html[Jolokia access policy]
(`jolokia-access.xml`) with a `<cors>` section, or configure a custom
restrictor that overrides `isOriginAllowed()`.
+
+==== Jolokia access policy
+
+Jolokia supports fine-grained access control via an XML policy file
(`jolokia-access.xml`). This can restrict access by IP address, CORS origin,
HTTP method, and MBean operation.
+
+The default Camel restrictor automatically loads `jolokia-access.xml` from the
classpath if present. Place the file in `src/main/resources/jolokia-access.xml`
to use it alongside the Camel restrictor. The Camel restrictor always allows
loopback connections. For non-loopback requests, it delegates remote access,
CORS, allowed request types (`<commands>`), and HTTP method (`<http>`) checks
to the policy file, while continuing to enforce MBean domain filtering.
+
+For example, the following `jolokia-access.xml` allows access from the
`10.0.0.0/8` subnet, CORS requests from a monitoring tool, and restricts
Jolokia to read-only operations over HTTP GET.
+
+[source,xml]
+----
+<?xml version="1.0" encoding="UTF-8"?>
+<restrict>
+ <remote>
+ <host>10.0.0.0/8</host>
+ </remote>
+ <cors>
+ <allow-origin>http://monitoring.example.com</allow-origin>
+ </cors>
+ <commands>
+ <command>read</command>
+ <command>list</command>
+ <command>version</command>
+ <command>search</command>
+ </commands>
+ <http>
+ <method>get</method>
+ </http>
+</restrict>
+----
+
+Refer to the https://jolokia.org/reference/html/manual/security.html[Jolokia
security documentation] for the full policy file format.
+
+To load the policy file from a different location, such as a file path mounted
from a Kubernetes ConfigMap, configure the `policyLocation` property.
+
+[source]
+----
+quarkus.camel.jolokia.additional-properties."policyLocation"=file:/etc/jolokia/jolokia-access.xml
+----
+
+As an alternative, you can disable the default Camel restrictor entirely and
let Jolokia manage the policy file directly. This gives full control to
`jolokia-access.xml` (including MBean-level rules) but loses the Camel MBean
domain filtering.
+
+[source]
+----
+quarkus.camel.jolokia.register-camel-restrictor=false
+----
+
+If you are building a native executable, the policy file must be explicitly
included as a native resource.
+
+[source]
+----
+quarkus.native.resources.includes=jolokia-access.xml
+----
diff --git
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
index d70297a2f2..f19619523c 100644
---
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
+++
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/CamelQuarkusJolokiaLogHandler.java
@@ -34,7 +34,7 @@ final class CamelQuarkusJolokiaLogHandler implements
LogHandler {
@Override
public void error(String s, Throwable throwable) {
- LOG.error(s);
+ LOG.error(s, throwable);
}
@Override
diff --git
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/JolokiaRecorder.java
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/JolokiaRecorder.java
index 56e08e980c..e697b108e6 100644
---
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/JolokiaRecorder.java
+++
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/JolokiaRecorder.java
@@ -31,6 +31,7 @@ import
org.apache.camel.quarkus.jolokia.config.JolokiaRuntimeConfig.DiscoveryEna
import org.apache.camel.quarkus.jolokia.config.JolokiaRuntimeConfig.Kubernetes;
import org.apache.camel.quarkus.jolokia.config.JolokiaRuntimeConfig.Server;
import org.apache.camel.quarkus.jolokia.restrictor.CamelJolokiaRestrictor;
+import org.apache.camel.quarkus.jolokia.util.JolokiaHostUtils;
import org.apache.camel.util.CollectionHelper;
import org.apache.camel.util.ObjectHelper;
import org.eclipse.microprofile.config.ConfigProvider;
@@ -73,7 +74,7 @@ public class JolokiaRecorder {
host = ALL_INTERFACES;
}
} else {
- host = ALL_INTERFACES;
+ host = LOCALHOST;
}
}
@@ -92,10 +93,27 @@ public class JolokiaRecorder {
serverOptions.put("useSslClientAuthentication", "true");
serverOptions.put("extendedClientCheck", "true");
serverOptions.put("caCert",
kubernetes.serviceCaCert().getAbsolutePath());
- kubernetes.clientPrincipal()
- .ifPresent(clientPrincipal ->
serverOptions.put("clientPrincipal", clientPrincipal));
- } else {
- LOG.warnf("Kubernetes service CA certificate %s does not
exist", kubernetes.serviceCaCert());
+ if (kubernetes.clientPrincipal().isPresent()) {
+ serverOptions.put("clientPrincipal",
kubernetes.clientPrincipal().get());
+ } else {
+ LOG.warn("Kubernetes SSL client authentication is enabled
but no client principal is configured"
+ + "
('quarkus.camel.jolokia.kubernetes.client-principal'). Any pod presenting a
valid"
+ + " certificate signed by the service CA can
access Jolokia. Set a client principal"
+ + " to restrict access to a specific service
identity.");
+ }
+ } else if (kubernetes.clientAuthenticationEnabled()) {
+ if (!isLoopbackHost(host)) {
+ throw new RuntimeException(
+ String.format("Kubernetes SSL client
authentication is enabled but the service CA certificate"
+ + " '%s' does not exist. The Jolokia
server is configured to bind to '%s'"
+ + " which would expose it without
authentication. Either provide the CA certificate,"
+ + " set
'quarkus.camel.jolokia.kubernetes.client-authentication-enabled=false',"
+ + " or bind to localhost with
'quarkus.camel.jolokia.server.host=localhost'.",
+ kubernetes.serviceCaCert(), host));
+ }
+ LOG.warnf("Kubernetes SSL client authentication is enabled but
the service CA certificate"
+ + " '%s' does not exist. Proceeding without TLS since
the Jolokia server is bound to"
+ + " the loopback interface.",
kubernetes.serviceCaCert());
}
}
@@ -152,6 +170,10 @@ public class JolokiaRecorder {
return new RuntimeValue<>(new
CamelQuarkusJolokiaServer(jolokiaServer.getValue()));
}
+ static boolean isLoopbackHost(String host) {
+ return JolokiaHostUtils.isLoopbackAddress(host);
+ }
+
static final class CamelQuarkusJolokiaAgent extends JolokiaServer {
CamelQuarkusJolokiaAgent(JolokiaServerConfig config) throws
IOException {
super(config);
diff --git
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/config/JolokiaRuntimeConfig.java
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/config/JolokiaRuntimeConfig.java
index 44097ac703..8561d9d4f4 100644
---
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/config/JolokiaRuntimeConfig.java
+++
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/config/JolokiaRuntimeConfig.java
@@ -58,6 +58,18 @@ public interface JolokiaRuntimeConfig {
@WithDefault("true")
boolean registerCamelRestrictor();
+ /**
+ * When `true`, the default Camel Jolokia restrictor allows connections
from non-loopback (remote) addresses.
+ * When `false` (the default), only connections from loopback addresses
(e.g. 127.0.0.1, ::1) are permitted.
+ * This provides defense-in-depth: even if the server host is configured
to bind to all interfaces,
+ * remote requests are rejected unless this property is explicitly set to
`true`.
+ *
+ * This option only takes effect when `register-camel-restrictor` is
`true` and a custom restrictor class
+ * is not configured via
`quarkus.camel.jolokia.additional-properties."restrictorClass"`.
+ */
+ @WithDefault("false")
+ boolean remoteAccessAllowed();
+
interface Server {
/**
* Whether the Jolokia agent HTTP server should be started
automatically.
@@ -69,8 +81,8 @@ public interface JolokiaRuntimeConfig {
/**
* The host address to which the Jolokia agent HTTP server should bind.
- * When unspecified, the default is localhost for dev and test mode.
- * In prod mode the default is to bind to all interfaces at 0.0.0.0.
+ * When unspecified, the default is localhost in all modes except
remote dev,
+ * where it defaults to 0.0.0.0.
*/
Optional<String> host();
diff --git
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/restrictor/CamelJolokiaRestrictor.java
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/restrictor/CamelJolokiaRestrictor.java
index 5f902163fe..aa2f8dec81 100644
---
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/restrictor/CamelJolokiaRestrictor.java
+++
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/restrictor/CamelJolokiaRestrictor.java
@@ -16,6 +16,8 @@
*/
package org.apache.camel.quarkus.jolokia.restrictor;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Set;
@@ -23,29 +25,109 @@ import javax.management.ObjectName;
import io.smallrye.config.SmallRyeConfig;
import org.apache.camel.quarkus.jolokia.config.JolokiaBuildTimeConfig;
+import org.apache.camel.quarkus.jolokia.util.JolokiaHostUtils;
import org.eclipse.microprofile.config.ConfigProvider;
-import org.jolokia.server.core.restrictor.AllowAllRestrictor;
+import org.jboss.logging.Logger;
+import org.jolokia.server.core.restrictor.RestrictorFactory;
+import org.jolokia.server.core.restrictor.policy.PolicyRestrictor;
+import org.jolokia.server.core.service.api.Restrictor;
+import org.jolokia.server.core.util.HttpMethod;
+import org.jolokia.server.core.util.RequestType;
+
+public class CamelJolokiaRestrictor implements Restrictor {
+ private static final String DEFAULT_POLICY_LOCATION =
"classpath:/jolokia-access.xml";
+ private static final Logger LOG =
Logger.getLogger(CamelJolokiaRestrictor.class);
-public final class CamelJolokiaRestrictor extends AllowAllRestrictor {
private final Set<String> ALLOWED_DOMAINS = Collections.unmodifiableSet(
ConfigProvider.getConfig()
.unwrap(SmallRyeConfig.class)
.getConfigMapping(JolokiaBuildTimeConfig.class)
.camelRestrictorAllowedMbeanDomains());
+ private final boolean remoteAccessAllowed = ConfigProvider.getConfig()
+ .getOptionalValue("quarkus.camel.jolokia.remote-access-allowed",
Boolean.class)
+ .orElse(false);
+
+ private final PolicyRestrictor policyRestrictor;
+
+ public CamelJolokiaRestrictor() {
+ this.policyRestrictor = loadPolicyRestrictor();
+ }
+
+ @Override
+ public boolean isRemoteAccessAllowed(String... hostOrAddress) {
+ if (remoteAccessAllowed) {
+ return true;
+ }
+ if (hostOrAddress != null) {
+ for (String addr : hostOrAddress) {
+ if (isLoopbackAddress(addr)) {
+ return true;
+ }
+ }
+ }
+ if (policyRestrictor != null) {
+ return policyRestrictor.isRemoteAccessAllowed(hostOrAddress);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isHttpMethodAllowed(HttpMethod method) {
+ if (policyRestrictor != null) {
+ return policyRestrictor.isHttpMethodAllowed(method);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isTypeAllowed(RequestType type) {
+ if (policyRestrictor != null) {
+ return policyRestrictor.isTypeAllowed(type);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isOriginAllowed(String origin, boolean strictCheck) {
+ if (origin == null || remoteAccessAllowed) {
+ return true;
+ }
+ try {
+ String host = new URI(origin).getHost();
+ if (isLoopbackAddress(host)) {
+ return true;
+ }
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ if (policyRestrictor != null) {
+ return policyRestrictor.isOriginAllowed(origin, strictCheck);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean ignoreScheme() {
+ if (policyRestrictor != null) {
+ return policyRestrictor.ignoreScheme();
+ }
+ return false;
+ }
+
@Override
public boolean isAttributeReadAllowed(ObjectName objectName, String
attribute) {
- return isAllowedDomain(objectName);
+ return isAllowedDomain(objectName) && isTypeAllowed(RequestType.READ);
}
@Override
public boolean isAttributeWriteAllowed(ObjectName objectName, String
attribute) {
- return isAllowedDomain(objectName);
+ return isAllowedDomain(objectName) && isTypeAllowed(RequestType.WRITE);
}
@Override
public boolean isOperationAllowed(ObjectName objectName, String operation)
{
- return isAllowedDomain(objectName);
+ return isAllowedDomain(objectName) && isTypeAllowed(RequestType.EXEC);
}
@Override
@@ -56,4 +138,20 @@ public final class CamelJolokiaRestrictor extends
AllowAllRestrictor {
private boolean isAllowedDomain(ObjectName objectName) {
return ALLOWED_DOMAINS.contains(objectName.getDomain());
}
+
+ private static boolean isLoopbackAddress(String hostOrAddress) {
+ return JolokiaHostUtils.isLoopbackAddress(hostOrAddress);
+ }
+
+ private static PolicyRestrictor loadPolicyRestrictor() {
+ String location = ConfigProvider.getConfig()
+
.getOptionalValue("quarkus.camel.jolokia.additional-properties.policyLocation",
String.class)
+ .orElse(DEFAULT_POLICY_LOCATION);
+ try {
+ return RestrictorFactory.lookupPolicyRestrictor(location);
+ } catch (Exception e) {
+ LOG.warnf("Failed to load Jolokia policy file from %s, policy
delegation disabled", location, e);
+ return null;
+ }
+ }
}
diff --git
a/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/util/JolokiaHostUtils.java
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/util/JolokiaHostUtils.java
new file mode 100644
index 0000000000..f452d4a8d6
--- /dev/null
+++
b/extensions/jolokia/runtime/src/main/java/org/apache/camel/quarkus/jolokia/util/JolokiaHostUtils.java
@@ -0,0 +1,66 @@
+/*
+ * 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.camel.quarkus.jolokia.util;
+
+public final class JolokiaHostUtils {
+
+ private JolokiaHostUtils() {
+ // Utility class
+ }
+
+ public static boolean isLoopbackAddress(String hostOrAddress) {
+ if (hostOrAddress == null || hostOrAddress.isEmpty()) {
+ return false;
+ }
+
+ String host = hostOrAddress;
+ if (host.startsWith("[") && host.endsWith("]")) {
+ host = host.substring(1, host.length() - 1);
+ }
+
+ if ("localhost".equalsIgnoreCase(host) ||
"localhost.localdomain".equalsIgnoreCase(host)) {
+ return true;
+ }
+
+ if (host.startsWith("127.")) {
+ return isIPv4Loopback(host);
+ }
+
+ return "::1".equals(host) || "0:0:0:0:0:0:0:1".equals(host);
+ }
+
+ private static boolean isIPv4Loopback(String ip) {
+ String[] parts = ip.split("\\.");
+ if (parts.length != 4) {
+ return false;
+ }
+ try {
+ if (Integer.parseInt(parts[0]) != 127) {
+ return false;
+ }
+ for (int i = 1; i < 4; i++) {
+ int octet = Integer.parseInt(parts[i]);
+ if (octet < 0 || octet > 255) {
+ return false;
+ }
+ }
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+}
diff --git
a/integration-tests/jolokia/src/main/java/org/apache/camel/quarkus/component/jolokia/it/CustomRestrictor.java
b/integration-tests/jolokia/src/main/java/org/apache/camel/quarkus/component/jolokia/it/CustomRestrictor.java
index 65fa5bf047..d779bcbe5b 100644
---
a/integration-tests/jolokia/src/main/java/org/apache/camel/quarkus/component/jolokia/it/CustomRestrictor.java
+++
b/integration-tests/jolokia/src/main/java/org/apache/camel/quarkus/component/jolokia/it/CustomRestrictor.java
@@ -18,12 +18,12 @@ package org.apache.camel.quarkus.component.jolokia.it;
import javax.management.ObjectName;
-import org.jolokia.server.core.restrictor.AllowAllRestrictor;
+import org.apache.camel.quarkus.jolokia.restrictor.CamelJolokiaRestrictor;
/**
* Only allows MBean operation sendStringBody.
*/
-public class CustomRestrictor extends AllowAllRestrictor {
+public class CustomRestrictor extends CamelJolokiaRestrictor {
@Override
public boolean isOperationAllowed(ObjectName pName, String pOperation) {
if (pOperation.startsWith("sendStringBody")) {
diff --git
a/integration-tests/jolokia/src/main/java/org/apache/camel/quarkus/component/jolokia/it/CustomRestrictor.java
b/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaRemoteAccessAllowedIT.java
similarity index 67%
copy from
integration-tests/jolokia/src/main/java/org/apache/camel/quarkus/component/jolokia/it/CustomRestrictor.java
copy to
integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaRemoteAccessAllowedIT.java
index 65fa5bf047..d25b0b010e 100644
---
a/integration-tests/jolokia/src/main/java/org/apache/camel/quarkus/component/jolokia/it/CustomRestrictor.java
+++
b/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaRemoteAccessAllowedIT.java
@@ -16,19 +16,8 @@
*/
package org.apache.camel.quarkus.component.jolokia.it;
-import javax.management.ObjectName;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
-import org.jolokia.server.core.restrictor.AllowAllRestrictor;
-
-/**
- * Only allows MBean operation sendStringBody.
- */
-public class CustomRestrictor extends AllowAllRestrictor {
- @Override
- public boolean isOperationAllowed(ObjectName pName, String pOperation) {
- if (pOperation.startsWith("sendStringBody")) {
- return true;
- }
- return false;
- }
+@QuarkusIntegrationTest
+class JolokiaRemoteAccessAllowedIT extends JolokiaRemoteAccessAllowedTest {
}
diff --git
a/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaRemoteAccessAllowedTest.java
b/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaRemoteAccessAllowedTest.java
new file mode 100644
index 0000000000..823bb748b7
--- /dev/null
+++
b/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaRemoteAccessAllowedTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.camel.quarkus.component.jolokia.it;
+
+import java.util.Map;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.TestProfile;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.equalTo;
+
+@TestProfile(JolokiaRemoteAccessAllowedTest.RemoteAccessAllowedProfile.class)
+@QuarkusTest
+class JolokiaRemoteAccessAllowedTest {
+ @BeforeEach
+ public void beforeEach() {
+ RestAssured.port = 8778;
+ }
+
+ @Test
+ void jolokiaAccessibleWithRemoteAccessAllowed() {
+ RestAssured.given()
+ .get("/jolokia/")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo(200));
+ }
+
+ @Test
+ void mbeanAccessStillRestrictedByDomain() {
+ RestAssured.given()
+
.get("/jolokia/read/java.util.logging:type=Logging/LoggerNames")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo(403));
+ }
+
+ public static final class RemoteAccessAllowedProfile implements
QuarkusTestProfile {
+ @Override
+ public Map<String, String> getConfigOverrides() {
+ return Map.of("quarkus.camel.jolokia.remote-access-allowed",
"true");
+ }
+ }
+}
diff --git
a/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaTest.java
b/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaTest.java
index 430d05c22a..c00032b8bf 100644
---
a/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaTest.java
+++
b/integration-tests/jolokia/src/test/java/org/apache/camel/quarkus/component/jolokia/it/JolokiaTest.java
@@ -112,4 +112,28 @@ class JolokiaTest {
.body(
"status", equalTo(403));
}
+
+ @Test
+ void disallowedDomainExecDenied() {
+ String payload =
"{\"type\":\"exec\",\"mbean\":\"java.util.logging:type=Logging\",\"operation\":\"getLoggerLevel\",\"arguments\":[\"\"]}";
+ RestAssured.given()
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .post("/jolokia/")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo(403));
+ }
+
+ @Test
+ void disallowedDomainWriteDenied() {
+ String payload =
"{\"type\":\"write\",\"mbean\":\"java.util.logging:type=Logging\",\"attribute\":\"LoggerNames\",\"value\":\"test\"}";
+ RestAssured.given()
+ .contentType(ContentType.JSON)
+ .body(payload)
+ .post("/jolokia/")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo(403));
+ }
}