Skip to content

Commit d3ba009

Browse files
committed
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 20c860a commit d3ba009

File tree

4 files changed

+93
-57
lines changed

4 files changed

+93
-57
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.StringHelper;
@@ -1043,7 +1042,9 @@ public String getUserTokenString(UserToken userToken) throws IOException, Genera
10431042
KeyAndCache keyAndCache = keyCache.activeKeyCache;
10441043
Version.writeVersion(userToken.getVersion(), out);
10451044
out.writeByteArray(keyAndCache.getSalt().bytes);
1046-
out.writeByteArray(keyAndCache.getKeyHash().bytes);
1045+
if (userToken.getVersion().onOrAfter(Version.V_6_0_0_beta2)) {
1046+
out.writeByteArray(keyAndCache.getKeyHash().bytes);
1047+
}
10471048
final byte[] initializationVector = getNewInitializationVector();
10481049
out.writeByteArray(initializationVector);
10491050
try (CipherOutputStream encryptedOutput =
@@ -1371,16 +1372,18 @@ private void initialize(ClusterService clusterService) {
13711372
return;
13721373
}
13731374

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

1383-
TokenMetaData custom = event.state().custom(TokenMetaData.TYPE);
13841387
if (custom != null && custom.equals(getTokenMetaData()) == false) {
13851388
logger.info("refresh keys");
13861389
try {
@@ -1396,33 +1399,31 @@ private void initialize(ClusterService clusterService) {
13961399
// to prevent too many cluster state update tasks to be queued for doing the same update
13971400
private final AtomicBoolean installTokenMetadataInProgress = new AtomicBoolean(false);
13981401

1399-
private void installTokenMetadata(MetaData metaData) {
1400-
if (metaData.custom(TokenMetaData.TYPE) == null) {
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);
1402+
private void installTokenMetadata() {
1403+
if (installTokenMetadataInProgress.compareAndSet(false, true)) {
1404+
clusterService.submitStateUpdateTask("install-token-metadata", new ClusterStateUpdateTask(Priority.URGENT) {
1405+
@Override
1406+
public ClusterState execute(ClusterState currentState) {
1407+
XPackPlugin.checkReadyForXPackCustomMetadata(currentState);
14061408

1407-
if (currentState.custom(TokenMetaData.TYPE) == null) {
1408-
return ClusterState.builder(currentState).putCustom(TokenMetaData.TYPE, getTokenMetaData()).build();
1409-
} else {
1410-
return currentState;
1411-
}
1409+
if (currentState.custom(TokenMetaData.TYPE) == null) {
1410+
return ClusterState.builder(currentState).putCustom(TokenMetaData.TYPE, getTokenMetaData()).build();
1411+
} else {
1412+
return currentState;
14121413
}
1414+
}
14131415

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

1420-
@Override
1421-
public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
1422-
installTokenMetadataInProgress.set(false);
1423-
}
1424-
});
1425-
}
1422+
@Override
1423+
public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
1424+
installTokenMetadataInProgress.set(false);
1425+
}
1426+
});
14261427
}
14271428
}
14281429

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

Lines changed: 60 additions & 21 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.SecurityLifecycleService;
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 SecurityLifecycleService lifecycleService;
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(lifecycleService).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
@@ -173,7 +199,7 @@ public void testAttachAndGetToken() throws Exception {
173199
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
174200
tokenService.getAndValidateToken(requestContext, future);
175201
UserToken serialized = future.get();
176-
assertEquals(authentication, serialized.getAuthentication());
202+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
177203
}
178204

179205
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
@@ -184,13 +210,13 @@ public void testAttachAndGetToken() throws Exception {
184210
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
185211
anotherService.getAndValidateToken(requestContext, future);
186212
UserToken fromOtherService = future.get();
187-
assertEquals(authentication, fromOtherService.getAuthentication());
213+
assertAuthenticationEquals(authentication, fromOtherService.getAuthentication());
188214
}
189215
}
190216

