Skip to content

Commit 4e93d2c

Browse files
authored
Testcases for Server-Only TLS with Application-Layer Authentication (#7987)
1 parent 3b21ac6 commit 4e93d2c

5 files changed

Lines changed: 1697 additions & 0 deletions

File tree

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
3+
* agreements. See the NOTICE file distributed with this work for additional information regarding
4+
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
5+
* "License"); you may not use this file except in compliance with the License. You may obtain a
6+
* copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software distributed under the License
11+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
* or implied. See the License for the specific language governing permissions and limitations under
13+
* the License.
14+
*/
15+
package org.apache.geode.cache.ssl;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
20+
import java.util.Properties;
21+
22+
import org.junit.Before;
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
import org.junit.experimental.categories.Category;
26+
27+
import org.apache.geode.cache.RegionShortcut;
28+
import org.apache.geode.security.GemFireSecurityException;
29+
import org.apache.geode.test.dunit.IgnoredException;
30+
import org.apache.geode.test.dunit.rules.ClusterStartupRule;
31+
import org.apache.geode.test.dunit.rules.MemberVM;
32+
import org.apache.geode.test.junit.categories.SecurityTest;
33+
import org.apache.geode.test.junit.rules.ServerOnlyTLSTestFixture;
34+
35+
/**
36+
* Distributed tests for Peer-to-Peer (P2P) Cache Topology using Server-only TLS with
37+
* Application-Layer Authentication (Approach 3).
38+
*
39+
* <p>
40+
* This test demonstrates that in a P2P cache configuration (where all members are peers, no
41+
* client/server distinction), Approach 3 works correctly:
42+
* <ul>
43+
* <li><strong>TLS Encryption:</strong> All peer-to-peer connections use TLS for transport
44+
* encryption</li>
45+
* <li><strong>No Certificate Authentication:</strong> Peers do NOT exchange certificates during
46+
* TLS handshake (ssl-require-authentication=false)</li>
47+
* <li><strong>Application-Layer Authentication:</strong> Peers authenticate using
48+
* username/password via SecurityManager</li>
49+
* <li><strong>Authorization:</strong> SecurityManager enforces CLUSTER:MANAGE permission for peer
50+
* join</li>
51+
* </ul>
52+
*
53+
* <p>
54+
* <strong>Key Difference from Client/Server:</strong> In P2P topology, all members are equal peers
55+
* that communicate directly. Each peer presents a server certificate for TLS encryption, but
56+
* authentication happens at the application layer using credentials validated by SecurityManager.
57+
*
58+
* <p>
59+
* This approach solves the public CA clientAuth EKU sunset problem for P2P topologies by:
60+
* <ol>
61+
* <li>Eliminating the need for client certificates entirely</li>
62+
* <li>Maintaining full TLS encryption for all transport</li>
63+
* <li>Using existing authentication infrastructure (LDAP, database, tokens)</li>
64+
* </ol>
65+
*
66+
* @see ServerOnlyTLSWithAuthDUnitTest for client/server topology tests
67+
*/
68+
@Category({SecurityTest.class})
69+
public class P2PServerOnlyTLSWithAuthDUnitTest {
70+
71+
private static final String REGION_NAME = "testRegion";
72+
73+
@Rule
74+
public ClusterStartupRule cluster = new ClusterStartupRule();
75+
76+
private ServerOnlyTLSTestFixture fixture;
77+
78+
@Before
79+
public void setUp() throws Exception {
80+
fixture = new ServerOnlyTLSTestFixture();
81+
82+
// Add ignored exceptions for SSL-related cleanup warnings
83+
IgnoredException.addIgnoredException("javax.net.ssl.SSLException");
84+
IgnoredException.addIgnoredException("java.io.IOException");
85+
IgnoredException.addIgnoredException("Authentication failed");
86+
IgnoredException.addIgnoredException("Security check failed");
87+
}
88+
89+
/**
90+
* Test basic P2P cluster formation with server-only TLS and application-layer authentication.
91+
*
92+
* <p>
93+
* Verifies:
94+
* <ul>
95+
* <li>Locator and servers present TLS certificates (server cert from public or private CA)</li>
96+
* <li>Peers do NOT present client certificates during TLS handshake</li>
97+
* <li>All peers authenticate using username/password</li>
98+
* <li>SecurityManager validates credentials and requires CLUSTER:MANAGE permission</li>
99+
* <li>Cluster forms successfully with encrypted peer-to-peer communication</li>
100+
* </ul>
101+
*/
102+
@Test
103+
public void testP2PClusterFormationWithServerOnlyTLSAndAppAuth() throws Exception {
104+
// Create certificate stores using fixture
105+
// All peers use the same certificate for TLS (server cert)
106+
CertStores clusterStores = fixture.createClusterStores();
107+
108+
// Configure locator with:
109+
// - Server-only TLS (ssl-require-authentication=false)
110+
// - Security manager for application-layer authentication
111+
// - Peer authentication credentials
112+
Properties locatorProps = clusterStores.propertiesWith("all", false, false);
113+
fixture.addSecurityManagerConfig(locatorProps);
114+
fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
115+
116+
// Start locator - it will authenticate itself when joining
117+
MemberVM locator = cluster.startLocatorVM(0, locatorProps);
118+
int locatorPort = locator.getPort();
119+
120+
// Configure first server with same setup
121+
Properties server1Props = clusterStores.propertiesWith("all", false, false);
122+
fixture.addSecurityManagerConfig(server1Props);
123+
fixture.addPeerAuthProperties(server1Props, "cluster", "cluster");
124+
125+
// Start first server - it joins via application-layer auth, not certificate auth
126+
MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort);
127+
128+
// Verify server1 successfully joined the cluster
129+
server1.invoke(() -> {
130+
assertThat(ClusterStartupRule.getCache()).isNotNull();
131+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
132+
.hasSize(1); // locator
133+
});
134+
135+
// Configure second server with same setup
136+
Properties server2Props = clusterStores.propertiesWith("all", false, false);
137+
fixture.addSecurityManagerConfig(server2Props);
138+
fixture.addPeerAuthProperties(server2Props, "cluster", "cluster");
139+
140+
// Start second server
141+
MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort);
142+
143+
// Verify server2 successfully joined and sees all peers
144+
server2.invoke(() -> {
145+
assertThat(ClusterStartupRule.getCache()).isNotNull();
146+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
147+
.hasSize(2); // locator + server1
148+
});
149+
150+
// Verify all peers see each other (peer-to-peer mesh formed)
151+
server1.invoke(() -> {
152+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
153+
.hasSize(2); // locator + server2
154+
});
155+
}
156+
157+
/**
158+
* Test P2P data replication across encrypted peer connections without certificate authentication.
159+
*
160+
* <p>
161+
* Verifies:
162+
* <ul>
163+
* <li>Data replicates across peers using TLS-encrypted connections</li>
164+
* <li>No certificate authentication is used (application credentials only)</li>
165+
* <li>All operations succeed over server-only TLS</li>
166+
* </ul>
167+
*/
168+
@Test
169+
public void testP2PDataReplicationOverServerOnlyTLS() throws Exception {
170+
CertStores clusterStores = fixture.createClusterStores();
171+
172+
// Configure and start locator
173+
Properties locatorProps = clusterStores.propertiesWith("all", false, false);
174+
fixture.addSecurityManagerConfig(locatorProps);
175+
fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
176+
MemberVM locator = cluster.startLocatorVM(0, locatorProps);
177+
int locatorPort = locator.getPort();
178+
179+
// Start server1 with replicated region
180+
Properties server1Props = clusterStores.propertiesWith("all", false, false);
181+
fixture.addSecurityManagerConfig(server1Props);
182+
fixture.addPeerAuthProperties(server1Props, "cluster", "cluster");
183+
MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort);
184+
185+
server1.invoke(() -> {
186+
ClusterStartupRule.getCache()
187+
.createRegionFactory(RegionShortcut.REPLICATE)
188+
.create(REGION_NAME);
189+
});
190+
191+
// Start server2 with same replicated region
192+
Properties server2Props = clusterStores.propertiesWith("all", false, false);
193+
fixture.addSecurityManagerConfig(server2Props);
194+
fixture.addPeerAuthProperties(server2Props, "cluster", "cluster");
195+
MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort);
196+
197+
server2.invoke(() -> {
198+
ClusterStartupRule.getCache()
199+
.createRegionFactory(RegionShortcut.REPLICATE)
200+
.create(REGION_NAME);
201+
});
202+
203+
// Put data on server1
204+
server1.invoke(() -> {
205+
ClusterStartupRule.getCache()
206+
.getRegion(REGION_NAME)
207+
.put("key1", "value1");
208+
});
209+
210+
// Verify data replicated to server2 over TLS-encrypted peer connection
211+
server2.invoke(() -> {
212+
Object value = ClusterStartupRule.getCache()
213+
.getRegion(REGION_NAME)
214+
.get("key1");
215+
assertThat(value).isEqualTo("value1");
216+
});
217+
218+
// Put data on server2
219+
server2.invoke(() -> {
220+
ClusterStartupRule.getCache()
221+
.getRegion(REGION_NAME)
222+
.put("key2", "value2");
223+
});
224+
225+
// Verify data replicated to server1
226+
server1.invoke(() -> {
227+
Object value = ClusterStartupRule.getCache()
228+
.getRegion(REGION_NAME)
229+
.get("key2");
230+
assertThat(value).isEqualTo("value2");
231+
});
232+
}
233+
234+
/**
235+
* Test that peer with invalid credentials cannot join P2P cluster.
236+
*
237+
* <p>
238+
* Verifies:
239+
* <ul>
240+
* <li>TLS handshake succeeds (server cert validation)</li>
241+
* <li>Application-layer authentication fails with wrong password</li>
242+
* <li>Peer is rejected and cannot join cluster</li>
243+
* </ul>
244+
*/
245+
@Test
246+
public void testP2PPeerRejectedWithInvalidCredentials() throws Exception {
247+
// Add ignored exception for authentication failure messages
248+
IgnoredException.addIgnoredException("Authentication FAILED");
249+
CertStores clusterStores = fixture.createClusterStores();
250+
251+
// Configure and start locator
252+
Properties locatorProps = clusterStores.propertiesWith("all", false, false);
253+
fixture.addSecurityManagerConfig(locatorProps);
254+
fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
255+
MemberVM locator = cluster.startLocatorVM(0, locatorProps);
256+
int locatorPort = locator.getPort();
257+
258+
// Try to start server with INVALID credentials
259+
Properties serverProps = clusterStores.propertiesWith("all", false, false);
260+
fixture.addSecurityManagerConfig(serverProps);
261+
fixture.addPeerAuthProperties(serverProps, "cluster", "wrongPassword"); // INVALID
262+
263+
// Server should fail to join due to authentication failure
264+
// Note: Root cause is SecurityException, not GemFireSecurityException
265+
assertThatThrownBy(() -> cluster.startServerVM(1, serverProps, locatorPort))
266+
.hasRootCauseInstanceOf(SecurityException.class)
267+
.hasStackTraceContaining("invalid username/password");
268+
}
269+
270+
/**
271+
* Test that peer without CLUSTER:MANAGE permission cannot join P2P cluster.
272+
*
273+
* <p>
274+
* Verifies:
275+
* <ul>
276+
* <li>TLS handshake succeeds</li>
277+
* <li>Application-layer authentication succeeds (valid username/password)</li>
278+
* <li>Authorization fails (lacks CLUSTER:MANAGE permission)</li>
279+
* <li>Peer is rejected and cannot join cluster</li>
280+
* </ul>
281+
*/
282+
@Test
283+
public void testP2PPeerRejectedWithoutClusterManagePermission() throws Exception {
284+
CertStores clusterStores = fixture.createClusterStores();
285+
286+
// Configure and start locator
287+
Properties locatorProps = clusterStores.propertiesWith("all", false, false);
288+
fixture.addSecurityManagerConfig(locatorProps);
289+
fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
290+
MemberVM locator = cluster.startLocatorVM(0, locatorProps);
291+
int locatorPort = locator.getPort();
292+
293+
// Try to start server with user that has valid credentials but no CLUSTER:MANAGE
294+
// SimpleSecurityManager allows authentication when username == password
295+
// but "data" user does NOT have CLUSTER:MANAGE permission
296+
Properties serverProps = clusterStores.propertiesWith("all", false, false);
297+
fixture.addSecurityManagerConfig(serverProps);
298+
fixture.addPeerAuthProperties(serverProps, "data", "data"); // Valid creds, insufficient perms
299+
300+
// Server should fail to join due to authorization failure
301+
// Note: Root cause is SecurityException, not GemFireSecurityException
302+
assertThatThrownBy(() -> cluster.startServerVM(1, serverProps, locatorPort))
303+
.hasRootCauseInstanceOf(SecurityException.class)
304+
.hasStackTraceContaining("not authorized for CLUSTER:MANAGE");
305+
}
306+
307+
/**
308+
* Test that peer with no credentials cannot join P2P cluster.
309+
*
310+
* <p>
311+
* Verifies:
312+
* <ul>
313+
* <li>TLS handshake succeeds (encryption established)</li>
314+
* <li>Application-layer authentication fails (no credentials provided)</li>
315+
* <li>Peer is rejected</li>
316+
* </ul>
317+
*/
318+
@Test
319+
public void testP2PPeerRejectedWithNoCredentials() throws Exception {
320+
CertStores clusterStores = fixture.createClusterStores();
321+
322+
// Configure and start locator
323+
Properties locatorProps = clusterStores.propertiesWith("all", false, false);
324+
fixture.addSecurityManagerConfig(locatorProps);
325+
fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
326+
MemberVM locator = cluster.startLocatorVM(0, locatorProps);
327+
int locatorPort = locator.getPort();
328+
329+
// Try to start server WITHOUT any authentication credentials
330+
Properties serverProps = clusterStores.propertiesWith("all", false, false);
331+
fixture.addSecurityManagerConfig(serverProps);
332+
// NO peer auth properties added - missing credentials
333+
334+
// Server should fail to join due to missing credentials
335+
assertThatThrownBy(() -> cluster.startServerVM(1, serverProps, locatorPort))
336+
.hasRootCauseInstanceOf(GemFireSecurityException.class);
337+
}
338+
339+
/**
340+
* Test multiple peers joining with different valid credentials.
341+
*
342+
* <p>
343+
* Verifies that the cluster supports heterogeneous peer credentials as long as all have
344+
* CLUSTER:MANAGE permission. This demonstrates flexibility in credential management where
345+
* different services/teams can use different credentials.
346+
*/
347+
@Test
348+
public void testMultiplePeersWithDifferentCredentials() throws Exception {
349+
CertStores clusterStores = fixture.createClusterStores();
350+
351+
// Start locator with "cluster" credentials
352+
Properties locatorProps = clusterStores.propertiesWith("all", false, false);
353+
fixture.addSecurityManagerConfig(locatorProps);
354+
fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
355+
MemberVM locator = cluster.startLocatorVM(0, locatorProps);
356+
int locatorPort = locator.getPort();
357+
358+
// Start server1 with "clusterManage" credentials
359+
// SimpleSecurityManager grants CLUSTER:MANAGE to "cluster" and "clusterManage" users
360+
Properties server1Props = clusterStores.propertiesWith("all", false, false);
361+
fixture.addSecurityManagerConfig(server1Props);
362+
fixture.addPeerAuthProperties(server1Props, "clusterManage", "clusterManage");
363+
MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort);
364+
365+
// Start server2 with "cluster" credentials (same as locator)
366+
Properties server2Props = clusterStores.propertiesWith("all", false, false);
367+
fixture.addSecurityManagerConfig(server2Props);
368+
fixture.addPeerAuthProperties(server2Props, "cluster", "cluster");
369+
MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort);
370+
371+
// Verify all peers joined successfully
372+
server1.invoke(() -> {
373+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
374+
.hasSize(2); // locator + server2
375+
});
376+
377+
server2.invoke(() -> {
378+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
379+
.hasSize(2); // locator + server1
380+
});
381+
}
382+
}

0 commit comments

Comments
 (0)