Skip to content

Commit c63cbc1

Browse files
authored
Security: fix token bwc with pre 6.0.0-beta2 (#31254)
This commit fixes a backwards compatibility bug in the token service that causes token decoding to fail when there is a pre 6.0.0-beta2 node in the cluster. The token encoding is actually the culprit as a version check is missing around the serialization of the key hash bytes. This value was added in 6.0.0-beta2 and cannot be sent to nodes that do not know about this value. The version check has been added and the token service unit tests have been enhanced to randomly run with some 5.6.x nodes in the cluster service. Additionally, a small change was made to the way we check to see if the token metadata needs to be installed. Previously we would pass the metadata to the install method and check that the token metadata is null. This null check is now done prior to checking if the metadata can be installed. Relates #30743 Closes #31195
1 parent 9e3c364 commit c63cbc1

File tree

4 files changed

+90
-51
lines changed

4 files changed

+90
-51
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import org.apache.lucene.util.BytesRef;
1010
import org.apache.lucene.util.BytesRefBuilder;
1111
import org.elasticsearch.cluster.ClusterStateUpdateTask;
12-
import org.elasticsearch.cluster.metadata.MetaData;
1312
import org.elasticsearch.common.Priority;
1413
import org.elasticsearch.core.internal.io.IOUtils;
1514
import org.apache.lucene.util.UnicodeUtil;
@@ -1041,7 +1040,9 @@ public String getUserTokenString(UserToken userToken) throws IOException, Genera
10411040
KeyAndCache keyAndCache = keyCache.activeKeyCache;
10421041
Version.writeVersion(userToken.getVersion(), out);
10431042
out.writeByteArray(keyAndCache.getSalt().bytes);
1044-
out.writeByteArray(keyAndCache.getKeyHash().bytes);
1043+
if (userToken.getVersion().onOrAfter(Version.V_6_0_0_beta2)) {
1044+
out.writeByteArray(keyAndCache.getKeyHash().bytes);
1045+
}
10451046
final byte[] initializationVector = getNewInitializationVector();
10461047
out.writeByteArray(initializationVector);
10471048
try (CipherOutputStream encryptedOutput =
@@ -1369,16 +1370,18 @@ private void initialize(ClusterService clusterService) {
13691370
return;
13701371
}
13711372

1373+
TokenMetaData custom = event.state().custom(TokenMetaData.TYPE);
13721374
if (state.nodes().isLocalNodeElectedMaster()) {
1373-
if (XPackPlugin.isReadyForXPackCustomMetadata(state)) {
1374-
installTokenMetadata(state.metaData());
1375-
} else {
1376-
logger.debug("cannot add token metadata to cluster as the following nodes might not understand the metadata: {}",
1377-
() -> XPackPlugin.nodesNotReadyForXPackCustomMetadata(state));
1375+
if (custom == null) {
1376+
if (XPackPlugin.isReadyForXPackCustomMetadata(state)) {
1377+
installTokenMetadata();
1378+
} else {
1379+
logger.debug("cannot add token metadata to cluster as the following nodes might not understand the metadata: {}",
1380+
() -> XPackPlugin.nodesNotReadyForXPackCustomMetadata(state));
1381+
}
13781382
}
13791383
}
13801384

1381-
TokenMetaData custom = event.state().custom(TokenMetaData.TYPE);
13821385
if (custom != null && custom.equals(getTokenMetaData()) == false) {
13831386
logger.info("refresh keys");
13841387
try {
@@ -1394,33 +1397,31 @@ private void initialize(ClusterService clusterService) {
13941397
// to prevent too many cluster state update tasks to be queued for doing the same update
13951398
private final AtomicBoolean installTokenMetadataInProgress = new AtomicBoolean(false);
13961399

1397-
private void installTokenMetadata(MetaData metaData) {
1398-
if (metaData.custom(TokenMetaData.TYPE) == null) {
1399-
if (installTokenMetadataInProgress.compareAndSet(false, true)) {
1400-
clusterService.submitStateUpdateTask("install-token-metadata", new ClusterStateUpdateTask(Priority.URGENT) {
1401-
@Override
1402-
public ClusterState execute(ClusterState currentState) {
1403-
XPackPlugin.checkReadyForXPackCustomMetadata(currentState);
1400+
private void installTokenMetadata() {
1401+
if (installTokenMetadataInProgress.compareAndSet(false, true)) {
1402+
clusterService.submitStateUpdateTask("install-token-metadata", new ClusterStateUpdateTask(Priority.URGENT) {
1403+
@Override
1404+
public ClusterState execute(ClusterState currentState) {
1405+
XPackPlugin.checkReadyForXPackCustomMetadata(currentState);
14041406

1405-
if (currentState.custom(TokenMetaData.TYPE) == null) {
1406-
return ClusterState.builder(currentState).putCustom(TokenMetaData.TYPE, getTokenMetaData()).build();
1407-
} else {
1408-
return currentState;
1409-
}
1407+
if (currentState.custom(TokenMetaData.TYPE) == null) {
1408+
return ClusterState.builder(currentState).putCustom(TokenMetaData.TYPE, getTokenMetaData()).build();
1409+
} else {
1410+
return currentState;
14101411
}
1412+
}
14111413

1412-
@Override
1413-
public void onFailure(String source, Exception e) {
1414-
installTokenMetadataInProgress.set(false);
1415-
logger.error("unable to install token metadata", e);
1416-
}
1414+
@Override
1415+
public void onFailure(String source, Exception e) {
1416+
installTokenMetadataInProgress.set(false);
1417+
logger.error("unable to install token metadata", e);
1418+
}
14171419

1418-
@Override
1419-
public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
1420-
installTokenMetadataInProgress.set(false);
1421-
}
1422-
});
1423-
}
1420+
@Override
1421+
public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
1422+
installTokenMetadataInProgress.set(false);
1423+
}
1424+
});
14241425
}
14251426
}
14261427

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
import org.elasticsearch.action.update.UpdateAction;
2727
import org.elasticsearch.action.update.UpdateRequestBuilder;
2828
import org.elasticsearch.client.Client;
29+
import org.elasticsearch.cluster.ClusterState;
30+
import org.elasticsearch.cluster.node.DiscoveryNode;
31+
import org.elasticsearch.cluster.node.DiscoveryNodes;
2932
import org.elasticsearch.cluster.service.ClusterService;
3033
import org.elasticsearch.common.settings.MockSecureSettings;
3134
import org.elasticsearch.common.Strings;
@@ -44,6 +47,7 @@
4447
import org.elasticsearch.test.ClusterServiceUtils;
4548
import org.elasticsearch.test.ESTestCase;
4649
import org.elasticsearch.test.EqualsHashCodeTestUtils;
50+
import org.elasticsearch.test.VersionUtils;
4751
import org.elasticsearch.threadpool.FixedExecutorBuilder;
4852
import org.elasticsearch.threadpool.ThreadPool;
4953
import org.elasticsearch.xpack.core.XPackSettings;
@@ -53,6 +57,7 @@
5357
import org.elasticsearch.xpack.core.security.user.User;
5458
import org.elasticsearch.xpack.core.watcher.watch.ClockMock;
5559
import org.elasticsearch.xpack.security.support.SecurityIndexManager;
60+
import org.junit.After;
5661
import org.junit.AfterClass;
5762
import org.junit.Before;
5863
import org.junit.BeforeClass;
@@ -66,6 +71,7 @@
6671
import java.time.temporal.ChronoUnit;
6772
import java.util.Base64;
6873
import java.util.Collections;
74+
import java.util.EnumSet;
6975
import java.util.HashMap;
7076
import java.util.Map;
7177
import java.util.concurrent.ExecutionException;
@@ -91,6 +97,7 @@ public class TokenServiceTests extends ESTestCase {
9197
private Client client;
9298
private SecurityIndexManager securityIndex;
9399
private ClusterService clusterService;
100+
private boolean mixedCluster;
94101
private Settings tokenServiceEnabledSettings = Settings.builder()
95102
.put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true).build();
96103

@@ -141,6 +148,25 @@ public void setupClient() {
141148
return null;
142149
}).when(securityIndex).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class));
143150
this.clusterService = ClusterServiceUtils.createClusterService(threadPool);
151+
this.mixedCluster = randomBoolean();
152+
if (mixedCluster) {
153+
Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_6_0, Version.V_5_6_10);
154+
logger.info("adding a node with version [{}] to the cluster service", version);
155+
ClusterState updatedState = ClusterState.builder(clusterService.state())
156+
.nodes(DiscoveryNodes.builder(clusterService.state().nodes())
157+
.add(new DiscoveryNode("56node", ESTestCase.buildNewFakeTransportAddress(), Collections.emptyMap(),
158+
EnumSet.allOf(DiscoveryNode.Role.class), version))
159+
.build())
160+
.build();
161+
ClusterServiceUtils.setState(clusterService, updatedState);
162+
}
163+
}
164+
165+
@After
166+
public void stopClusterService() {
167+
if (clusterService != null) {
168+
clusterService.close();
169+
}
144170
}
145171

146172
@BeforeClass
@@ -172,7 +198,7 @@ public void testAttachAndGetToken() throws Exception {
172198
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
173199
tokenService.getAndValidateToken(requestContext, future);
174200
UserToken serialized = future.get();
175-
assertEquals(authentication, serialized.getAuthentication());
201+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
176202
}
177203

178204
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
@@ -183,11 +209,12 @@ public void testAttachAndGetToken() throws Exception {
183209
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
184210
anotherService.getAndValidateToken(requestContext, future);
185211
UserToken fromOtherService = future.get();
186-
assertEquals(authentication, fromOtherService.getAuthentication());
212+
assertAuthenticationEquals(authentication, fromOtherService.getAuthentication());
187213
}
188214
}
189215

190216
public void testRotateKey() throws Exception {
217+
assumeFalse("internally managed keys do not work in a mixed cluster", mixedCluster);
191218
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService);
192219
Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null);
193220
PlainActionFuture<Tuple<UserToken, String>> tokenFuture = new PlainActionFuture<>();
@@ -203,15 +230,15 @@ public void testRotateKey() throws Exception {
203230
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
204231
tokenService.getAndValidateToken(requestContext, future);
205232
UserToken serialized = future.get();
206-
assertEquals(authentication, serialized.getAuthentication());
233+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
207234
}
208235
rotateKeys(tokenService);
209236

