diff --git a/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/P2PServerOnlyTLSWithAuthDUnitTest.java b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/P2PServerOnlyTLSWithAuthDUnitTest.java new file mode 100644 index 000000000000..0424c6bd7998 --- /dev/null +++ b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/P2PServerOnlyTLSWithAuthDUnitTest.java @@ -0,0 +1,382 @@ +/* + * 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.geode.cache.ssl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Properties; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.apache.geode.cache.RegionShortcut; +import org.apache.geode.security.GemFireSecurityException; +import org.apache.geode.test.dunit.IgnoredException; +import org.apache.geode.test.dunit.rules.ClusterStartupRule; +import org.apache.geode.test.dunit.rules.MemberVM; +import org.apache.geode.test.junit.categories.SecurityTest; +import org.apache.geode.test.junit.rules.ServerOnlyTLSTestFixture; + +/** + * Distributed tests for Peer-to-Peer (P2P) Cache Topology using Server-only TLS with + * Application-Layer Authentication (Approach 3). + * + *

+ * This test demonstrates that in a P2P cache configuration (where all members are peers, no + * client/server distinction), Approach 3 works correctly: + *

+ * + *

+ * Key Difference from Client/Server: In P2P topology, all members are equal peers + * that communicate directly. Each peer presents a server certificate for TLS encryption, but + * authentication happens at the application layer using credentials validated by SecurityManager. + * + *

+ * This approach solves the public CA clientAuth EKU sunset problem for P2P topologies by: + *

    + *
  1. Eliminating the need for client certificates entirely
  2. + *
  3. Maintaining full TLS encryption for all transport
  4. + *
  5. Using existing authentication infrastructure (LDAP, database, tokens)
  6. + *
+ * + * @see ServerOnlyTLSWithAuthDUnitTest for client/server topology tests + */ +@Category({SecurityTest.class}) +public class P2PServerOnlyTLSWithAuthDUnitTest { + + private static final String REGION_NAME = "testRegion"; + + @Rule + public ClusterStartupRule cluster = new ClusterStartupRule(); + + private ServerOnlyTLSTestFixture fixture; + + @Before + public void setUp() throws Exception { + fixture = new ServerOnlyTLSTestFixture(); + + // Add ignored exceptions for SSL-related cleanup warnings + IgnoredException.addIgnoredException("javax.net.ssl.SSLException"); + IgnoredException.addIgnoredException("java.io.IOException"); + IgnoredException.addIgnoredException("Authentication failed"); + IgnoredException.addIgnoredException("Security check failed"); + } + + /** + * Test basic P2P cluster formation with server-only TLS and application-layer authentication. + * + *

+ * Verifies: + *

+ */ + @Test + public void testP2PClusterFormationWithServerOnlyTLSAndAppAuth() throws Exception { + // Create certificate stores using fixture + // All peers use the same certificate for TLS (server cert) + CertStores clusterStores = fixture.createClusterStores(); + + // Configure locator with: + // - Server-only TLS (ssl-require-authentication=false) + // - Security manager for application-layer authentication + // - Peer authentication credentials + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + + // Start locator - it will authenticate itself when joining + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Configure first server with same setup + Properties server1Props = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(server1Props); + fixture.addPeerAuthProperties(server1Props, "cluster", "cluster"); + + // Start first server - it joins via application-layer auth, not certificate auth + MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort); + + // Verify server1 successfully joined the cluster + server1.invoke(() -> { + assertThat(ClusterStartupRule.getCache()).isNotNull(); + assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers()) + .hasSize(1); // locator + }); + + // Configure second server with same setup + Properties server2Props = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(server2Props); + fixture.addPeerAuthProperties(server2Props, "cluster", "cluster"); + + // Start second server + MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort); + + // Verify server2 successfully joined and sees all peers + server2.invoke(() -> { + assertThat(ClusterStartupRule.getCache()).isNotNull(); + assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers()) + .hasSize(2); // locator + server1 + }); + + // Verify all peers see each other (peer-to-peer mesh formed) + server1.invoke(() -> { + assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers()) + .hasSize(2); // locator + server2 + }); + } + + /** + * Test P2P data replication across encrypted peer connections without certificate authentication. + * + *

+ * Verifies: + *

+ */ + @Test + public void testP2PDataReplicationOverServerOnlyTLS() throws Exception { + CertStores clusterStores = fixture.createClusterStores(); + + // Configure and start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server1 with replicated region + Properties server1Props = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(server1Props); + fixture.addPeerAuthProperties(server1Props, "cluster", "cluster"); + MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort); + + server1.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Start server2 with same replicated region + Properties server2Props = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(server2Props); + fixture.addPeerAuthProperties(server2Props, "cluster", "cluster"); + MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort); + + server2.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Put data on server1 + server1.invoke(() -> { + ClusterStartupRule.getCache() + .getRegion(REGION_NAME) + .put("key1", "value1"); + }); + + // Verify data replicated to server2 over TLS-encrypted peer connection + server2.invoke(() -> { + Object value = ClusterStartupRule.getCache() + .getRegion(REGION_NAME) + .get("key1"); + assertThat(value).isEqualTo("value1"); + }); + + // Put data on server2 + server2.invoke(() -> { + ClusterStartupRule.getCache() + .getRegion(REGION_NAME) + .put("key2", "value2"); + }); + + // Verify data replicated to server1 + server1.invoke(() -> { + Object value = ClusterStartupRule.getCache() + .getRegion(REGION_NAME) + .get("key2"); + assertThat(value).isEqualTo("value2"); + }); + } + + /** + * Test that peer with invalid credentials cannot join P2P cluster. + * + *

+ * Verifies: + *

+ */ + @Test + public void testP2PPeerRejectedWithInvalidCredentials() throws Exception { + // Add ignored exception for authentication failure messages + IgnoredException.addIgnoredException("Authentication FAILED"); + CertStores clusterStores = fixture.createClusterStores(); + + // Configure and start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Try to start server with INVALID credentials + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "wrongPassword"); // INVALID + + // Server should fail to join due to authentication failure + // Note: Root cause is SecurityException, not GemFireSecurityException + assertThatThrownBy(() -> cluster.startServerVM(1, serverProps, locatorPort)) + .hasRootCauseInstanceOf(SecurityException.class) + .hasStackTraceContaining("invalid username/password"); + } + + /** + * Test that peer without CLUSTER:MANAGE permission cannot join P2P cluster. + * + *

+ * Verifies: + *

+ */ + @Test + public void testP2PPeerRejectedWithoutClusterManagePermission() throws Exception { + CertStores clusterStores = fixture.createClusterStores(); + + // Configure and start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Try to start server with user that has valid credentials but no CLUSTER:MANAGE + // SimpleSecurityManager allows authentication when username == password + // but "data" user does NOT have CLUSTER:MANAGE permission + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "data", "data"); // Valid creds, insufficient perms + + // Server should fail to join due to authorization failure + // Note: Root cause is SecurityException, not GemFireSecurityException + assertThatThrownBy(() -> cluster.startServerVM(1, serverProps, locatorPort)) + .hasRootCauseInstanceOf(SecurityException.class) + .hasStackTraceContaining("not authorized for CLUSTER:MANAGE"); + } + + /** + * Test that peer with no credentials cannot join P2P cluster. + * + *

+ * Verifies: + *

+ */ + @Test + public void testP2PPeerRejectedWithNoCredentials() throws Exception { + CertStores clusterStores = fixture.createClusterStores(); + + // Configure and start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Try to start server WITHOUT any authentication credentials + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + // NO peer auth properties added - missing credentials + + // Server should fail to join due to missing credentials + assertThatThrownBy(() -> cluster.startServerVM(1, serverProps, locatorPort)) + .hasRootCauseInstanceOf(GemFireSecurityException.class); + } + + /** + * Test multiple peers joining with different valid credentials. + * + *

+ * Verifies that the cluster supports heterogeneous peer credentials as long as all have + * CLUSTER:MANAGE permission. This demonstrates flexibility in credential management where + * different services/teams can use different credentials. + */ + @Test + public void testMultiplePeersWithDifferentCredentials() throws Exception { + CertStores clusterStores = fixture.createClusterStores(); + + // Start locator with "cluster" credentials + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server1 with "clusterManage" credentials + // SimpleSecurityManager grants CLUSTER:MANAGE to "cluster" and "clusterManage" users + Properties server1Props = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(server1Props); + fixture.addPeerAuthProperties(server1Props, "clusterManage", "clusterManage"); + MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort); + + // Start server2 with "cluster" credentials (same as locator) + Properties server2Props = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(server2Props); + fixture.addPeerAuthProperties(server2Props, "cluster", "cluster"); + MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort); + + // Verify all peers joined successfully + server1.invoke(() -> { + assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers()) + .hasSize(2); // locator + server2 + }); + + server2.invoke(() -> { + assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers()) + .hasSize(2); // locator + server1 + }); + } +} diff --git a/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthDUnitTest.java b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthDUnitTest.java new file mode 100644 index 000000000000..04305b0a0f32 --- /dev/null +++ b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthDUnitTest.java @@ -0,0 +1,513 @@ +/* + * 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.geode.cache.ssl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionShortcut; +import org.apache.geode.cache.client.ClientCache; +import org.apache.geode.cache.client.ClientCacheFactory; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.apache.geode.examples.SimpleSecurityManager; +import org.apache.geode.test.dunit.IgnoredException; +import org.apache.geode.test.dunit.rules.ClientVM; +import org.apache.geode.test.dunit.rules.ClusterStartupRule; +import org.apache.geode.test.dunit.rules.MemberVM; +import org.apache.geode.test.junit.categories.SecurityTest; +import org.apache.geode.test.junit.rules.ServerOnlyTLSTestFixture; + +/** + * Distributed tests for Server-only TLS with Alternative Client Authentication. + * + *

+ * These tests verify that: + *

+ */ +@Category({SecurityTest.class}) +public class ServerOnlyTLSWithAuthDUnitTest { + + private static final String REGION_NAME = "testRegion"; + + @Rule + public ClusterStartupRule cluster = new ClusterStartupRule(); + + private ServerOnlyTLSTestFixture fixture; + + @Before + public void setUp() throws Exception { + fixture = new ServerOnlyTLSTestFixture(); + + // Add ignored exceptions for SSL-related cleanup warnings + IgnoredException.addIgnoredException("javax.net.ssl.SSLException"); + IgnoredException.addIgnoredException("java.io.IOException"); + } + + /** + * Test basic client connection with TLS transport encryption and username/password + * authentication. + * + *

+ * Verifies: + *

+ */ + @Test + public void testBasicConnectionWithUsernamePassword() throws Exception { + // Create certificates and stores using fixture + // Note: Use createClusterStores() for both locator and server peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Configure locator with server-only TLS (require-authentication=false) and security manager + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + + // Start locator + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Configure server with server-only TLS and security manager + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + + // Start server + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region on server + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Configure client with server-only TLS (truststore only, no keystore) + Properties clientSSLProps = clientStores.propertiesWith("all", false, false); + + // Add authentication properties (username/password) + // SimpleSecurityManager accepts username when username == password + Properties clientAuthProps = fixture.createClientAuthProperties("data", "data"); + clientSSLProps.putAll(clientAuthProps); + + // Connect client + ClientVM client = cluster.startClientVM(2, c -> c + .withProperties(clientSSLProps) + .withLocatorConnection(locatorPort)); + + // Verify client can perform operations + client.invoke(() -> { + ClientCache clientCache = ClusterStartupRule.getClientCache(); + assertThat(clientCache).isNotNull(); + + Region region = clientCache + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + + // Perform basic operations + region.put("key1", "value1"); + Object value = region.get("key1"); + assertThat(value).isEqualTo("value1"); + }); + } + + /** + * Test multiple clients connecting with different credentials. + * + *

+ * Verifies: + *

+ */ + @Test + public void testMultipleClientsWithDifferentCredentials() throws Exception { + // Create certificates and stores + CertStores locatorStores = fixture.createLocatorStores(); + CertStores clusterStores = fixture.createClusterStores(); + CertStores client1Stores = fixture.createClientStores(); + CertStores client2Stores = fixture.createClientStores(); + CertStores client3Stores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = locatorStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Connect client 1 with user "dataRead" + Properties client1Props = client1Stores.propertiesWith("all", false, false); + client1Props.putAll(fixture.createClientAuthProperties("data", "data")); + ClientVM client1 = cluster.startClientVM(2, c -> c + .withProperties(client1Props) + .withLocatorConnection(locatorPort)); + + // Connect client 2 with user "dataWrite" + Properties client2Props = client2Stores.propertiesWith("all", false, false); + client2Props.putAll(fixture.createClientAuthProperties("dataWrite", "dataWrite")); + ClientVM client2 = cluster.startClientVM(3, c -> c + .withProperties(client2Props) + .withLocatorConnection(locatorPort)); + + // Connect client 3 with user "dataManage" + Properties client3Props = client3Stores.propertiesWith("all", false, false); + client3Props.putAll(fixture.createClientAuthProperties("dataManage", "dataManage")); + ClientVM client3 = cluster.startClientVM(4, c -> c + .withProperties(client3Props) + .withLocatorConnection(locatorPort)); + + // Verify all clients can access region + for (ClientVM client : new ClientVM[] {client1, client2, client3}) { + client.invoke(() -> { + Region region = ClusterStartupRule.getClientCache() + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + assertThat(region).isNotNull(); + }); + } + } + + /** + * Test token-based authentication instead of username/password. + * + *

+ * Verifies: + *

+ */ + @Test + public void testTokenBasedAuthentication() throws Exception { + // Create certificates and stores + // Note: Locator and server must use same CertStores for peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Configure client with token authentication + Properties clientProps = clientStores.propertiesWith("all", false, false); + Properties tokenAuthProps = fixture.createClientTokenAuthProperties( + SimpleSecurityManager.VALID_TOKEN); + clientProps.putAll(tokenAuthProps); + + // Connect client + ClientVM client = cluster.startClientVM(2, c -> c + .withProperties(clientProps) + .withLocatorConnection(locatorPort)); + + // Verify client can perform operations + client.invoke(() -> { + Region region = ClusterStartupRule.getClientCache() + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + + region.put("tokenKey", "tokenValue"); + assertThat(region.get("tokenKey")).isEqualTo("tokenValue"); + }); + } + + /** + * Test server restart with client reconnection. + * + *

+ * Verifies: + *

    + *
  • Client can reconnect after server restart
  • + *
  • TLS session is re-established
  • + *
  • Client re-authenticates successfully
  • + *
+ */ + @Test + public void testServerRestartWithClientReconnection() throws Exception { + // Create certificates and stores + // Note: Locator and server must use same CertStores for peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Connect client + Properties clientProps = clientStores.propertiesWith("all", false, false); + clientProps.putAll(fixture.createClientAuthProperties("data", "data")); + ClientVM client = cluster.startClientVM(2, c -> c + .withProperties(clientProps) + .withLocatorConnection(locatorPort)); + + // Verify initial connection + client.invoke(() -> { + Region region = ClusterStartupRule.getClientCache() + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + region.put("beforeRestart", "value1"); + }); + + // Stop and restart server + cluster.stop(1); + server = cluster.startServerVM(1, serverProps, locatorPort); + + // Recreate region on restarted server + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Verify client can reconnect and operate + client.invoke(() -> { + Region region = ClusterStartupRule.getClientCache().getRegion(REGION_NAME); + region.put("afterRestart", "value2"); + assertThat(region.get("afterRestart")).isEqualTo("value2"); + }); + } + + /** + * Test concurrent client operations over TLS. + * + *

+ * Verifies: + *

    + *
  • Multiple threads can perform concurrent operations over TLS
  • + *
  • TLS connection handles concurrent load
  • + *
  • Authentication works with concurrent operations
  • + *
+ */ + @Test + public void testConcurrentClientConnections() throws Exception { + // Create certificates and stores + // Note: Use createClusterStores() for both locator and server peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Create single client cache with TLS + Properties clientProps = clientStores.propertiesWith("all", false, false); + clientProps.putAll(fixture.createClientAuthProperties("data", "data")); + + ClientCacheFactory factory = new ClientCacheFactory(clientProps) + .addPoolLocator("localhost", locatorPort) + .setPoolSubscriptionEnabled(true); + + try (ClientCache clientCache = factory.create()) { + Region region = clientCache + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + + // Perform concurrent operations from multiple threads + int numThreads = 10; + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch completionLatch = new CountDownLatch(numThreads); + List> futures = new ArrayList<>(); + + for (int i = 0; i < numThreads; i++) { + final int operationId = i; + Future future = executor.submit(() -> { + try { + // Wait for all threads to be ready + startLatch.await(); + + // Perform operation + region.put("key" + operationId, "value" + operationId); + assertThat(region.get("key" + operationId)).isEqualTo("value" + operationId); + + completionLatch.countDown(); + } catch (Exception e) { + throw new RuntimeException("Operation " + operationId + " failed", e); + } + }); + futures.add(future); + } + + // Start all operations simultaneously + startLatch.countDown(); + + // Wait for all operations to complete + boolean completed = completionLatch.await(2, TimeUnit.MINUTES); + + // Check for exceptions first to see what actually failed + for (Future future : futures) { + future.get(); + } + + assertThat(completed).isTrue(); + + executor.shutdown(); + } + } + + /** + * Test region operations with authorization checks. + * + *

+ * Verifies: + *

    + *
  • SecurityManager enforces authorization based on principal
  • + *
  • Different users have different permissions
  • + *
  • Authorization works with TLS transport encryption
  • + *
+ */ + @Test + public void testRegionOperationsWithAuthorization() throws Exception { + // Create certificates and stores + // Note: Locator and server must use same CertStores for peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Connect client with "data" credentials + // SimpleSecurityManager authorizes based on username prefix matching permission + Properties clientProps = clientStores.propertiesWith("all", false, false); + clientProps.putAll(fixture.createClientAuthProperties("data", "data")); + ClientVM client = cluster.startClientVM(2, c -> c + .withProperties(clientProps) + .withLocatorConnection(locatorPort)); + + // Verify authorized operations succeed + client.invoke(() -> { + Region region = ClusterStartupRule.getClientCache() + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + + // User "data" should be authorized for DATA:READ and DATA:WRITE + region.put("authKey", "authValue"); + assertThat(region.get("authKey")).isEqualTo("authValue"); + }); + } +} diff --git a/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthNegativeTest.java b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthNegativeTest.java new file mode 100644 index 000000000000..6c762d214d53 --- /dev/null +++ b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthNegativeTest.java @@ -0,0 +1,481 @@ +/* + * 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.geode.cache.ssl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Properties; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionShortcut; +import org.apache.geode.cache.client.ClientCache; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.apache.geode.cache.client.ServerOperationException; +import org.apache.geode.security.AuthenticationFailedException; +import org.apache.geode.security.AuthenticationRequiredException; +import org.apache.geode.security.NotAuthorizedException; +import org.apache.geode.test.dunit.IgnoredException; +import org.apache.geode.test.dunit.rules.ClientVM; +import org.apache.geode.test.dunit.rules.ClusterStartupRule; +import org.apache.geode.test.dunit.rules.MemberVM; +import org.apache.geode.test.junit.categories.SecurityTest; +import org.apache.geode.test.junit.rules.ServerOnlyTLSTestFixture; + +/** + * Negative tests for Server-only TLS with Alternative Client Authentication. + * + *

+ * These tests verify that security violations are properly detected and rejected: + *

    + *
  • Invalid credentials are rejected
  • + *
  • Missing credentials are rejected
  • + *
  • Invalid tokens are rejected
  • + *
  • Unauthorized operations are blocked
  • + *
  • Missing or invalid server certificates are detected
  • + *
+ */ +@Category({SecurityTest.class}) +public class ServerOnlyTLSWithAuthNegativeTest { + + private static final String REGION_NAME = "testRegion"; + + @Rule + public ClusterStartupRule cluster = new ClusterStartupRule(); + + private ServerOnlyTLSTestFixture fixture; + + @Before + public void setUp() throws Exception { + fixture = new ServerOnlyTLSTestFixture(); + } + + @org.junit.After + public void tearDown() throws Exception { + // Remove ignored exceptions + IgnoredException.removeAllExpectedExceptions(); + // Give VMs time to fully shut down + Thread.sleep(500); + } + + /** + * Test that clients with invalid credentials are rejected. + * + *

+ * Verifies: + *

    + *
  • Wrong password causes authentication failure
  • + *
  • TLS connection is established but authentication fails
  • + *
+ */ + @Test + public void testClientWithInvalidCredentialsRejected() throws Exception { + IgnoredException.addIgnoredException(AuthenticationFailedException.class.getName()); + IgnoredException.addIgnoredException("Authentication FAILED"); + IgnoredException.addIgnoredException("ServerOperationException"); + + // Create certificates and stores + // Note: Locator and server must use same CertStores for peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Configure client with WRONG password + Properties clientProps = clientStores.propertiesWith("all", false, false); + clientProps.putAll(fixture.createClientAuthProperties("testUser", "wrongPassword")); + + // Client connection succeeds (TLS is established), but authentication fails on first operation + ClientVM client = cluster.startClientVM(2, c -> c + .withProperties(clientProps) + .withLocatorConnection(locatorPort)); + + // Verify authentication fails when attempting an operation + client.invoke(() -> { + ClientCache clientCache = ClusterStartupRule.getClientCache(); + Region region = clientCache + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + + // Authentication should fail on first operation + assertThatThrownBy(() -> region.put("key", "value")) + .isInstanceOf(ServerOperationException.class) + .hasCauseInstanceOf(AuthenticationFailedException.class); + }); + } + + /** + * Test that clients without credentials are rejected. + * + *

+ * Verifies: + *

    + *
  • Missing authentication credentials cause connection failure
  • + *
  • TLS connection might establish but authentication is required
  • + *
+ */ + @Test + public void testClientWithMissingCredentialsRejected() throws Exception { + IgnoredException.addIgnoredException(AuthenticationFailedException.class.getName()); + IgnoredException.addIgnoredException("Authentication FAILED"); + IgnoredException.addIgnoredException("AuthenticationRequiredException"); + IgnoredException.addIgnoredException("No security credentials are provided"); + IgnoredException.addIgnoredException("ServerOperationException"); + + // Create certificates and stores + // Note: Locator and server must use same CertStores for peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Configure client with TLS but NO authentication credentials + Properties clientProps = clientStores.propertiesWith("all", false, false); + // Do NOT add authentication properties + + // Client connection succeeds (TLS is established), but authentication fails on first operation + ClientVM client = cluster.startClientVM(2, c -> c + .withProperties(clientProps) + .withLocatorConnection(locatorPort)); + + // Verify authentication fails when attempting an operation + client.invoke(() -> { + ClientCache clientCache = ClusterStartupRule.getClientCache(); + Region region = clientCache + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + + // Authentication should fail on first operation due to missing credentials + assertThatThrownBy(() -> region.put("key", "value")) + .isInstanceOf(ServerOperationException.class) + .hasCauseInstanceOf(AuthenticationRequiredException.class); + }); + } + + /** + * Test that clients with invalid tokens are rejected. + * + *

+ * Verifies: + *

    + *
  • Invalid bearer tokens cause authentication failure
  • + *
  • Token validation is enforced
  • + *
+ */ + @Test + public void testClientWithInvalidTokenRejected() throws Exception { + IgnoredException.addIgnoredException(AuthenticationFailedException.class.getName()); + IgnoredException.addIgnoredException("Authentication FAILED"); + IgnoredException.addIgnoredException("Token authentication FAILED"); + IgnoredException.addIgnoredException("ServerOperationException"); + + // Create certificates and stores + // Note: Locator and server must use same CertStores for peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Configure client with INVALID token + Properties clientProps = clientStores.propertiesWith("all", false, false); + clientProps.putAll(fixture.createClientTokenAuthProperties("INVALID_TOKEN")); + + // Client connection succeeds (TLS is established), but authentication fails on first operation + ClientVM client = cluster.startClientVM(2, c -> c + .withProperties(clientProps) + .withLocatorConnection(locatorPort)); + + // Verify authentication fails when attempting an operation + client.invoke(() -> { + ClientCache clientCache = ClusterStartupRule.getClientCache(); + Region region = clientCache + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + + // Authentication should fail on first operation due to invalid token + assertThatThrownBy(() -> region.put("key", "value")) + .isInstanceOf(ServerOperationException.class) + .hasCauseInstanceOf(AuthenticationFailedException.class); + }); + } + + /** + * Test that authenticated clients without authorization are blocked from operations. + * + *

+ * Verifies: + *

    + *
  • Authentication succeeds
  • + *
  • Unauthorized operations are blocked by SecurityManager
  • + *
+ */ + @Test + public void testClientUnauthorizedForOperation() throws Exception { + IgnoredException.addIgnoredException(NotAuthorizedException.class.getName()); + + // Create certificates and stores + // Note: Locator and server must use same CertStores for peer SSL communication + CertStores clusterStores = fixture.createClusterStores(); + CertStores clientStores = fixture.createClientStores(); + + // Start locator + Properties locatorProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Start server + Properties serverProps = clusterStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + // Create region + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Connect client with credentials that don't match required permissions + // SimpleSecurityManager authorizes based on principal matching permission string prefix + // User "readonly" will NOT be authorized for "DATA:WRITE" operations + Properties clientProps = clientStores.propertiesWith("all", false, false); + clientProps.putAll(fixture.createClientAuthProperties("readonly", "readonly")); + + ClientVM client = cluster.startClientVM(2, c -> c + .withProperties(clientProps) + .withLocatorConnection(locatorPort)); + + // Verify client can connect but unauthorized operations fail + client.invoke(() -> { + Region region = ClusterStartupRule.getClientCache() + .createClientRegionFactory(ClientRegionShortcut.PROXY) + .create(REGION_NAME); + + // This operation should fail due to lack of authorization + assertThatThrownBy(() -> region.put("unauthorizedKey", "value")) + .hasCauseInstanceOf(NotAuthorizedException.class); + }); + } + + /** + * Test that clients reject connections to servers without certificates. + * + *

+ * Verifies: + *

    + *
  • TLS handshake fails when server doesn't present certificate
  • + *
  • Client-side verification is enforced
  • + *
+ */ + @Test + public void testClientCannotConnectWithoutServerCert() throws Exception { + IgnoredException.addIgnoredException("SSLHandshakeException"); + IgnoredException.addIgnoredException("Server expecting SSL handshake"); + + // This test would require starting a server WITHOUT SSL configuration + // which is complex in a proper test environment. The test verifies the concept + // that mixing SSL and non-SSL components fails. + + // Create locator WITH SSL + CertStores locatorStores = fixture.createLocatorStores(); + + Properties locatorProps = locatorStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(2, locatorProps); + int locatorPort = locator.getPort(); + + // Start server WITHOUT SSL (simulating misconfiguration) + Properties serverProps = new Properties(); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + // No SSL properties + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + + // Server should fail to join the cluster due to SSL mismatch + assertThatThrownBy(() -> { + cluster.startServerVM(3, serverProps, locatorPort); + }).getCause().getCause().getCause() + .isInstanceOf(javax.net.ssl.SSLHandshakeException.class); + } + + /** + * Test that clients reject servers with invalid or untrusted certificates. + * + *

+ * Verifies: + *

    + *
  • Client verifies server certificate against truststore
  • + *
  • Invalid server certificates are rejected
  • + *
  • PKIX path validation is enforced
  • + *
+ */ + @Test + public void testClientRejectsServerWithInvalidCert() throws Exception { + IgnoredException.addIgnoredException("SSLHandshakeException"); + IgnoredException.addIgnoredException("path"); + IgnoredException.addIgnoredException("certificate"); + + // Create two separate CAs + CertificateMaterial validCA = fixture.getCA(); + + // Create a separate untrusted CA + CertificateMaterial untrustedCA = new CertificateBuilder() + .commonName("Untrusted-CA") + .isCA() + .generate(); + + // Create locator with valid CA + CertStores locatorStores = fixture.createLocatorStores(); + + Properties locatorProps = locatorStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(locatorProps); + fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster"); + MemberVM locator = cluster.startLocatorVM(0, locatorProps); + int locatorPort = locator.getPort(); + + // Create server with certificate from UNTRUSTED CA + CertificateMaterial untrustedServerCert = new CertificateBuilder() + .commonName("untrusted-server") + .issuedBy(untrustedCA) + .sanDnsName("localhost") + .sanIpAddress("127.0.0.1") + .generate(); + + CertStores untrustedServerStores = CertStores.serverStore(); + untrustedServerStores.withCertificate("server", untrustedServerCert); + untrustedServerStores.trust("untrustedCA", untrustedCA); // Server ONLY trusts untrusted CA, not + // valid CA + + Properties serverProps = untrustedServerStores.propertiesWith("all", false, false); + fixture.addSecurityManagerConfig(serverProps); + fixture.addPeerAuthProperties(serverProps, "cluster", "cluster"); + serverProps.setProperty("locators", "localhost[" + locatorPort + "]"); + serverProps.setProperty("member-timeout", "5000"); // Fail fast if can't join cluster + + // Server might start but cluster communication could fail due to cert mismatch + // OR server might fail to join cluster + // Either way, this demonstrates certificate validation + + // Create client truststore with valid CA only (doesn't trust untrusted CA) + CertStores clientStores = CertStores.clientStore(); + clientStores.trust("ca", validCA); // Client ONLY trusts valid CA + + Properties clientProps = clientStores.propertiesWith("all", false, false); + clientProps.putAll(fixture.createClientAuthProperties("testUser", "testUser")); + + // If server manages to start, client connection should fail due to untrusted certificate + try { + MemberVM server = cluster.startServerVM(1, serverProps, locatorPort); + + server.invoke(() -> { + ClusterStartupRule.getCache() + .createRegionFactory(RegionShortcut.REPLICATE) + .create(REGION_NAME); + }); + + // Client should reject server's untrusted certificate + assertThatThrownBy(() -> { + cluster.startClientVM(2, c -> c + .withProperties(clientProps) + .withLocatorConnection(locatorPort)); + }).satisfiesAnyOf( + e -> assertThat(e).hasMessageContaining("PKIX"), + e -> assertThat(e).hasMessageContaining("certificate"), + e -> assertThat(e).hasMessageContaining("trust")); + } catch (Exception e) { + // Server startup failure due to certificate mismatch is expected + // The cause chain should contain SSL/certificate/handshake errors + Throwable cause = e; + boolean foundSSLError = false; + while (cause != null && !foundSSLError) { + String message = cause.getClass().getName() + ": " + cause.getMessage(); + if (message.contains("SSL") || message.contains("certificate") + || message.contains("handshake")) { + foundSSLError = true; + } + cause = cause.getCause(); + } + assertThat(foundSSLError).as("Should find SSL/certificate/handshake error in cause chain") + .isTrue(); + } + } +} diff --git a/geode-junit/src/main/java/org/apache/geode/security/templates/TokenAuthInit.java b/geode-junit/src/main/java/org/apache/geode/security/templates/TokenAuthInit.java new file mode 100644 index 000000000000..a7aa7d3255e7 --- /dev/null +++ b/geode-junit/src/main/java/org/apache/geode/security/templates/TokenAuthInit.java @@ -0,0 +1,68 @@ +/* + * 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.geode.security.templates; + +import java.util.Properties; + +import org.apache.geode.LogWriter; +import org.apache.geode.distributed.DistributedMember; +import org.apache.geode.security.AuthInitialize; +import org.apache.geode.security.AuthenticationFailedException; +import org.apache.geode.security.SecurityManager; + +/** + * An {@link AuthInitialize} implementation that obtains a bearer token as credentials from the + * given set of properties. + * + * To use this class the {@code security-client-auth-init} property should be set to the fully + * qualified name of the static {@code create} method viz. + * {@code org.apache.geode.security.templates.TokenAuthInit.create} + */ +public class TokenAuthInit implements AuthInitialize { + + public static final String BEARER_TOKEN = "security-bearer-token"; + + protected LogWriter systemLogWriter; + protected LogWriter securityLogWriter; + + public static AuthInitialize create() { + return new TokenAuthInit(); + } + + @Override + public void init(final LogWriter systemLogWriter, final LogWriter securityLogWriter) + throws AuthenticationFailedException { + this.systemLogWriter = systemLogWriter; + this.securityLogWriter = securityLogWriter; + } + + @Override + public Properties getCredentials(final Properties securityProperties, + final DistributedMember server, final boolean isPeer) throws AuthenticationFailedException { + String token = securityProperties.getProperty(BEARER_TOKEN); + if (token == null) { + throw new AuthenticationFailedException( + "TokenAuthInit: bearer token property [" + BEARER_TOKEN + "] not set."); + } + + Properties securityPropertiesCopy = new Properties(); + // SecurityManager expects TOKEN property + securityPropertiesCopy.setProperty(SecurityManager.TOKEN, token); + return securityPropertiesCopy; + } + + @Override + public void close() {} +} diff --git a/geode-junit/src/main/java/org/apache/geode/test/junit/rules/ServerOnlyTLSTestFixture.java b/geode-junit/src/main/java/org/apache/geode/test/junit/rules/ServerOnlyTLSTestFixture.java new file mode 100644 index 000000000000..20ffc0924d96 --- /dev/null +++ b/geode-junit/src/main/java/org/apache/geode/test/junit/rules/ServerOnlyTLSTestFixture.java @@ -0,0 +1,253 @@ +/* + * 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.geode.test.junit.rules; + +import java.util.Properties; + +import org.apache.geode.cache.ssl.CertStores; +import org.apache.geode.cache.ssl.CertificateBuilder; +import org.apache.geode.cache.ssl.CertificateMaterial; +import org.apache.geode.examples.SimpleSecurityManager; +import org.apache.geode.security.templates.UserPasswordAuthInit; + +/** + * Test fixture for Server-only TLS with Alternative Client Authentication scenarios. + * + *

+ * This fixture creates: + *

    + *
  • A Certificate Authority (CA)
  • + *
  • Server certificates signed by the CA (for locators and servers)
  • + *
  • NO client certificates - clients authenticate via username/password or tokens
  • + *
  • Server/locator keystores with server certificates
  • + *
  • Client truststores with CA certificate (to verify servers)
  • + *
  • Security configuration using SimpleSecurityManager
  • + *
+ * + *

+ * Key characteristics: + *

    + *
  • ssl-require-authentication=false - servers don't require client certificates
  • + *
  • All transport is TLS-encrypted in both directions
  • + *
  • Clients authenticate using application-layer credentials (username/password or tokens)
  • + *
+ */ +public class ServerOnlyTLSTestFixture { + + private static final String CA_CN = "Server-Only-TLS-CA"; + private static final String SERVER_CN = "geode-server"; + private static final String LOCATOR_CN = "geode-locator"; + + private final CertificateMaterial ca; + private final CertificateMaterial serverCertificate; + private final CertificateMaterial locatorCertificate; + + public ServerOnlyTLSTestFixture() throws Exception { + // Create CA + ca = createCA(); + + // Create server and locator certificates (no client certificates needed) + serverCertificate = createServerCertificate(ca); + locatorCertificate = createLocatorCertificate(ca); + } + + /** + * Creates a Certificate Authority for signing server certificates. + */ + private CertificateMaterial createCA() throws Exception { + CertificateBuilder caBuilder = new CertificateBuilder() + .commonName(CA_CN) + .isCA(); + + return caBuilder.generate(); + } + + /** + * Creates a server certificate signed by the CA. + * Includes comprehensive Subject Alternative Names for server verification. + */ + private CertificateMaterial createServerCertificate(CertificateMaterial ca) throws Exception { + CertificateBuilder serverBuilder = new CertificateBuilder() + .commonName(SERVER_CN) + .issuedBy(ca) + .sanDnsName("localhost") + .sanDnsName("server.localdomain") + .sanIpAddress("127.0.0.1") + .sanIpAddress("0.0.0.0"); + + return serverBuilder.generate(); + } + + /** + * Creates a locator certificate signed by the CA. + * Includes comprehensive Subject Alternative Names for locator verification. + */ + private CertificateMaterial createLocatorCertificate(CertificateMaterial ca) throws Exception { + CertificateBuilder locatorBuilder = new CertificateBuilder() + .commonName(LOCATOR_CN) + .issuedBy(ca) + .sanDnsName("localhost") + .sanDnsName("locator.localdomain") + .sanIpAddress("127.0.0.1") + .sanIpAddress("0.0.0.0"); + + return locatorBuilder.generate(); + } + + /** + * Creates and returns server CertStores for server-only TLS. + * + *

+ * Server truststore contains the CA certificate to verify peer connections. + *

+ * Server keystore contains the server's certificate for presentation to clients. + * + * @return CertStores configured for server with server certificate + */ + public CertStores createServerStores() { + CertStores certStores = CertStores.serverStore(); + certStores.withCertificate("server", serverCertificate); + certStores.trust("ca", ca); + return certStores; + } + + /** + * Creates and returns locator CertStores for server-only TLS. + * + *

+ * Locator truststore contains the CA certificate to verify peer connections. + *

+ * Locator keystore contains the locator's certificate for presentation to clients and peers. + * + * @return CertStores configured for locator with locator certificate + */ + public CertStores createLocatorStores() { + CertStores certStores = CertStores.locatorStore(); + certStores.withCertificate("locator", locatorCertificate); + certStores.trust("ca", ca); + return certStores; + } + + /** + * Creates and returns cluster CertStores for both locator and server. + * + *

+ * For peer SSL communication, both locator and server need compatible certificates. + * This method creates CertStores with the server certificate that works for both. + * The server certificate includes SANs for localhost/127.0.0.1 which work for both roles. + * + * @return CertStores configured with server certificate and CA trust + */ + public CertStores createClusterStores() { + CertStores certStores = CertStores.serverStore(); + // Use server certificate for both locator and server (SANs cover both roles) + certStores.withCertificate("server", serverCertificate); + certStores.trust("ca", ca); + return certStores; + } + + /** + * Creates and returns client CertStores for server-only TLS. + * + *

+ * Client truststore contains ONLY the CA certificate to verify server certificates. + *

+ * NO keystore is created - clients do not present certificates. + * + * @return CertStores configured for client with only truststore (no client certificate) + */ + public CertStores createClientStores() { + CertStores certStores = CertStores.clientStore(); + // Client only needs truststore with CA to verify servers + certStores.trust("ca", ca); + // NO client keystore - clients don't present certificates + return certStores; + } + + /** + * Adds security manager configuration to properties. + * Uses SimpleSecurityManager for authentication and authorization. + */ + public Properties addSecurityManagerConfig(Properties props) { + props.setProperty("security-manager", SimpleSecurityManager.class.getName()); + return props; + } + + /** + * Adds peer authentication credentials for server/locator-to-locator connections. + * Required when security-manager is enabled. + * + * @param props the properties to add authentication to + * @param username the username for peer authentication + * @param password the password for peer authentication + * @return the properties with peer authentication configured + */ + public Properties addPeerAuthProperties(Properties props, String username, String password) { + props.setProperty("security-username", username); + props.setProperty("security-password", password); + return props; + } + + /** + * Creates client authentication properties with username and password. + * + * @param username the username + * @param password the password + * @return Properties with authentication configuration + */ + public Properties createClientAuthProperties(String username, String password) { + Properties props = new Properties(); + props.setProperty("security-client-auth-init", + UserPasswordAuthInit.class.getName() + ".create"); + props.setProperty(UserPasswordAuthInit.USER_NAME, username); + props.setProperty(UserPasswordAuthInit.PASSWORD, password); + return props; + } + + /** + * Creates client authentication properties with a bearer token. + * + * @param token the bearer token + * @return Properties with token authentication configuration + */ + public Properties createClientTokenAuthProperties(String token) { + Properties props = new Properties(); + props.setProperty("security-client-auth-init", + "org.apache.geode.security.templates.TokenAuthInit.create"); + props.setProperty("security-bearer-token", token); + return props; + } + + /** + * Gets the CA certificate material for testing purposes. + */ + public CertificateMaterial getCA() { + return ca; + } + + /** + * Gets the server certificate material for testing purposes. + */ + public CertificateMaterial getServerCertificate() { + return serverCertificate; + } + + /** + * Gets the locator certificate material for testing purposes. + */ + public CertificateMaterial getLocatorCertificate() { + return locatorCertificate; + } +}