Skip to content
2 changes: 2 additions & 0 deletions config/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ config:
disabled_transport_categories:
- AUTHENTICATED
- GRANTED_PRIVILEGES
- CLUSTER_SETTINGS_CHANGED
- INDEX_SETTINGS_CHANGED

# Users to be excluded from auditing. Wildcard patterns are supported. Eg:
# ignore_users: ["test-user", "employee-*"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public interface AuditLog extends Closeable {
// index event requests
void logIndexEvent(String privilege, TransportRequest request, Task task);

// settings change events
void logSettingsChange(String action, TransportRequest request, Task task);

// spoof
void logBadHeaders(TransportRequest request, String action, Task task);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public void logIndexEvent(String privilege, TransportRequest request, Task task)
// noop, intentionally left empty
}

@Override
public void logSettingsChange(String action, TransportRequest request, Task task) {
// noop, intentionally left empty
}

@Override
public void logBadHeaders(TransportRequest request, String action, Task task) {
// noop, intentionally left empty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
* "enable_transport" : true,
* "disabled_transport_categories" : [
* "GRANTED_PRIVILEGES",
* "AUTHENTICATED"
* "AUTHENTICATED",
* "CLUSTER_SETTINGS_CHANGED",
* "INDEX_SETTINGS_CHANGED"
* ],
* "resolve_bulk_requests" : false,
* "log_request_body" : true,
Expand Down Expand Up @@ -246,14 +248,14 @@ public static Filter from(Map<String, Object> properties) {
getOrDefault(
properties,
FilterEntries.DISABLE_REST_CATEGORIES.getKey(),
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_REST_CATEGORIES_DEFAULT
)
);
final Set<AuditCategory> disabledTransportCategories = AuditCategory.parse(
getOrDefault(
properties,
FilterEntries.DISABLE_TRANSPORT_CATEGORIES.getKey(),
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_TRANSPORT_CATEGORIES_DEFAULT
)
);
final List<String> rawIgnoredUsers = getOrDefault(properties, FilterEntries.IGNORE_USERS.getKey(), DEFAULT_IGNORED_USERS);
Expand Down Expand Up @@ -300,14 +302,14 @@ public static Filter from(Settings settings) {
fromSettingStringSet(
settings,
FilterEntries.DISABLE_REST_CATEGORIES,
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_REST_CATEGORIES_DEFAULT
)
);
final Set<AuditCategory> disabledTransportCategories = AuditCategory.parse(
fromSettingStringSet(
settings,
FilterEntries.DISABLE_TRANSPORT_CATEGORIES,
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_TRANSPORT_CATEGORIES_DEFAULT
)
);
final Set<String> ignoredAuditUsers = fromSettingStringSet(settings, FilterEntries.IGNORE_USERS, DEFAULT_IGNORED_USERS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.opensearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest;
import org.opensearch.action.bulk.BulkRequest;
import org.opensearch.action.bulk.BulkShardRequest;
import org.opensearch.action.delete.DeleteRequest;
import org.opensearch.action.index.IndexRequest;
import org.opensearch.action.support.IndicesOptions;
import org.opensearch.action.update.UpdateRequest;
import org.opensearch.cluster.metadata.IndexNameExpressionResolver;
import org.opensearch.cluster.service.ClusterService;
Expand Down Expand Up @@ -325,6 +328,170 @@ public void logIndexEvent(String privilege, TransportRequest request, Task task)
msgs.forEach(this::save);
}

// Routes settings change audit to the appropriate handler
@Override
public void logSettingsChange(String action, TransportRequest request, Task task) {
if (request instanceof ClusterUpdateSettingsRequest) {
logClusterSettingsChange(action, (ClusterUpdateSettingsRequest) request, task);
} else if (request instanceof UpdateSettingsRequest) {
logIndexSettingsChange(action, (UpdateSettingsRequest) request, task);
}
}

// Audits cluster settings changes (persistent and transient)
private void logClusterSettingsChange(String action, ClusterUpdateSettingsRequest request, Task task) {
if (!checkTransportFilter(AuditCategory.CLUSTER_SETTINGS_CHANGED, action, getUser(), request)) {
return;
}

final List<Map<String, Object>> changes = new ArrayList<>();

final Settings persistentSettings = request.persistentSettings();
if (!persistentSettings.isEmpty()) {
Settings currentPersistent = Settings.EMPTY;
try {
currentPersistent = clusterService.state().metadata().persistentSettings();
} catch (final Exception e) {
log.debug("Unable to retrieve current persistent settings for audit", e);
}
changes.addAll(buildSettingsChanges(persistentSettings, currentPersistent, "persistent"));
}

final Settings transientSettings = request.transientSettings();
if (!transientSettings.isEmpty()) {
Settings currentTransient = Settings.EMPTY;
try {
currentTransient = clusterService.state().metadata().transientSettings();
} catch (final Exception e) {
log.debug("Unable to retrieve current transient settings for audit", e);
}
changes.addAll(buildSettingsChanges(transientSettings, currentTransient, "transient"));
}

if (changes.isEmpty()) {
return;
}

final AuditMessage msg = new AuditMessage(AuditCategory.CLUSTER_SETTINGS_CHANGED, clusterService, getOrigin(), Origin.TRANSPORT);
TransportAddress remoteAddress = getRemoteAddress();
msg.addRemoteAddress(remoteAddress);
msg.addEffectiveUser(getUser());
msg.addAction(action);
msg.addRequestType(request.getClass().getSimpleName());
msg.addSettingsChanges(changes);

if (task != null) {
msg.addTaskId(task.getId());
}

save(msg);
}

// Audits index-level settings changes
private void logIndexSettingsChange(String action, UpdateSettingsRequest request, Task task) {
if (!checkTransportFilter(AuditCategory.INDEX_SETTINGS_CHANGED, action, getUser(), request)) {
return;
}

final Settings newSettings = request.settings();
final String[] indices = request.indices();
final String[] resolvedIndices = resolveIndices(indices);

// Use settings from the first resolved index as representative current state
Settings currentSettings = Settings.EMPTY;
if (resolvedIndices.length > 0) {
try {
final var indexMetadata = clusterService.state().metadata().index(resolvedIndices[0]);
if (indexMetadata != null) {
currentSettings = indexMetadata.getSettings();
}
} catch (final Exception e) {
log.debug("Unable to retrieve current index settings for audit", e);
}
}

final List<Map<String, Object>> changes = buildSettingsChanges(newSettings, currentSettings, "index");
if (changes.isEmpty()) {
return;
}

final AuditMessage msg = new AuditMessage(AuditCategory.INDEX_SETTINGS_CHANGED, clusterService, getOrigin(), Origin.TRANSPORT);
TransportAddress remoteAddress = getRemoteAddress();
msg.addRemoteAddress(remoteAddress);
msg.addEffectiveUser(getUser());
msg.addAction(action);
msg.addRequestType(request.getClass().getSimpleName());
msg.addIndices(indices);
msg.addResolvedIndices(resolvedIndices);
msg.addSettingsChanges(changes);

if (task != null) {
msg.addTaskId(task.getId());
}

save(msg);
}

// Compares old vs new values for each setting and builds change entries
private List<Map<String, Object>> buildSettingsChanges(Settings newSettings, Settings currentSettings, String scope) {
final List<Map<String, Object>> changes = new ArrayList<>();
for (String key : newSettings.keySet()) {
final String newValue = newSettings.get(key);
final String oldValue = currentSettings.get(key);
final boolean isSensitive = isSensitiveSetting(key);

final Map<String, Object> change = new HashMap<>();
change.put("setting", key);
change.put("old_value", isSensitive && oldValue != null ? "***REDACTED***" : oldValue);
change.put("scope", scope);

if (newValue == null) {
change.put("new_value", null);
change.put("operation", "removed");
} else {
change.put("new_value", isSensitive ? "***REDACTED***" : newValue);
change.put("operation", "set");
}

changes.add(change);
}
return changes;
}

/**
* Checks if a setting should have its value redacted in audit logs.
* Uses ClusterSettings.isSensitiveSetting() to detect SecureSetting instances (e.g., keystore passwords,
* TLS keys). Falls back to pattern matching for plugin-specific settings
* that may not be registered in the cluster settings registry.
*/
private boolean isSensitiveSetting(String key) {
try {
// Looks up the Setting object by key and checks setting.isSensitive(),
// which returns true for SecureSetting instances — the proper way to identify secrets
if (clusterService.getClusterSettings().isSensitiveSetting(key)) {
return true;
}
} catch (Exception e) {
// Setting not registered in cluster settings — fall through to pattern match
}
// Pattern fallback for settings not registered as SecureSetting (e.g., plugin SSL settings)
final String lowerKey = key.toLowerCase();
return lowerKey.contains("password") || lowerKey.contains("secret") || lowerKey.contains("token");
}

// Resolves index patterns to concrete index names
private String[] resolveIndices(String[] indices) {
if (indices == null || indices.length == 0) {
return new String[0];
}
try {
return resolver.concreteIndexNames(clusterService.state(), IndicesOptions.lenientExpandOpen(), indices);
} catch (Exception e) {
log.debug("Unable to resolve indices for settings change audit", e);
return indices;
}
}

@Override
public void logBadHeaders(TransportRequest request, String action, Task task) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public enum AuditCategory {
COMPLIANCE_DOC_WRITE,
COMPLIANCE_EXTERNAL_CONFIG,
COMPLIANCE_INTERNAL_CONFIG_READ,
COMPLIANCE_INTERNAL_CONFIG_WRITE;
COMPLIANCE_INTERNAL_CONFIG_WRITE,
CLUSTER_SETTINGS_CHANGED,
INDEX_SETTINGS_CHANGED;

public static Set<AuditCategory> parse(final Collection<String> categories) {
if (categories.isEmpty()) return Collections.emptySet();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ public void logIndexEvent(String privilege, TransportRequest request, Task task)
}
}

@Override
public void logSettingsChange(String action, TransportRequest request, Task task) {
if (enabled) {
super.logSettingsChange(action, request, task);
}
}

@Override
public void logBadHeaders(TransportRequest request, String action, Task task) {
if (enabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ public final class AuditMessage {
public static final String COMPLIANCE_OPERATION = "audit_compliance_operation";
public static final String COMPLIANCE_DOC_VERSION = "audit_compliance_doc_version";

public static final String SETTINGS_CHANGES = "audit_settings_changes";

public static final String SPLIT_MESSAGE_IDENTIFIER = "audit_split_message_id";

private static final DateTimeFormatter DEFAULT_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
Expand Down Expand Up @@ -460,6 +462,12 @@ public void addComplianceDocVersion(long version) {
auditInfo.put(COMPLIANCE_DOC_VERSION, version);
}

public void addSettingsChanges(List<Map<String, Object>> changes) {
if (changes != null && !changes.isEmpty()) {
auditInfo.put(SETTINGS_CHANGES, changes);
}
}

public Map<String, Object> getAsMap() {
return new HashMap<>(this.auditInfo);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ public static class AuditRequestContentValidator extends RequestContentValidator
AuditCategory.GRANTED_PRIVILEGES,
AuditCategory.MISSING_PRIVILEGES,
AuditCategory.INDEX_EVENT,
AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT
AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT,
AuditCategory.CLUSTER_SETTINGS_CHANGED,
AuditCategory.INDEX_SETTINGS_CHANGED
);

protected AuditRequestContentValidator(ValidationContext validationContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ private <Request extends ActionRequest, Response extends ActionResponse> void ap
if (userIsAdmin && !confRequest && !internalRequest && !passThroughRequest) {
auditLog.logGrantedPrivileges(action, request, task);
auditLog.logIndexEvent(action, request, task);
auditLog.logSettingsChange(action, request, task);
}

chain.proceed(task, action, request, listener);
Expand Down Expand Up @@ -424,6 +425,7 @@ private <Request extends ActionRequest, Response extends ActionResponse> void ap
if (response.isAllowed()) {
auditLog.logGrantedPrivileges(action, request, task);
auditLog.logIndexEvent(action, request, task);
auditLog.logSettingsChange(action, request, task);
chain.proceed(task, action, request, listener);
} else {
handleUnauthorized.accept(response);
Expand Down Expand Up @@ -452,6 +454,7 @@ private <Request extends ActionRequest, Response extends ActionResponse> void ap
if (pres.isAllowed()) {
auditLog.logGrantedPrivileges(action, request, task);
auditLog.logIndexEvent(action, request, task);
auditLog.logSettingsChange(action, request, task);
if (!dlsFlsValve.invoke(context, listener)) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,16 @@ public class ConfigConstants {
"opendistro_security.audit.config.disabled_transport_categories";
public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES =
"opendistro_security.audit.config.disabled_rest_categories";
public static final List<String> OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT = ImmutableList.of(
public static final List<String> OPENDISTRO_SECURITY_AUDIT_DISABLED_REST_CATEGORIES_DEFAULT = ImmutableList.of(
AuditCategory.AUTHENTICATED.toString(),
AuditCategory.GRANTED_PRIVILEGES.toString()
);
public static final List<String> OPENDISTRO_SECURITY_AUDIT_DISABLED_TRANSPORT_CATEGORIES_DEFAULT = ImmutableList.of(
AuditCategory.AUTHENTICATED.toString(),
AuditCategory.GRANTED_PRIVILEGES.toString(),
AuditCategory.CLUSTER_SETTINGS_CHANGED.toString(),
AuditCategory.INDEX_SETTINGS_CHANGED.toString()
);
public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS = "opendistro_security.audit.ignore_users";
public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS = "opendistro_security.audit.ignore_requests";
public static final String SECURITY_AUDIT_IGNORE_HEADERS = SECURITY_SETTINGS_PREFIX + "audit.ignore_headers";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ public class AuditConfigFilterTest {
public void testDefault() {
// arrange
final WildcardMatcher defaultIgnoredUserMatcher = WildcardMatcher.from("kibanaserver");
final EnumSet<AuditCategory> defaultDisabledCategories = EnumSet.of(AUTHENTICATED, GRANTED_PRIVILEGES);
final EnumSet<AuditCategory> defaultDisabledRestCategories = EnumSet.of(AUTHENTICATED, GRANTED_PRIVILEGES);
final EnumSet<AuditCategory> defaultDisabledTransportCategories = EnumSet.of(
AUTHENTICATED,
GRANTED_PRIVILEGES,
AuditCategory.CLUSTER_SETTINGS_CHANGED,
AuditCategory.INDEX_SETTINGS_CHANGED
);
// act
final AuditConfig.Filter auditConfigFilter = AuditConfig.Filter.from(Settings.EMPTY);
// assert
Expand All @@ -60,8 +66,8 @@ public void testDefault() {
assertSame(WildcardMatcher.NONE, auditConfigFilter.getIgnoredAuditRequestsMatcher());
assertThat(auditConfigFilter.getIgnoredAuditUsersMatcher(), is(defaultIgnoredUserMatcher));
assertSame(WildcardMatcher.NONE, auditConfigFilter.getIgnoredCustomHeadersMatcher());
assertThat(defaultDisabledCategories, is(auditConfigFilter.getDisabledRestCategories()));
assertThat(defaultDisabledCategories, is(auditConfigFilter.getDisabledTransportCategories()));
assertThat(defaultDisabledRestCategories, is(auditConfigFilter.getDisabledRestCategories()));
assertThat(defaultDisabledTransportCategories, is(auditConfigFilter.getDisabledTransportCategories()));
}

@Test
Expand Down Expand Up @@ -205,7 +211,11 @@ public void fromSettingStringSet() {
public void fromSettingParseAuditCategory() {
final FilterEntries entry = FilterEntries.DISABLE_REST_CATEGORIES;
final Function<Settings, Set<AuditCategory>> parse = (settings) -> AuditCategory.parse(
AuditConfig.Filter.fromSettingStringSet(settings, entry, ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT)
AuditConfig.Filter.fromSettingStringSet(
settings,
entry,
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_REST_CATEGORIES_DEFAULT
)
);

final Settings noValues = Settings.builder().build();
Expand Down
Loading
Loading