210237
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
211238
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
212239
tokenService.getAndValidateToken(requestContext, future);
213240
UserToken serialized = future.get();
214-
assertEquals(authentication, serialized.getAuthentication());
241+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
215242
}
216243

217244
PlainActionFuture<Tuple<UserToken, String>> newTokenFuture = new PlainActionFuture<>();
@@ -240,6 +267,7 @@ private void rotateKeys(TokenService tokenService) {
240267
}
241268

242269
public void testKeyExchange() throws Exception {
270+
assumeFalse("internally managed keys do not work in a mixed cluster", mixedCluster);
243271
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService);
244272
int numRotations = 0;randomIntBetween(1, 5);
245273
for (int i = 0; i < numRotations; i++) {
@@ -261,7 +289,7 @@ public void testKeyExchange() throws Exception {
261289
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
262290
otherTokenService.getAndValidateToken(requestContext, future);
263291
UserToken serialized = future.get();
264-
assertEquals(authentication, serialized.getAuthentication());
292+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
265293
}
266294

267295
rotateKeys(tokenService);
@@ -272,11 +300,12 @@ public void testKeyExchange() throws Exception {
272300
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
273301
otherTokenService.getAndValidateToken(requestContext, future);
274302
UserToken serialized = future.get();
275-
assertEquals(authentication, serialized.getAuthentication());
303+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
276304
}
277305
}
278306

