This is an automated email from the ASF dual-hosted git repository. ramackri pushed a commit to branch backport-RANGER-5689-ranger-2.9 in repository https://gitbox.apache.org/repos/asf/ranger.git
commit e28ad8906784acd3d2142cf8377eedde3149ee97 Author: ramk <[email protected]> AuthorDate: Wed Jul 15 18:08:28 2026 +0530 RANGER-5689: Backport uniform TLS hostname verification to ranger-2.9 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). --- .../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));