191217
public void testRotateKey() throws Exception {
192-
TokenService tokenService =
193-
new TokenService(tokenServiceEnabledSettings, systemUTC(), client, lifecycleService, clusterService);
218+
assumeFalse("internally managed keys do not work in a mixed cluster", mixedCluster);
219+
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, lifecycleService, clusterService);
194220
Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null);
195221
PlainActionFuture<Tuple<UserToken, String>> tokenFuture = new PlainActionFuture<>();
196222
tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap());
@@ -205,15 +231,15 @@ public void testRotateKey() throws Exception {
205231
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
206232
tokenService.getAndValidateToken(requestContext, future);
207233
UserToken serialized = future.get();
208-
assertEquals(authentication, serialized.getAuthentication());
234+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
209235
}
210236
rotateKeys(tokenService);
211237

212238
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
213239
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
214240
tokenService.getAndValidateToken(requestContext, future);
215241
UserToken serialized = future.get();
216-
assertEquals(authentication, serialized.getAuthentication());
242+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
217243
}
218244

219245
PlainActionFuture<Tuple<UserToken, String>> newTokenFuture = new PlainActionFuture<>();
@@ -242,8 +268,8 @@ private void rotateKeys(TokenService tokenService) {
242268
}
243269

244270
public void testKeyExchange() throws Exception {
245-
TokenService tokenService =
246-
new TokenService(tokenServiceEnabledSettings, systemUTC(), client, lifecycleService, clusterService);
271+
assumeFalse("internally managed keys do not work in a mixed cluster", mixedCluster);
272+
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, lifecycleService, clusterService);
247273
int numRotations = 0;randomIntBetween(1, 5);
248274
for (int i = 0; i < numRotations; i++) {
249275
rotateKeys(tokenService);
@@ -264,7 +290,7 @@ public void testKeyExchange() throws Exception {
264290
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
265291
otherTokenService.getAndValidateToken(requestContext, future);
266292
UserToken serialized = future.get();
267-
assertEquals(authentication, serialized.getAuthentication());
293+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
268294
}
269295

270296
rotateKeys(tokenService);
@@ -275,13 +301,13 @@ public void testKeyExchange() throws Exception {
275301
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
276302
otherTokenService.getAndValidateToken(requestContext, future);
277303
UserToken serialized = future.get();
278-
assertEquals(authentication, serialized.getAuthentication());
304+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
279305
}
280306
}
281307

282308
public void testPruneKeys() throws Exception {
283-
TokenService tokenService =
284-
new TokenService(tokenServiceEnabledSettings, systemUTC(), client, lifecycleService, clusterService);
309+
assumeFalse("internally managed keys do not work in a mixed cluster", mixedCluster);
310+
TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, lifecycleService, clusterService);
285311
Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null);
286312
PlainActionFuture<Tuple<UserToken, String>> tokenFuture = new PlainActionFuture<>();
287313
tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap());
@@ -296,7 +322,7 @@ public void testPruneKeys() throws Exception {
296322
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
297323
tokenService.getAndValidateToken(requestContext, future);
298324
UserToken serialized = future.get();
299-
assertEquals(authentication, serialized.getAuthentication());
325+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
300326
}
301327
TokenMetaData metaData = tokenService.pruneKeys(randomIntBetween(0, 100));
302328
tokenService.refreshMetaData(metaData);
@@ -310,7 +336,7 @@ public void testPruneKeys() throws Exception {
310336
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
311337
tokenService.getAndValidateToken(requestContext, future);
312338
UserToken serialized = future.get();
313-
assertEquals(authentication, serialized.getAuthentication());
339+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
314340
}
315341

316342
PlainActionFuture<Tuple<UserToken, String>> newTokenFuture = new PlainActionFuture<>();
@@ -336,7 +362,7 @@ public void testPruneKeys() throws Exception {
336362
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
337363
tokenService.getAndValidateToken(requestContext, future);
338364
UserToken serialized = future.get();
339-
assertEquals(authentication, serialized.getAuthentication());
365+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
340366
}
341367

342368
}
@@ -358,7 +384,7 @@ public void testPassphraseWorks() throws Exception {
358384
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
359385
tokenService.getAndValidateToken(requestContext, future);
360386
UserToken serialized = future.get();
361-
assertEquals(authentication, serialized.getAuthentication());
387+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
362388
}
363389

364390
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
@@ -459,7 +485,7 @@ public void testTokenExpiry() throws Exception {
459485
// the clock is still frozen, so the cookie should be valid
460486
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
461487
tokenService.getAndValidateToken(requestContext, future);
462-
assertEquals(authentication, future.get().getAuthentication());
488+
assertAuthenticationEquals(authentication, future.get().getAuthentication());
463489
}
464490

465491
final TimeValue defaultExpiration = TokenService.TOKEN_EXPIRATION.get(Settings.EMPTY);
@@ -469,7 +495,7 @@ public void testTokenExpiry() throws Exception {
469495
clock.fastForwardSeconds(fastForwardAmount);
470496
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
471497
tokenService.getAndValidateToken(requestContext, future);
472-
assertEquals(authentication, future.get().getAuthentication());
498+
assertAuthenticationEquals(authentication, future.get().getAuthentication());
473499
}
474500

475501
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
@@ -478,7 +504,7 @@ public void testTokenExpiry() throws Exception {
478504
clock.rewind(TimeValue.timeValueNanos(clock.instant().getNano())); // trim off nanoseconds since don't store them in the index
479505
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
480506
tokenService.getAndValidateToken(requestContext, future);
481-
assertEquals(authentication, future.get().getAuthentication());
507+
assertAuthenticationEquals(authentication, future.get().getAuthentication());
482508
}
483509

484510
try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) {
@@ -574,7 +600,7 @@ public void testIndexNotAvailable() throws Exception {
574600
PlainActionFuture<UserToken> future = new PlainActionFuture<>();
575601
tokenService.getAndValidateToken(requestContext, future);
576602
UserToken serialized = future.get();
577-
assertEquals(authentication, serialized.getAuthentication());
603+
assertAuthenticationEquals(authentication, serialized.getAuthentication());
578604

579605
when(lifecycleService.isSecurityIndexAvailable()).thenReturn(false);
580606
when(lifecycleService.isSecurityIndexExisting()).thenReturn(true);
@@ -606,6 +632,7 @@ public void testDecodePre6xToken() throws GeneralSecurityException, ExecutionExc
606632
assertWarnings("[xpack.security.authc.token.passphrase] setting was deprecated in Elasticsearch and will be removed in a future" +
607633
" release! See the breaking changes documentation for the next major version.");
608634
}
635+
609636
public void testGetAuthenticationWorksWithExpiredToken() throws Exception {
610637
TokenService tokenService =
611638
new TokenService(tokenServiceEnabledSettings, Clock.systemUTC(), client, lifecycleService, clusterService);
@@ -616,7 +643,7 @@ public void testGetAuthenticationWorksWithExpiredToken() throws Exception {
616643
PlainActionFuture<Tuple<Authentication, Map<String, Object>>> authFuture = new PlainActionFuture<>();
617644
tokenService.getAuthenticationAndMetaData(userTokenString, authFuture);
618645
Authentication retrievedAuth = authFuture.actionGet().v1();
619-
assertEquals(authentication, retrievedAuth);
646+
assertAuthenticationEquals(authentication, retrievedAuth);
620647
}
621648

622649
private void mockGetTokenFromId(UserToken userToken) {
@@ -643,4 +670,16 @@ public static void mockGetTokenFromId(UserToken userToken, Client client) {
643670
return Void.TYPE;
644671
}).when(client).get(any(GetRequest.class), any(ActionListener.class));
645672
}
673+
674+
private void assertAuthenticationEquals(Authentication expected, Authentication actual) {
675+
if (mixedCluster) {
676+
assertNotNull(expected);
677+
assertNotNull(actual);
678+
assertEquals(expected.getUser(), actual.getUser());
679+
assertEquals(expected.getAuthenticatedBy(), actual.getAuthenticatedBy());
680+
assertEquals(expected.getLookedUpBy(), actual.getLookedUpBy());
681+
} else {
682+
assertEquals(expected, actual);
683+
}
684+
}
646685
}

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)