279307
public void testPruneKeys() throws Exception {
308+
assumeFalse("internally managed keys do not work in a mixed cluster", mixedCluster);
280309
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService);
281310
Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null);
282311
PlainActionFuture<Tuple<UserToken, String>> tokenFuture = new PlainActionFuture<>();
@@ -292,7 +321,7 @@ public void testPruneKeys() throws Exception {
292321
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
293322
tokenService.getAndValidateToken(requestContext, future);
294323
UserToken serialized = future.get();
295-
assertEquals(authentication, serialized.getAuthentication());
324+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
296325
}
297326
TokenMetaData metaData = tokenService.pruneKeys(randomIntBetween(0, 100));
298327
tokenService.refreshMetaData(metaData);
@@ -306,7 +335,7 @@ public void testPruneKeys() throws Exception {
306335
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
307336
tokenService.getAndValidateToken(requestContext, future);
308337
UserToken serialized = future.get();
309-
assertEquals(authentication, serialized.getAuthentication());
338+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
310339
}
311340

312341
PlainActionFuture<Tuple<UserToken, String>> newTokenFuture = new PlainActionFuture<>();
@@ -332,7 +361,7 @@ public void testPruneKeys() throws Exception {
332361
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
333362
tokenService.getAndValidateToken(requestContext, future);
334363
UserToken serialized = future.get();
335-
assertEquals(authentication, serialized.getAuthentication());
364+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
336365
}
337366

338367
}
@@ -353,7 +382,7 @@ public void testPassphraseWorks() throws Exception {
353382
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
354383
tokenService.getAndValidateToken(requestContext, future);
355384
UserToken serialized = future.get();
356-
assertEquals(authentication, serialized.getAuthentication());
385+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
357386
}
358387

