This is an automated email from the ASF dual-hosted git repository.
ramackri pushed a commit to branch ranger-2.9
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/ranger-2.9 by this push:
new 72fdb0cd9 RANGER-5689: Backport uniform TLS hostname verification to
ranger-2.9 (#1078)
72fdb0cd9 is described below
commit 72fdb0cd957eabbdffaa6a54f0cba63fe3b9322a
Author: Ramachandran Krishnan <[email protected]>
AuthorDate: Wed Jul 15 18:52:28 2026 +0530
RANGER-5689: Backport uniform TLS hostname verification to ranger-2.9
(#1078)
Backport of c631e8314 (#1076): introduce RangerDefaultHostnameVerifier
and replace ad-hoc session.getPeerHost() verifiers in RangerRESTClient,
RangerSslHelper, RangerAdminJersey2RESTClient, and RangerUgSyncRESTClient.
Adapt unit test and keystore script for JDK 8 (JKS storetype, keytool SAN
limits, and Java 8-compatible stream handling).
Co-authored-by: ramk <[email protected]>
---
.../plugin/util/RangerDefaultHostnameVerifier.java | 45 ++++++
.../ranger/plugin/util/RangerRESTClient.java | 7 +-
.../apache/ranger/plugin/util/RangerSslHelper.java | 9 +-
.../util/TestRangerDefaultHostnameVerifier.java | 178 +++++++++++++++++++++
.../src/test/resources/generate-test-keystores.sh | 52 ++++++
.../admin/client/RangerAdminJersey2RESTClient.java | 7 +-
.../process/RangerUgSyncRESTClient.java | 8 +-
7 files changed, 280 insertions(+), 26 deletions(-)
diff --git
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerDefaultHostnameVerifier.java
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerDefaultHostnameVerifier.java
new file mode 100644
index 000000000..e8c74b360
--- /dev/null
+++
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerDefaultHostnameVerifier.java
@@ -0,0 +1,45 @@
+/*
+ * 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.ranger.plugin.util;
+
+import org.apache.http.conn.ssl.DefaultHostnameVerifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+public class RangerDefaultHostnameVerifier implements HostnameVerifier {
+ private static final Logger LOG =
LoggerFactory.getLogger(RangerDefaultHostnameVerifier.class);
+ // Thread-safe, stateless, and standard Apache implementation
+ private static final HostnameVerifier DELEGATE = new
DefaultHostnameVerifier();
+
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ if (hostname == null || session == null) {
+ return false;
+ }
+ boolean verified = DELEGATE.verify(hostname, session);
+ if (!verified && LOG.isWarnEnabled()) {
+ LOG.warn("Hostname verification failed for [{}]: certificate
identity does not match requested host", hostname);
+ }
+ return verified;
+ }
+}
diff --git
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java
index e421ec9a5..777f3bc75 100644
---
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java
+++
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java
@@ -42,7 +42,6 @@
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.ws.rs.core.Cookie;
@@ -262,11 +261,7 @@ private Client buildClient() {
config.getClasses().add(JacksonJsonProvider.class); //
to handle List<> unmarshalling
- HostnameVerifier hv = new HostnameVerifier() {
- public boolean verify(String urlHostName,
SSLSession session) {
- return
session.getPeerHost().equals(urlHostName);
- }
- };
+ HostnameVerifier hv = new
RangerDefaultHostnameVerifier();
config.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES, new
HTTPSProperties(hv, sslContext));
diff --git
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerSslHelper.java
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerSslHelper.java
index 8d803e32f..0dc3d9e57 100644
---
a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerSslHelper.java
+++
b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerSslHelper.java
@@ -35,7 +35,6 @@
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
@@ -73,13 +72,7 @@ public class RangerSslHelper {
private String mTrustStoreFile;
private String mTrustStoreType;
- final static HostnameVerifier _Hv = new HostnameVerifier() {
-
- @Override
- public boolean verify(String urlHostName, SSLSession session) {
- return session.getPeerHost().equals(urlHostName);
- }
- };
+ final static HostnameVerifier _Hv = new RangerDefaultHostnameVerifier();
final String mSslConfigFileName;
diff --git
a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerDefaultHostnameVerifier.java
b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerDefaultHostnameVerifier.java
new file mode 100644
index 000000000..39c725e53
--- /dev/null
+++
b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerDefaultHostnameVerifier.java
@@ -0,0 +1,178 @@
+/*
+ * 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.ranger.plugin.util;
+
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestRangerDefaultHostnameVerifier {
+ private static final RangerDefaultHostnameVerifier VERIFIER = new
RangerDefaultHostnameVerifier();
+ private static Path keystoreDir;
+ private static HttpsServer mismatchedHostServer;
+ private static HttpsServer matchedHostServer;
+ private static HttpsServer wildcardHostServer;
+
+ @BeforeAll
+ static void generateKeystoresAndStartServers() throws Exception {
+ keystoreDir =
Files.createTempDirectory("ranger-hostname-verifier-test");
+
+ java.net.URL scriptUrl = TestRangerDefaultHostnameVerifier.class
+ .getClassLoader()
+ .getResource("generate-test-keystores.sh");
+
+ if (scriptUrl == null) {
+ throw new IllegalStateException("generate-test-keystores.sh not
found on classpath");
+ }
+
+ String scriptPath = new
java.io.File(scriptUrl.toURI()).getAbsolutePath();
+
+ ProcessBuilder pb = new ProcessBuilder("bash", scriptPath,
keystoreDir.toString());
+ pb.redirectErrorStream(true);
+ Process process = pb.start();
+
+ String output = readStream(process.getInputStream());
+ int exitCode = process.waitFor();
+ if (exitCode != 0) {
+ throw new IllegalStateException("generate-test-keystores.sh failed
(exit=" + exitCode + "):\n" + output);
+ }
+
+ mismatchedHostServer = startServer("attacker-cert.jks");
+ matchedHostServer = startServer("localhost-cert.jks");
+ wildcardHostServer = startServer("wildcard-cert.jks");
+ }
+
+ @AfterAll
+ static void stopServersAndCleanup() throws Exception {
+ if (mismatchedHostServer != null) {
+ mismatchedHostServer.stop(0);
+ }
+ if (matchedHostServer != null) {
+ matchedHostServer.stop(0);
+ }
+ if (wildcardHostServer != null) {
+ wildcardHostServer.stop(0);
+ }
+ try (Stream<Path> files = Files.walk(keystoreDir)) {
+ files.sorted(Comparator.reverseOrder()).forEach(p ->
p.toFile().delete());
+ }
+ }
+
+ @Test
+ void rejectsCertificateIssuedForDifferentHostname() throws Exception {
+ assertFalse(VERIFIER.verify("localhost",
handshake(mismatchedHostServer)));
+ }
+
+ @Test
+ void acceptsCertificateMatchingHostname() throws Exception {
+ assertTrue(VERIFIER.verify("localhost", handshake(matchedHostServer)));
+ }
+
+ @Test
+ void acceptsWildcardSanForSingleLabel() throws Exception {
+ assertTrue(VERIFIER.verify("foo.example.test",
handshake(wildcardHostServer)));
+ }
+
+ @Test
+ void rejectsWildcardSanForMultiLabel() throws Exception {
+ assertFalse(VERIFIER.verify("a.b.example.test",
handshake(wildcardHostServer)));
+ }
+
+ @Test
+ void rejectsNullHostnameOrSession() {
+ assertFalse(VERIFIER.verify(null, null));
+ }
+
+ private static String readStream(InputStream in) throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int read;
+
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+
+ return out.toString("UTF-8");
+ }
+
+ private static HttpsServer startServer(String keystoreFileName) throws
Exception {
+ char[] pw = "changeit".toCharArray();
+ KeyStore ks = KeyStore.getInstance("JKS");
+ try (FileInputStream in = new
FileInputStream(keystoreDir.resolve(keystoreFileName).toFile())) {
+ ks.load(in, pw);
+ }
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+ kmf.init(ks, pw);
+
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(kmf.getKeyManagers(), null, null);
+
+ HttpsServer server = HttpsServer.create(new InetSocketAddress(0), 0);
+ server.setHttpsConfigurator(new HttpsConfigurator(ctx));
+ server.createContext("/", exchange -> {
+ exchange.sendResponseHeaders(200, 0);
+ exchange.close();
+ });
+ server.start();
+ return server;
+ }
+
+ private static SSLSession handshake(HttpsServer server) throws Exception {
+ TrustManager trustAllCerts = new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ public void checkClientTrusted(X509Certificate[] certs, String
authType) {
+ }
+
+ public void checkServerTrusted(X509Certificate[] certs, String
authType) {
+ }
+ };
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(null, new TrustManager[] {trustAllCerts}, null);
+ try (SSLSocket socket = (SSLSocket)
ctx.getSocketFactory().createSocket("localhost",
server.getAddress().getPort())) {
+ socket.startHandshake();
+ return socket.getSession();
+ }
+ }
+}
diff --git a/agents-common/src/test/resources/generate-test-keystores.sh
b/agents-common/src/test/resources/generate-test-keystores.sh
new file mode 100755
index 000000000..27ce03873
--- /dev/null
+++ b/agents-common/src/test/resources/generate-test-keystores.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+# Generates throwaway test keystores for TestRangerDefaultHostnameVerifier.
+set -euo pipefail
+
+OUT_DIR="${1:?Usage: generate-test-keystores.sh <output-dir>}"
+PASS="changeit"
+KEYTOOL="${JAVA_HOME:+${JAVA_HOME}/bin/}keytool"
+STORE_TYPE="JKS"
+
+mkdir -p "$OUT_DIR"
+cd "$OUT_DIR"
+
+rm -f ca.jks ca.pem *.jks *.csr *.pem 2>/dev/null || true
+
+# --- CA ---
+"$KEYTOOL" -genkeypair -alias fakeCA -keyalg RSA -keysize 2048 -validity 30
-dname "CN=Ranger Test CA" -ext bc:c=ca:true -keystore ca.jks -storetype
"$STORE_TYPE" -storepass "$PASS" -keypass "$PASS" -noprompt
+"$KEYTOOL" -exportcert -alias fakeCA -keystore ca.jks -storetype "$STORE_TYPE"
-storepass "$PASS" -rfc -file ca.pem
+
+# --- helper: issue a leaf cert with given CN/SAN into its own keystore ---
+issue_cert() {
+ local alias="$1" keystore="$2" dname="$3" san="$4"
+ "$KEYTOOL" -genkeypair -alias "$alias" -keyalg RSA -keysize 2048 -validity
30 -dname "$dname" -keystore "$keystore" -storetype "$STORE_TYPE" -storepass
"$PASS" -keypass "$PASS" -noprompt
+ "$KEYTOOL" -certreq -alias "$alias" -keystore "$keystore" -storetype
"$STORE_TYPE" -storepass "$PASS" -file "$alias.csr"
+ "$KEYTOOL" -gencert -alias fakeCA -keystore ca.jks -storetype "$STORE_TYPE"
-storepass "$PASS" -infile "$alias.csr" -outfile "$alias.pem" -ext "$san"
-validity 30
+ "$KEYTOOL" -importcert -alias fakeCA -keystore "$keystore" -storetype
"$STORE_TYPE" -storepass "$PASS" -file ca.pem -noprompt
+ "$KEYTOOL" -importcert -alias "$alias" -keystore "$keystore" -storetype
"$STORE_TYPE" -storepass "$PASS" -file "$alias.pem" -noprompt
+}
+
+issue_cert serverkey attacker-cert.jks "CN=attacker.internal"
"san=dns:attacker.internal"
+issue_cert serverkey localhost-cert.jks "CN=localhost"
"san=dns:localhost"
+issue_cert serverkey wildcard-cert.jks "CN=*.example.test"
"san=dns:foo.example.test"
+
+echo "Generated attacker-cert.jks, localhost-cert.jks, wildcard-cert.jks in
$OUT_DIR"
diff --git
a/knox-agent/src/main/java/org/apache/ranger/admin/client/RangerAdminJersey2RESTClient.java
b/knox-agent/src/main/java/org/apache/ranger/admin/client/RangerAdminJersey2RESTClient.java
index ea4ead5eb..a8c0de7c9 100644
---
a/knox-agent/src/main/java/org/apache/ranger/admin/client/RangerAdminJersey2RESTClient.java
+++
b/knox-agent/src/main/java/org/apache/ranger/admin/client/RangerAdminJersey2RESTClient.java
@@ -33,7 +33,6 @@
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
@@ -409,11 +408,7 @@ Client buildClient() {
_sslContext = sslHelper.createContext();
}
if (_hv == null) {
- _hv = new HostnameVerifier() {
- public boolean verify(String
urlHostName, SSLSession session) {
- return
session.getPeerHost().equals(urlHostName);
- }
- };
+ _hv = new RangerDefaultHostnameVerifier();
}
_client = ClientBuilder.newBuilder()
.sslContext(_sslContext)
diff --git
a/ugsync/src/main/java/org/apache/ranger/unixusersync/process/RangerUgSyncRESTClient.java
b/ugsync/src/main/java/org/apache/ranger/unixusersync/process/RangerUgSyncRESTClient.java
index 3daa9767c..5c5f99fc9 100644
---
a/ugsync/src/main/java/org/apache/ranger/unixusersync/process/RangerUgSyncRESTClient.java
+++
b/ugsync/src/main/java/org/apache/ranger/unixusersync/process/RangerUgSyncRESTClient.java
@@ -22,12 +22,12 @@
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.security.SecureClientLogin;
+import org.apache.ranger.plugin.util.RangerDefaultHostnameVerifier;
import org.apache.ranger.plugin.util.RangerRESTClient;
import org.apache.ranger.unixusersync.config.UserGroupSyncConfig;
@@ -61,11 +61,7 @@ public RangerUgSyncRESTClient(String policyMgrBaseUrls,
String ugKeyStoreFile, S
ClientConfig config = new DefaultClientConfig();
config.getClasses().add(JacksonJsonProvider.class); //
to handle List<> unmarshalling
- HostnameVerifier hv = new HostnameVerifier() {
- public boolean verify(String urlHostName,
SSLSession session) {
- return
session.getPeerHost().equals(urlHostName);
- }
- };
+ HostnameVerifier hv = new
RangerDefaultHostnameVerifier();
config.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES, new
HTTPSProperties(hv, sslContext));
setClient(Client.create(config));