Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,14 +35,18 @@
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;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.settings.SecureSetting;
import org.opensearch.common.settings.Settings;
import org.opensearch.common.xcontent.XContentHelper;
import org.opensearch.common.xcontent.XContentType;
Expand Down Expand Up @@ -333,6 +337,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 isSecure = isSecureSetting(key);

final Map<String, Object> change = new HashMap<>();
change.put("setting", key);
change.put("old_value", isSecure && 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", isSecure ? "***REDACTED***" : newValue);
change.put("operation", "set");
}

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

/**
* Checks if a setting should have its value redacted in audit logs.
* Returns true for settings registered as SecureSetting (secrets stored
* in the keystore), or settings whose key contains common secret indicators
* (password, secret, token).
*/
private boolean isSecureSetting(final String key) {
try {
// Looks up the Setting object by key and checks if it is an instance of SecureSetting
// i.e. secrets stored in the opensearch-keystore that must be redacted
if (clusterService.getClusterSettings().get(key) instanceof SecureSetting) {
return true;
}
} catch (final 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 @@ -31,6 +31,8 @@ public enum AuditCategory {
COMPLIANCE_EXTERNAL_CONFIG,
COMPLIANCE_INTERNAL_CONFIG_READ,
COMPLIANCE_INTERNAL_CONFIG_WRITE,
CLUSTER_SETTINGS_CHANGED,
INDEX_SETTINGS_CHANGED,
API_TOKEN_WRITE;

public static Set<AuditCategory> parse(final Collection<String> categories) {
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
Loading
Loading