359388
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
@@ -454,7 +483,7 @@ public void testTokenExpiry() throws Exception {
454483
// the clock is still frozen, so the cookie should be valid
455484
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
456485
tokenService.getAndValidateToken(requestContext, future);
457-
assertEquals(authentication, future.get().getAuthentication());
486+
assertAuthenticationEquals(authentication, future.get().getAuthentication());
458487
}
459488

460489
final TimeValue defaultExpiration = TokenService.TOKEN_EXPIRATION.get(Settings.EMPTY);
@@ -464,7 +493,7 @@ public void testTokenExpiry() throws Exception {
464493
clock.fastForwardSeconds(fastForwardAmount);
465494
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
466495
tokenService.getAndValidateToken(requestContext, future);
467-
assertEquals(authentication, future.get().getAuthentication());
496+
assertAuthenticationEquals(authentication, future.get().getAuthentication());
468497
}
469498

470499
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
@@ -473,7 +502,7 @@ public void testTokenExpiry() throws Exception {
473502
clock.rewind(TimeValue.timeValueNanos(clock.instant().getNano())); // trim off nanoseconds since don't store them in the index
474503
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
475504
tokenService.getAndValidateToken(requestContext, future);
476-
assertEquals(authentication, future.get().getAuthentication());
505+
assertAuthenticationEquals(authentication, future.get().getAuthentication());
477506
}
478507

479508
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
@@ -569,7 +598,7 @@ public void testIndexNotAvailable() throws Exception {
569598
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
570599
tokenService.getAndValidateToken(requestContext, future);
571600
UserToken serialized = future.get();
572-
assertEquals(authentication, serialized.getAuthentication());
601+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
573602

574603
when(securityIndex.isAvailable()).thenReturn(false);
575604
when(securityIndex.indexExists()).thenReturn(true);
@@ -601,6 +630,7 @@ public void testDecodePre6xToken() throws GeneralSecurityException, ExecutionExc
601630
assertWarnings("[xpack.security.authc.token.passphrase] setting was deprecated in Elasticsearch and will be removed in a future" +
602631
" release! See the breaking changes documentation for the next major version.");
603632
}
633+
604634
public void testGetAuthenticationWorksWithExpiredToken() throws Exception {
605635
TokenService tokenService =
606636
new TokenService(tokenServiceEnabledSettings, Clock.systemUTC(), client, securityIndex, clusterService);
@@ -611,7 +641,7 @@ public void testGetAuthenticationWorksWithExpiredToken() throws Exception {
611641
PlainActionFuture<Tuple<Authentication, Map<String, Object>>> authFuture = new PlainActionFuture<>();
612642
tokenService.getAuthenticationAndMetaData(userTokenString, authFuture);
613643
Authentication retrievedAuth = authFuture.actionGet().v1();
614-
assertEquals(authentication, retrievedAuth);
644+
assertAuthenticationEquals(authentication, retrievedAuth);
615645
}
616646

617647
private void mockGetTokenFromId(UserToken userToken) {
@@ -638,4 +668,16 @@ public static void mockGetTokenFromId(UserToken userToken, Client client) {
638668
return Void.TYPE;
639669
}).when(client).get(any(GetRequest.class), any(ActionListener.class));
640670
}
671+
672+
private void assertAuthenticationEquals(Authentication expected, Authentication actual) {
673+
if (mixedCluster) {
674+
assertNotNull(expected);
675+
assertNotNull(actual);
676+
assertEquals(expected.getUser(), actual.getUser());
677+
assertEquals(expected.getAuthenticatedBy(), actual.getAuthenticatedBy());
678+
assertEquals(expected.getLookedUpBy(), actual.getLookedUpBy());
679+
} else {
680+
assertEquals(expected, actual);
681+
}
682+
}
641683
}

x-pack/qa/rolling-upgrade/build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ subprojects {
146146
if (version.onOrAfter('6.0.0') == false) {
147147
// this is needed since in 5.6 we don't bootstrap the token service if there is no explicit initial password
148148
keystoreSetting 'xpack.security.authc.token.passphrase', 'xpack_token_passphrase'
149-
setting 'xpack.security.authc.token.enabled', 'true'
150149
}
151150
dependsOn copyTestNodeKeystore
152151
extraConfigFile 'testnode.jks', new File(outputDir + '/testnode.jks')

0 commit comments

Comments
 (0)