diff --git a/rhel-168577.patch b/rhel-168577.patch
new file mode 100644
index 0000000..5d196da
--- /dev/null
+++ b/rhel-168577.patch
@@ -0,0 +1,346 @@
+diff --git a/java/org/apache/catalina/storeconfig/LocalStrings.properties b/java/org/apache/catalina/storeconfig/LocalStrings.properties
+index d3e8585df5..4c2aa53bee 100644
+--- a/java/org/apache/catalina/storeconfig/LocalStrings.properties
++++ b/java/org/apache/catalina/storeconfig/LocalStrings.properties
+@@ -28,8 +28,11 @@ factory.storeTag=store tag [{0}] ( Object: [{1}] )
+ globalNamingResourcesSF.noFactory=Cannot find NamingResources store factory
+ globalNamingResourcesSF.wrongElement=Wrong element [{0}]
+
++registry.interfacesLoaded=Loaded [{0}] interface classes for registry
+ registry.loadClassFailed=Failed to load class [{0}]
+ registry.noDescriptor=Can't find descriptor for key [{0}]
++registry.optionalClassLoaded=Loaded optional class [{0}]
++registry.optionalClassNotFound=Optional class [{0}] not found, skipping
+
+ standardContextSF.cannotWriteFile=Cannot write file at [{0}]
+ standardContextSF.canonicalPathError=Failed to obtain the canonical path of the configuration file [{0}]
+diff --git a/java/org/apache/catalina/storeconfig/StandardEngineSF.java b/java/org/apache/catalina/storeconfig/StandardEngineSF.java
+index 0f600ee5c1..b6bd5d7168 100644
+--- a/java/org/apache/catalina/storeconfig/StandardEngineSF.java
++++ b/java/org/apache/catalina/storeconfig/StandardEngineSF.java
+@@ -26,13 +26,23 @@ import org.apache.catalina.LifecycleListener;
+ import org.apache.catalina.Realm;
+ import org.apache.catalina.Valve;
+ import org.apache.catalina.core.StandardEngine;
+-import org.apache.catalina.ha.ClusterValve;
+
+ /**
+ * Store server.xml Element Engine
+ */
+ public class StandardEngineSF extends StoreFactoryBase {
+
++ private static final Class> clusterValveClass;
++ static {
++ Class> clazz = null;
++ try {
++ clazz = Class.forName("org.apache.catalina.ha.ClusterValve");
++ } catch (ClassNotFoundException e) {
++ // Expected when clustering JARs are not present
++ }
++ clusterValveClass = clazz;
++ }
++
+ /**
+ * Store the specified Engine properties.
+ *
+@@ -64,7 +74,7 @@ public class StandardEngineSF extends StoreFactoryBase {
+ if (valves != null && valves.length > 0) {
+ List engineValves = new ArrayList<>();
+ for (Valve valve : valves) {
+- if (!(valve instanceof ClusterValve)) {
++ if (clusterValveClass == null || !clusterValveClass.isInstance(valve)) {
+ engineValves.add(valve);
+ }
+ }
+diff --git a/java/org/apache/catalina/storeconfig/StandardHostSF.java b/java/org/apache/catalina/storeconfig/StandardHostSF.java
+index 60d473982b..1e8f2d3a58 100644
+--- a/java/org/apache/catalina/storeconfig/StandardHostSF.java
++++ b/java/org/apache/catalina/storeconfig/StandardHostSF.java
+@@ -26,13 +26,23 @@ import org.apache.catalina.LifecycleListener;
+ import org.apache.catalina.Realm;
+ import org.apache.catalina.Valve;
+ import org.apache.catalina.core.StandardHost;
+-import org.apache.catalina.ha.ClusterValve;
+
+ /**
+ * Store server.xml Element Host
+ */
+ public class StandardHostSF extends StoreFactoryBase {
+
++ private static final Class> clusterValveClass;
++ static {
++ Class> clazz = null;
++ try {
++ clazz = Class.forName("org.apache.catalina.ha.ClusterValve");
++ } catch (ClassNotFoundException e) {
++ // Expected when clustering JARs are not present
++ }
++ clusterValveClass = clazz;
++ }
++
+ /**
+ * Store the specified Host properties and children (Listener,Alias,Realm,Valve,Cluster, Context)
+ *
+@@ -68,7 +78,7 @@ public class StandardHostSF extends StoreFactoryBase {
+ if (valves != null && valves.length > 0) {
+ List hostValves = new ArrayList<>();
+ for (Valve valve : valves) {
+- if (!(valve instanceof ClusterValve)) {
++ if (clusterValveClass == null || !clusterValveClass.isInstance(valve)) {
+ hostValves.add(valve);
+ }
+ }
+diff --git a/java/org/apache/catalina/storeconfig/StoreRegistry.java b/java/org/apache/catalina/storeconfig/StoreRegistry.java
+index c17dd3817e..ee803abe36 100644
+--- a/java/org/apache/catalina/storeconfig/StoreRegistry.java
++++ b/java/org/apache/catalina/storeconfig/StoreRegistry.java
+@@ -16,7 +16,9 @@
+ */
+ package org.apache.catalina.storeconfig;
+
++import java.util.ArrayList;
+ import java.util.HashMap;
++import java.util.List;
+ import java.util.Map;
+
+ import javax.naming.directory.DirContext;
+@@ -28,17 +30,6 @@ import org.apache.catalina.Realm;
+ import org.apache.catalina.Valve;
+ import org.apache.catalina.WebResourceRoot;
+ import org.apache.catalina.WebResourceSet;
+-import org.apache.catalina.ha.CatalinaCluster;
+-import org.apache.catalina.ha.ClusterDeployer;
+-import org.apache.catalina.ha.ClusterListener;
+-import org.apache.catalina.tribes.Channel;
+-import org.apache.catalina.tribes.ChannelInterceptor;
+-import org.apache.catalina.tribes.ChannelReceiver;
+-import org.apache.catalina.tribes.ChannelSender;
+-import org.apache.catalina.tribes.Member;
+-import org.apache.catalina.tribes.MembershipService;
+-import org.apache.catalina.tribes.MessageListener;
+-import org.apache.catalina.tribes.transport.DataSender;
+ import org.apache.coyote.UpgradeProtocol;
+ import org.apache.juli.logging.Log;
+ import org.apache.juli.logging.LogFactory;
+@@ -61,11 +52,74 @@ public class StoreRegistry {
+ private String version;
+
+ // Access Information
+- private static final Class>[] interfaces = { CatalinaCluster.class, ChannelSender.class, ChannelReceiver.class,
+- Channel.class, MembershipService.class, ClusterDeployer.class, Realm.class, Manager.class, DirContext.class,
+- LifecycleListener.class, Valve.class, ClusterListener.class, MessageListener.class, DataSender.class,
+- ChannelInterceptor.class, Member.class, WebResourceRoot.class, WebResourceSet.class,
+- CredentialHandler.class, UpgradeProtocol.class, CookieProcessor.class };
++ // Lazily initialized to gracefully handle optional features like clustering
++ private static volatile Class>[] interfaces = null;
++
++ /**
++ * Initialize the interfaces array with all available classes.
++ * Uses dynamic loading for optional classes (e.g., clustering) to avoid
++ * ClassNotFoundException when those JARs are not present. This approach
++ * is consistent with how Catalina.addClusterRuleSet() handles clustering.
++ */
++ private static Class>[] getInterfaces() {
++ if (interfaces == null) {
++ synchronized (StoreRegistry.class) {
++ if (interfaces == null) {
++ // Required interfaces - always present
++ List> list = new ArrayList<>();
++ list.add(Realm.class);
++ list.add(Manager.class);
++ list.add(DirContext.class);
++ list.add(LifecycleListener.class);
++ list.add(Valve.class);
++ list.add(WebResourceRoot.class);
++ list.add(WebResourceSet.class);
++ list.add(CredentialHandler.class);
++ list.add(UpgradeProtocol.class);
++ list.add(CookieProcessor.class);
++
++ // Optional clustering interfaces - load dynamically to support
++ // deployments where clustering JARs may not be present
++ tryAddClass(list, "org.apache.catalina.ha.CatalinaCluster");
++ tryAddClass(list, "org.apache.catalina.tribes.ChannelSender");
++ tryAddClass(list, "org.apache.catalina.tribes.ChannelReceiver");
++ tryAddClass(list, "org.apache.catalina.tribes.Channel");
++ tryAddClass(list, "org.apache.catalina.tribes.MembershipService");
++ tryAddClass(list, "org.apache.catalina.ha.ClusterDeployer");
++ tryAddClass(list, "org.apache.catalina.ha.ClusterListener");
++ tryAddClass(list, "org.apache.catalina.tribes.MessageListener");
++ tryAddClass(list, "org.apache.catalina.tribes.transport.DataSender");
++ tryAddClass(list, "org.apache.catalina.tribes.ChannelInterceptor");
++ tryAddClass(list, "org.apache.catalina.tribes.Member");
++
++ interfaces = list.toArray(new Class>[0]);
++
++ if (log.isDebugEnabled()) {
++ log.debug(sm.getString("registry.interfacesLoaded", Integer.valueOf(interfaces.length)));
++ }
++ }
++ }
++ }
++ return interfaces;
++ }
++
++ /**
++ * Try to load a class by name and add it to the list if successful.
++ * Logs at TRACE level if the class is not available.
++ */
++ private static void tryAddClass(List> list, String className) {
++ try {
++ Class> clazz = Class.forName(className, false, StoreRegistry.class.getClassLoader());
++ list.add(clazz);
++ if (log.isTraceEnabled()) {
++ log.trace(sm.getString("registry.optionalClassLoaded", className));
++ }
++ } catch (ClassNotFoundException | NoClassDefFoundError e) {
++ if (log.isTraceEnabled()) {
++ log.trace(sm.getString("registry.optionalClassNotFound", className));
++ }
++ }
++ }
+
+ /**
+ * @return the name
+@@ -116,9 +170,10 @@ public class StoreRegistry {
+ }
+ if (aClass != null) {
+ desc = descriptors.get(aClass.getName());
+- for (int i = 0; desc == null && i < interfaces.length; i++) {
+- if (interfaces[i].isAssignableFrom(aClass)) {
+- desc = descriptors.get(interfaces[i].getName());
++ Class>[] availableInterfaces = getInterfaces();
++ for (int i = 0; desc == null && i < availableInterfaces.length; i++) {
++ if (availableInterfaces[i].isAssignableFrom(aClass)) {
++ desc = descriptors.get(availableInterfaces[i].getName());
+ }
+ }
+ }
+diff --git a/test/org/apache/catalina/storeconfig/TestStoreRegistry.java b/test/org/apache/catalina/storeconfig/TestStoreRegistry.java
+new file mode 100644
+index 0000000000..e6869d3642
+--- /dev/null
++++ b/test/org/apache/catalina/storeconfig/TestStoreRegistry.java
+@@ -0,0 +1,104 @@
++/*
++ * 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.catalina.storeconfig;
++
++import java.lang.reflect.Method;
++
++import org.junit.Assert;
++import org.junit.Test;
++
++import org.apache.catalina.Manager;
++import org.apache.catalina.Realm;
++import org.apache.catalina.Valve;
++
++/**
++ * Test StoreRegistry behavior, particularly dynamic loading of optional classes like clustering.
++ *
++ * Verifies StoreRegistry uses the same dynamic loading pattern.
++ */
++public class TestStoreRegistry {
++
++ /**
++ * Test that clustering classes are dynamically loaded like other Tomcat components.
++ *
++ * StoreRegistry should initialize successfully whether clustering is available or not.
++ * This matches the pattern used in Catalina.addClusterRuleSet().
++ */
++ @Test
++ public void testClusteringClassesOptional() throws Exception {
++ // Verify StoreRegistry initializes successfully with dynamic class loading
++ StoreRegistry registry = new StoreRegistry();
++ Assert.assertNotNull("Registry should initialize with dynamic loading", registry);
++
++ // Trigger lazy loading of interfaces array
++ Method getInterfacesMethod = StoreRegistry.class.getDeclaredMethod("getInterfaces");
++ getInterfacesMethod.setAccessible(true);
++
++ Class>[] interfaces = (Class>[]) getInterfacesMethod.invoke(null);
++ Assert.assertNotNull("Interfaces should load dynamically", interfaces);
++
++ // Test passes if we get here without ClassNotFoundException.
++ // The actual number of interfaces loaded depends on whether clustering is available,
++ // but we should always have at least the core 10 interfaces.
++ Assert.assertTrue("Should have at least 10 core interfaces",
++ interfaces.length >= 10);
++
++ // Verify required core interfaces are always present
++ boolean hasRealm = false;
++ boolean hasManager = false;
++ boolean hasValve = false;
++
++ for (Class> iface : interfaces) {
++ if (iface.equals(Realm.class)) {
++ hasRealm = true;
++ }
++ if (iface.equals(Manager.class)) {
++ hasManager = true;
++ }
++ if (iface.equals(Valve.class)) {
++ hasValve = true;
++ }
++ }
++
++ Assert.assertTrue("Should contain Realm interface", hasRealm);
++ Assert.assertTrue("Should contain Manager interface", hasManager);
++ Assert.assertTrue("Should contain Valve interface", hasValve);
++ }
++
++ /**
++ * Test that findDescription works with interface inheritance and
++ * dynamically loaded interfaces.
++ */
++ @Test
++ public void testFindDescriptionWithDynamicInterfaces() throws Exception {
++ StoreRegistry registry = new StoreRegistry();
++
++ // Register a description for the Valve interface
++ StoreDescription valveDesc = new StoreDescription();
++ valveDesc.setId(Valve.class.getName());
++ valveDesc.setTag("Valve");
++ valveDesc.setTagClass(Valve.class.getName());
++ registry.registerDescription(valveDesc);
++
++ // AccessLogValve implements Valve interface - should find via dynamic interface matching
++ String accessLogValveClass = "org.apache.catalina.valves.AccessLogValve";
++ StoreDescription foundDesc = registry.findDescription(accessLogValveClass);
++
++ Assert.assertNotNull("Should find description via interface matching", foundDesc);
++ Assert.assertEquals("Should match Valve descriptor", "Valve", foundDesc.getTag());
++ }
++}
+diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
+index 808165f044..792dad2fa4 100644
+--- a/webapps/docs/changelog.xml
++++ b/webapps/docs/changelog.xml
+@@ -113,6 +113,13 @@
+ in RFC 9110 that hash functions used to generate strong ETags should be
+ collision resistant. (markt)
+
++
++ Update StoreRegistry to dynamically load optional clustering
++ classes rather than statically referencing them. This matches the pattern
++ used in Catalina.addClusterRuleSet() and prevents
++ NoClassDefFoundError when StoreConfigLifecycleListener
++ is configured but clustering classes are not available. (csutherl)
++
+
+ Correct a regression in the fix for 69781 that broke
+ FileStore. (markt)
diff --git a/tomcat.spec b/tomcat.spec
index 0252713..48cc1df 100644
--- a/tomcat.spec
+++ b/tomcat.spec
@@ -56,7 +56,7 @@
Name: tomcat
Epoch: 1
Version: %{major_version}.%{minor_version}.%{micro_version}
-Release: 1%{?dist}
+Release: 2%{?dist}
Summary: Apache Servlet/JSP Engine, RI for Servlet %{servletspec}/JSP %{jspspec} API
License: ASL 2.0
@@ -81,8 +81,10 @@ Patch2: %{name}-build.patch
Patch3: %{name}-%{major_version}.%{minor_version}-catalina-policy.patch
Patch4: rhbz-1857043.patch
Patch6: %{name}-%{major_version}.%{minor_version}-bnd-annotation.patch
+Patch7: rhel-168577.patch
BuildArch: noarch
+ExclusiveArch: %{java_arches} noarch
BuildRequires: ant
BuildRequires: ecj >= 1:4.10
@@ -197,6 +199,7 @@ find . -type f \( -name "*.bat" -o -name "*.class" -o -name Thumbs.db -o -name "
%patch -P3 -p0
%patch -P4 -p0
%patch -P6 -p0
+%patch -P7 -p1
# Remove webservices naming resources as it's generally unused
%{__rm} -rf java/org/apache/naming/factory/webservices
@@ -208,6 +211,8 @@ find . -type f \( -name "*.bat" -o -name "*.class" -o -name Thumbs.db -o -name "
%mvn_alias "org.apache.tomcat:tomcat-jsp-api" "org.eclipse.jetty.orbit:javax.servlet.jsp"
%mvn_package ":tomcat-servlet-api" tomcat-servlet-api
+%pom_remove_dep org.apache.tomcat:tomcat-tribes res/maven/tomcat-storeconfig.pom
+%pom_remove_dep org.apache.tomcat:tomcat-catalina-ha res/maven/tomcat-storeconfig.pom
%build
export OPT_JAR_LIST="xalan-j2-serializer"
@@ -279,6 +284,10 @@ pushd output/build
%{__cp} -a webapps/* ${RPM_BUILD_ROOT}%{appdir}
popd
+# Clustering is unsupported in RHEL
+rm -f ${RPM_BUILD_ROOT}%{libdir}/catalina-ha.jar
+rm -f ${RPM_BUILD_ROOT}%{libdir}/catalina-tribes.jar
+
%{__sed} -e "s|\@\@\@TCHOME\@\@\@|%{homedir}|g" \
-e "s|\@\@\@TCTEMP\@\@\@|%{tempdir}|g" \
-e "s|\@\@\@LIBDIR\@\@\@|%{_libdir}|g" %{SOURCE1} \
@@ -372,8 +381,6 @@ popd
%mvn_artifact res/maven/tomcat-api.pom ${RPM_BUILD_ROOT}%{libdir}/tomcat-api.jar
%mvn_file org.apache.tomcat:tomcat-catalina-ant tomcat/catalina-ant
%mvn_artifact res/maven/tomcat-catalina-ant.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-ant.jar
-%mvn_file org.apache.tomcat:tomcat-catalina-ha tomcat/catalina-ha
-%mvn_artifact res/maven/tomcat-catalina-ha.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-ha.jar
%mvn_file org.apache.tomcat:tomcat-catalina tomcat/catalina
%mvn_artifact res/maven/tomcat-catalina.pom ${RPM_BUILD_ROOT}%{libdir}/catalina.jar
%mvn_artifact res/maven/tomcat-coyote.pom ${RPM_BUILD_ROOT}%{libdir}/tomcat-coyote.jar
@@ -400,8 +407,6 @@ popd
%mvn_artifact res/maven/tomcat-ssi.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-ssi.jar
%mvn_file org.apache.tomcat:tomcat-storeconfig tomcat/catalina-storeconfig
%mvn_artifact res/maven/tomcat-storeconfig.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-storeconfig.jar
-%mvn_file org.apache.tomcat:tomcat-tribes tomcat/catalina-tribes
-%mvn_artifact res/maven/tomcat-tribes.pom ${RPM_BUILD_ROOT}%{libdir}/catalina-tribes.jar
%mvn_artifact res/maven/tomcat-util-scan.pom ${RPM_BUILD_ROOT}%{libdir}/tomcat-util-scan.jar
%mvn_artifact res/maven/tomcat-util.pom ${RPM_BUILD_ROOT}%{libdir}/tomcat-util.jar
%mvn_file org.apache.tomcat:tomcat-websocket-api tomcat/websocket-api
@@ -562,6 +567,10 @@ fi
%{appdir}/ROOT
%changelog
+* Tue Jun 4 2026 Pietro Meloni - 1:9.0.117-2
+- Resolves: RHEL-183992 Remove tomcat clustering JAR from RPM builds
+- Exclude i686 architecture from build
+
* Wed May 26 2026 Pietro Meloni - 1:9.0.117-1
- Resolves: RHEL-150714 Certificate revocation bypass due to improper OCSP response validation
- Resolves: