Skip to content

Commit 6f553ab

Browse files
Fix IndexAuditTrail rolling upgrade on rollover edge 2 (#38286)
Fixes a race during the rolling upgrade with the index audit output enabled. The race is that after the upgraded node is restarted, it installs the audit template and updates the mapping of the "current" (from his perspective) audit index. But the template might be installed after a new daily rolled-over index has been created by the other old nodes, using the old templates. However, the new node, even if it installs the template after the rollover edge, can accumulate audit events before the edge, and will correctly try to update the mapping of the audit index before the edge. But this way, the mapping of the index after the edge remains un-updated, because only the master node does the mapping updates. The fix keeps the design of only allowing the master to update the mapping, but the master will try, on a best effort policy, to also possibly update the mapping of the next rollover audit index.
1 parent 1ff3a96 commit 6f553ab

File tree

4 files changed

+70
-31
lines changed

4 files changed

+70
-31
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
import static org.elasticsearch.xpack.security.audit.AuditUtil.indices;
110110
import static org.elasticsearch.xpack.security.audit.AuditUtil.restRequestContent;
111111
import static org.elasticsearch.xpack.security.audit.index.IndexNameResolver.resolve;
112+
import static org.elasticsearch.xpack.security.audit.index.IndexNameResolver.resolveNext;
112113
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_VERSION_STRING;
113114

114115
/**
@@ -308,6 +309,17 @@ private String getIndexName() {
308309
return index;
309310
}
310311

312+
private String getNextIndexName() {
313+
final Message first = peek();
314+
final String index;
315+
if (first == null) {
316+
index = resolveNext(IndexAuditTrailField.INDEX_NAME_PREFIX, DateTime.now(DateTimeZone.UTC), rollover);
317+
} else {
318+
index = resolveNext(IndexAuditTrailField.INDEX_NAME_PREFIX, first.timestamp, rollover);
319+
}
320+
return index;
321+
}
322+
311323
private boolean hasStaleMessage() {
312324
final Message first = peek();
313325
if (first == null) {
@@ -337,7 +349,7 @@ public void onResponse(ClusterStateResponse clusterStateResponse) {
337349
updateCurrentIndexMappingsIfNecessary(clusterStateResponse.getState());
338350
} else if (TemplateUtils.checkTemplateExistsAndVersionMatches(INDEX_TEMPLATE_NAME,
339351
SECURITY_VERSION_STRING, clusterStateResponse.getState(), logger,
340-
Version.CURRENT::onOrAfter) == false) {
352+
Version.CURRENT::onOrBefore) == false) {
341353
putTemplate(customAuditIndexSettings(settings, logger),
342354
e -> {
343355
logger.error("failed to put audit trail template", e);
@@ -377,6 +389,7 @@ public void onFailure(Exception e) {
377389

378390
// pkg private for tests
379391
void updateCurrentIndexMappingsIfNecessary(ClusterState state) {
392+
final String nextIndex = getNextIndexName();
380393
final String index = getIndexName();
381394

382395
AliasOrIndex aliasOrIndex = state.getMetaData().getAliasAndIndexLookup().get(index);
@@ -391,48 +404,60 @@ void updateCurrentIndexMappingsIfNecessary(ClusterState state) {
391404
MappingMetaData docMapping = indexMetaData.mapping("doc");
392405
if (docMapping == null) {
393406
if (indexToRemoteCluster || state.nodes().isLocalNodeElectedMaster() || hasStaleMessage()) {
394-
putAuditIndexMappingsAndStart(index);
407+
putAuditIndexMappingsAndStart(index, nextIndex);
395408
} else {
396-
logger.trace("audit index [{}] is missing mapping for type [{}]", index, DOC_TYPE);
409+
logger.debug("audit index [{}] is missing mapping for type [{}]", index, DOC_TYPE);
397410
transitionStartingToInitialized();
398411
}
399412
} else {
400413
@SuppressWarnings("unchecked")
401414
Map<String, Object> meta = (Map<String, Object>) docMapping.sourceAsMap().get("_meta");
402415
if (meta == null) {
403-
logger.info("Missing _meta field in mapping [{}] of index [{}]", docMapping.type(), index);
404-
throw new IllegalStateException("Cannot read security-version string in index " + index);
405-
}
406-
407-
final String versionString = (String) meta.get(SECURITY_VERSION_STRING);
408-
if (versionString != null && Version.fromString(versionString).onOrAfter(Version.CURRENT)) {
409-
innerStart();
410-
} else {
416+
logger.warn("Missing _meta field in mapping [{}] of index [{}]", docMapping.type(), index);
411417
if (indexToRemoteCluster || state.nodes().isLocalNodeElectedMaster() || hasStaleMessage()) {
412-
putAuditIndexMappingsAndStart(index);
413-
} else if (versionString == null) {
414-
logger.trace("audit index [{}] mapping is missing meta field [{}]", index, SECURITY_VERSION_STRING);
415-
transitionStartingToInitialized();
418+
putAuditIndexMappingsAndStart(index, nextIndex);
416419
} else {
417-
logger.trace("audit index [{}] has the incorrect version [{}]", index, versionString);
420+
logger.debug("audit index [{}] is missing _meta for type [{}]", index, DOC_TYPE);
418421
transitionStartingToInitialized();
419422
}
423+
} else {
424+
final String versionString = (String) meta.get(SECURITY_VERSION_STRING);
425+
if (versionString != null && Version.fromString(versionString).onOrAfter(Version.CURRENT)) {
426+
innerStart();
427+
} else {
428+
if (indexToRemoteCluster || state.nodes().isLocalNodeElectedMaster() || hasStaleMessage()) {
429+
putAuditIndexMappingsAndStart(index, nextIndex);
430+
} else if (versionString == null) {
431+
logger.debug("audit index [{}] mapping is missing meta field [{}]", index, SECURITY_VERSION_STRING);
432+
transitionStartingToInitialized();
433+
} else {
434+
logger.debug("audit index [{}] has the incorrect version [{}]", index, versionString);
435+
transitionStartingToInitialized();
436+
}
437+
}
420438
}
421439
}
422440
} else {
423441
innerStart();
424442
}
425443
}
426444

427-
private void putAuditIndexMappingsAndStart(String index) {
428-
putAuditIndexMappings(index, getPutIndexTemplateRequest(Settings.EMPTY).mappings().get(DOC_TYPE),
429-
ActionListener.wrap(ignore -> {
430-
logger.trace("updated mappings on audit index [{}]", index);
445+
private void putAuditIndexMappingsAndStart(String index, String nextIndex) {
446+
final String docMapping = getPutIndexTemplateRequest(Settings.EMPTY).mappings().get(DOC_TYPE);
447+
putAuditIndexMappings(index, docMapping, ActionListener.wrap(ignore -> {
448+
logger.debug("updated mappings on audit index [{}]", index);
449+
putAuditIndexMappings(nextIndex, docMapping, ActionListener.wrap(ignoreToo -> {
450+
logger.debug("updated mappings on next audit index [{}]", nextIndex);
451+
innerStart();
452+
}, e2 -> {
453+
// best effort only
454+
logger.debug("Failed to update mappings on next audit index [{}]", nextIndex);
431455
innerStart();
432-
}, e -> {
433-
logger.error(new ParameterizedMessage("failed to update mappings on audit index [{}]", index), e);
434-
transitionStartingToInitialized(); // reset to initialized so we can retry
435456
}));
457+
}, e -> {
458+
logger.error(new ParameterizedMessage("failed to update mappings on audit index [{}]", index), e);
459+
transitionStartingToInitialized(); // reset to initialized so we can retry
460+
}));
436461
}
437462

438463
private void transitionStartingToInitialized() {
@@ -451,7 +476,7 @@ void innerStart() {
451476
assert false : message;
452477
logger.error(message);
453478
} else {
454-
logger.trace("successful state transition from starting to started, current value: [{}]", state.get());
479+
logger.debug("successful state transition from starting to started, current value: [{}]", state.get());
455480
}
456481
}
457482

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexNameResolver.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,31 @@
99
import org.joda.time.format.DateTimeFormat;
1010
import org.joda.time.format.DateTimeFormatter;
1111

12+
import java.util.function.Function;
13+
1214
public class IndexNameResolver {
1315

1416
public enum Rollover {
15-
HOURLY ("-yyyy.MM.dd.HH"),
16-
DAILY ("-yyyy.MM.dd"),
17-
WEEKLY ("-yyyy.w"),
18-
MONTHLY ("-yyyy.MM");
17+
HOURLY ("-yyyy.MM.dd.HH", d -> d.plusHours(1)),
18+
DAILY ("-yyyy.MM.dd", d -> d.plusDays(1)),
19+
WEEKLY ("-yyyy.w", d -> d.plusWeeks(1)),
20+
MONTHLY ("-yyyy.MM", d -> d.plusMonths(1));
1921

2022
private final DateTimeFormatter formatter;
23+
private final Function<DateTime, DateTime> next;
2124

22-
Rollover(String format) {
25+
Rollover(String format, Function<DateTime, DateTime> next) {
2326
this.formatter = DateTimeFormat.forPattern(format);
27+
this.next = next;
2428
}
2529

2630
DateTimeFormatter formatter() {
2731
return formatter;
2832
}
33+
34+
Function<DateTime, DateTime> getNext() {
35+
return next;
36+
}
2937
}
3038

3139
private IndexNameResolver() {}
@@ -34,6 +42,10 @@ public static String resolve(DateTime timestamp, Rollover rollover) {
3442
return rollover.formatter().print(timestamp);
3543
}
3644

45+
public static String resolveNext(String indexNamePrefix, DateTime timestamp, Rollover rollover) {
46+
return resolve(indexNamePrefix, rollover.getNext().apply(timestamp), rollover);
47+
}
48+
3749
public static String resolve(String indexNamePrefix, DateTime timestamp, Rollover rollover) {
3850
return indexNamePrefix + resolve(timestamp, rollover);
3951
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ subprojects {
157157
setting 'xpack.security.audit.outputs', 'index'
158158
setting 'xpack.security.transport.ssl.keystore.path', 'testnode.jks'
159159
setting 'xpack.security.transport.ssl.keystore.password', 'testnode'
160+
setting 'logger.org.elasticsearch.xpack.security.audit.index', 'DEBUG'
160161
if (version.onOrAfter('6.0.0') == false) {
161162
// this is needed since in 5.6 we don't bootstrap the token service if there is no explicit initial password
162163
keystoreSetting 'xpack.security.authc.token.passphrase', 'xpack_token_passphrase'
@@ -217,6 +218,7 @@ subprojects {
217218
setting 'xpack.security.enabled', 'true'
218219
setting 'xpack.security.transport.ssl.enabled', 'true'
219220
setting 'xpack.security.transport.ssl.keystore.path', 'testnode.jks'
221+
setting 'logger.org.elasticsearch.xpack.security.audit.index', 'DEBUG'
220222
keystoreSetting 'xpack.security.transport.ssl.keystore.secure_password', 'testnode'
221223
if (version.onOrAfter('6.0.0') == false) {
222224
// this is needed since in 5.6 we don't bootstrap the token service if there is no explicit initial password

x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.Collections;
1818
import java.util.List;
1919
import java.util.Map;
20+
import java.util.concurrent.TimeUnit;
2021

2122
import static org.hamcrest.Matchers.hasSize;
2223

@@ -62,12 +63,11 @@ public void findMinVersionInCluster() throws IOException {
6263
}
6364
}
6465

65-
@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33867")
6666
public void testAuditLogs() throws Exception {
6767
assertBusy(() -> {
6868
assertAuditDocsExist();
6969
assertNumUniqueNodeNameBuckets(expectedNumUniqueNodeNameBuckets());
70-
});
70+
}, 30, TimeUnit.SECONDS);
7171
}
7272

7373
private int expectedNumUniqueNodeNameBuckets() throws IOException {

0 commit comments

Comments
 (0)