Pārlūkot izejas kodu

Add an API for managing the settings of Security system indices (#97630)

This PR adds an API similar to #95342 for managing settings
of system indices.

Example calls:

```
GET /_security/settings

PUT /_security/settings
{
    "security": {
        "index.auto_expand_replicas": "0-all"
    },
    "security_tokens": {
            "index.auto_expand_replicas": "0-all"
    },
    "security_profile": {
            "index.auto_expand_replicas": "0-all"
    }
}
```
Athena Brown 2 gadi atpakaļ
vecāks
revīzija
2ffef2f658
20 mainītis faili ar 1100 papildinājumiem un 5 dzēšanām
  1. 5 0
      docs/changelog/97630.yaml
  2. 23 0
      rest-api-spec/src/main/resources/rest-api-spec/api/security.get_settings.json
  3. 27 0
      rest-api-spec/src/main/resources/rest-api-spec/api/security.update_settings.json
  4. 27 0
      x-pack/docs/en/rest-api/security/get-settings.asciidoc
  5. 50 0
      x-pack/docs/en/rest-api/security/update-settings.asciidoc
  6. 1 0
      x-pack/plugin/core/src/main/java/module-info.java
  7. 96 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/settings/GetSecuritySettingsAction.java
  8. 140 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/settings/UpdateSecuritySettingsAction.java
  9. 3 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java
  10. 126 0
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/settings/UpdateSecuritySettingsActionTests.java
  11. 6 2
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java
  12. 2 0
      x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java
  13. 101 0
      x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/SecuritySettingsIT.java
  14. 2 1
      x-pack/plugin/security/src/main/java/module-info.java
  15. 11 1
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
  16. 125 0
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/settings/TransportGetSecuritySettingsAction.java
  17. 168 0
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/settings/TransportUpdateSecuritySettingsAction.java
  18. 42 0
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/settings/RestGetSecuritySettingsAction.java
  19. 42 0
      x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/settings/RestUpdateSecuritySettingsAction.java
  20. 103 0
      x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/security/settings/10_update_security_settings.yml

+ 5 - 0
docs/changelog/97630.yaml

@@ -0,0 +1,5 @@
+pr: 97630
+summary: Add an API for managing the settings of Security system indices
+area: Security
+type: enhancement
+issues: []

+ 23 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/security.get_settings.json

@@ -0,0 +1,23 @@
+{
+  "security.get_settings":{
+    "documentation":{
+      "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-settings.html",
+      "description":"Retrieve settings for the security system indices"
+    },
+    "stability":"stable",
+    "visibility":"public",
+    "headers":{
+      "accept": [ "application/json"],
+      "content_type": ["application/json"]
+    },
+    "url":{
+      "paths":[
+        {
+          "path":"/_security/settings",
+          "methods":["GET"]
+        }
+      ]
+    },
+    "params":{}
+  }
+}

+ 27 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/security.update_settings.json

@@ -0,0 +1,27 @@
+{
+  "security.update_settings":{
+    "documentation":{
+      "url":"https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-update-settings.html",
+      "description":"Update settings for the security system index"
+    },
+    "stability":"stable",
+    "visibility":"public",
+    "headers":{
+      "accept": [ "application/json"],
+      "content_type": ["application/json"]
+    },
+    "url":{
+      "paths":[
+        {
+          "path":"/_security/settings",
+          "methods":["PUT"]
+        }
+      ]
+    },
+    "params":{},
+    "body":{
+      "description": "An object with the new settings for each index, if any",
+      "required": true
+    }
+  }
+}

+ 27 - 0
x-pack/docs/en/rest-api/security/get-settings.asciidoc

@@ -0,0 +1,27 @@
+[role="xpack"]
+[[security-api-get-settings]]
+=== Get Security index settings
+++++
+<titleabbrev>Get Security settings</titleabbrev>
+++++
+
+==== {api-prereq-title}
+
+* To use this API, you must have at least the `read_security` cluster privilege.
+
+==== {api-description-title}
+This API allows a user to retrieve the user-configurable settings for the Security internal index (`.security` and associated indices). Only a subset of the index settings — those that are user-configurable—will be shown. This includes:
+
+- `index.auto_expand_replicas`
+- `index.number_of_replicas`
+
+An example of retrieving the Security settings:
+
+[source,console]
+-----------------------------------------------------------
+GET /_security/settings
+-----------------------------------------------------------
+// TEST[setup:user_profiles]
+// TEST[setup:service_token42]
+
+The configurable settings can be modified using the <<security-api-update-settings,Update Security index settings>> API.

+ 50 - 0
x-pack/docs/en/rest-api/security/update-settings.asciidoc

@@ -0,0 +1,50 @@
+[role="xpack"]
+[[security-api-update-settings]]
+=== Update Security index settings
+++++
+<titleabbrev>Update Security settings</titleabbrev>
+++++
+
+==== {api-prereq-title}
+
+* To use this API, you must have at least the `manage_security` cluster privilege.
+
+==== {api-description-title}
+This API allows a user to modify the settings for the Security internal indices (`.security` and associated indices). Only a subset of settings are allowed to be modified. This includes:
+
+- `index.auto_expand_replicas`
+- `index.number_of_replicas`
+
+An example of modifying the Security settings:
+
+[source,console]
+-----------------------------------------------------------
+PUT /_security/settings
+{
+    "security": {
+        "index.auto_expand_replicas": "0-all"
+    },
+    "security-tokens": {
+        "index.auto_expand_replicas": "0-all"
+    },
+    "security-profile": {
+        "index.auto_expand_replicas": "0-all"
+    }
+}
+-----------------------------------------------------------
+// TEST[skip:making sure all the indices have been created reliably is difficult]
+
+The configured settings can be retrieved using the <<security-api-get-settings,Get Security index settings>> API. If a
+given index is not in use on the system, but settings are provided for it, the request will be rejected - this API does
+not yet support configuring the settings for these indices before they are in use.
+
+==== {api-request-body-title}
+`security`::
+(Optional, object) Settings to be used for the index used for most security configuration, including Native realm users
+and roles configured via the API.
+
+`security-tokens`::
+(Optional, object) Settings to be used for the index used to store <<tokens,security-api-get-token>>.
+
+`security`::
+(Optional, object) Settings to be used for the index used to store <<Profile,security-api-activate-user-profile>> information.

+ 1 - 0
x-pack/plugin/core/src/main/java/module-info.java

@@ -141,6 +141,7 @@ module org.elasticsearch.xcore {
     exports org.elasticsearch.xpack.core.security.action.service;
     exports org.elasticsearch.xpack.core.security.action.token;
     exports org.elasticsearch.xpack.core.security.action.user;
+    exports org.elasticsearch.xpack.core.security.action.settings;
     exports org.elasticsearch.xpack.core.security.action;
     exports org.elasticsearch.xpack.core.security.authc.esnative;
     exports org.elasticsearch.xpack.core.security.authc.file;

+ 96 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/settings/GetSecuritySettingsAction.java

@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.security.action.settings;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.support.master.MasterNodeReadRequest;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.xcontent.ToXContentObject;
+import org.elasticsearch.xcontent.XContentBuilder;
+
+import java.io.IOException;
+
+import static org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction.MAIN_INDEX_NAME;
+import static org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction.PROFILES_INDEX_NAME;
+import static org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction.TOKENS_INDEX_NAME;
+
+public class GetSecuritySettingsAction extends ActionType<GetSecuritySettingsAction.Response> {
+
+    public static final GetSecuritySettingsAction INSTANCE = new GetSecuritySettingsAction();
+    public static final String NAME = "cluster:admin/xpack/security/settings/get";
+
+    public GetSecuritySettingsAction() {
+        super(NAME, GetSecuritySettingsAction.Response::new);
+    }
+
+    public static class Request extends MasterNodeReadRequest<GetSecuritySettingsAction.Request> {
+
+        public Request() {}
+
+        public Request(StreamInput in) throws IOException {}
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {}
+
+        @Override
+        public ActionRequestValidationException validate() {
+            return null;
+        }
+    }
+
+    public static class Response extends ActionResponse implements ToXContentObject {
+        private final Settings mainIndexSettings;
+        private final Settings tokensIndexSettings;
+        private final Settings profilesIndexSettings;
+
+        public Response(Settings mainIndexSettings, Settings tokensIndexSettings, Settings profilesIndexSettings) {
+            this.mainIndexSettings = mainIndexSettings;
+            this.tokensIndexSettings = tokensIndexSettings;
+            this.profilesIndexSettings = profilesIndexSettings;
+        }
+
+        public Response(StreamInput in) throws IOException {
+            this.mainIndexSettings = Settings.readSettingsFromStream(in);
+            this.tokensIndexSettings = Settings.readSettingsFromStream(in);
+            this.profilesIndexSettings = Settings.readSettingsFromStream(in);
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            this.mainIndexSettings.writeTo(out);
+            this.tokensIndexSettings.writeTo(out);
+            this.profilesIndexSettings.writeTo(out);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.startObject(MAIN_INDEX_NAME);
+            {
+                this.mainIndexSettings.toXContent(builder, params);
+            }
+            builder.endObject();
+            builder.startObject(TOKENS_INDEX_NAME);
+            {
+                this.tokensIndexSettings.toXContent(builder, params);
+            }
+            builder.endObject();
+            builder.startObject(PROFILES_INDEX_NAME);
+            {
+                this.profilesIndexSettings.toXContent(builder, params);
+            }
+            builder.endObject();
+            builder.endObject();
+            return builder;
+        }
+    }
+}

+ 140 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/settings/UpdateSecuritySettingsAction.java

@@ -0,0 +1,140 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.security.action.settings;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.ValidateActions;
+import org.elasticsearch.action.support.master.AcknowledgedRequest;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.util.set.Sets;
+import org.elasticsearch.xcontent.ConstructingObjectParser;
+import org.elasticsearch.xcontent.ParseField;
+import org.elasticsearch.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+public class UpdateSecuritySettingsAction extends ActionType<AcknowledgedResponse> {
+    public static final UpdateSecuritySettingsAction INSTANCE = new UpdateSecuritySettingsAction();
+    public static final String NAME = "cluster:admin/xpack/security/settings/update";
+
+    // The names here are separate constants for 2 reasons:
+    // 1. Keeping the names defined here helps ensure REST compatibility, even if the internal aliases of these indices change,
+    // 2. The actual constants for these indices are in the security package, whereas this class is in core
+    public static final String MAIN_INDEX_NAME = "security";
+    public static final String TOKENS_INDEX_NAME = "security-tokens";
+    public static final String PROFILES_INDEX_NAME = "security-profile";
+
+    public static final Set<String> ALLOWED_SETTING_KEYS = Set.of(
+        IndexMetadata.SETTING_NUMBER_OF_REPLICAS,
+        IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS
+    );
+
+    public UpdateSecuritySettingsAction() {
+        super(NAME, AcknowledgedResponse::readFrom);
+    }
+
+    public static class Request extends AcknowledgedRequest<Request> {
+
+        private final Map<String, Object> mainIndexSettings;
+        private final Map<String, Object> tokensIndexSettings;
+        private final Map<String, Object> profilesIndexSettings;
+
+        @SuppressWarnings("unchecked")
+        private static final ConstructingObjectParser<Request, Void> PARSER = new ConstructingObjectParser<>(
+            "update_security_settings_request",
+            false,
+            a -> new Request((Map<String, Object>) a[0], (Map<String, Object>) a[1], (Map<String, Object>) a[2])
+        );
+
+        static {
+            PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField(MAIN_INDEX_NAME));
+            PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField(TOKENS_INDEX_NAME));
+            PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField(PROFILES_INDEX_NAME));
+        }
+
+        public Request(
+            Map<String, Object> mainIndexSettings,
+            Map<String, Object> tokensIndexSettings,
+            Map<String, Object> profilesIndexSettings
+        ) {
+            this.mainIndexSettings = Objects.requireNonNullElse(mainIndexSettings, Collections.emptyMap());
+            this.tokensIndexSettings = Objects.requireNonNullElse(tokensIndexSettings, Collections.emptyMap());
+            this.profilesIndexSettings = Objects.requireNonNullElse(profilesIndexSettings, Collections.emptyMap());
+        }
+
+        public Request(StreamInput in) throws IOException {
+            this.mainIndexSettings = in.readMap();
+            this.tokensIndexSettings = in.readMap();
+            this.profilesIndexSettings = in.readMap();
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeGenericMap(this.mainIndexSettings);
+            out.writeGenericMap(this.tokensIndexSettings);
+            out.writeGenericMap(this.profilesIndexSettings);
+        }
+
+        public static Request parse(XContentParser parser) {
+            return PARSER.apply(parser, null);
+        }
+
+        public Map<String, Object> mainIndexSettings() {
+            return this.mainIndexSettings;
+        }
+
+        public Map<String, Object> tokensIndexSettings() {
+            return this.tokensIndexSettings;
+        }
+
+        public Map<String, Object> profilesIndexSettings() {
+            return this.profilesIndexSettings;
+        }
+
+        @Override
+        public ActionRequestValidationException validate() {
+            if (mainIndexSettings.isEmpty() && tokensIndexSettings.isEmpty() && profilesIndexSettings.isEmpty()) {
+                return ValidateActions.addValidationError("No settings given to update", null);
+            }
+            ActionRequestValidationException validationException = validateIndexSettings(mainIndexSettings, MAIN_INDEX_NAME, null);
+            validationException = validateIndexSettings(tokensIndexSettings, TOKENS_INDEX_NAME, validationException);
+            validationException = validateIndexSettings(profilesIndexSettings, PROFILES_INDEX_NAME, validationException);
+            return validationException;
+        }
+
+        private static ActionRequestValidationException validateIndexSettings(
+            Map<String, Object> indexSettings,
+            String indexName,
+            ActionRequestValidationException existingExceptions
+        ) {
+            Set<String> forbiddenSettings = Sets.difference(indexSettings.keySet(), ALLOWED_SETTING_KEYS);
+            if (forbiddenSettings.size() > 0) {
+                return ValidateActions.addValidationError(
+                    "illegal settings for index ["
+                        + indexName
+                        + "]: "
+                        + forbiddenSettings
+                        + ", these settings may not be configured. Only the following settings may be configured for that index: "
+                        + ALLOWED_SETTING_KEYS,
+                    existingExceptions
+                );
+            }
+            return existingExceptions;
+        }
+    }
+}

+ 3 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java

@@ -44,6 +44,7 @@ import org.elasticsearch.xpack.core.security.action.rolemapping.GetRoleMappingsA
 import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction;
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction;
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsAction;
+import org.elasticsearch.xpack.core.security.action.settings.GetSecuritySettingsAction;
 import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction;
 import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction;
 import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
@@ -233,7 +234,8 @@ public class ClusterPrivilegeResolver {
             GetServiceAccountCredentialsAction.NAME + "*",
             GetUsersAction.NAME,
             GetUserPrivilegesAction.NAME, // normally authorized under the "same-user" authz check, but added here for uniformity
-            HasPrivilegesAction.NAME
+            HasPrivilegesAction.NAME,
+            GetSecuritySettingsAction.NAME
         )
     );
     public static final NamedClusterPrivilege MANAGE_SAML = new ActionClusterPrivilege("manage_saml", MANAGE_SAML_PATTERN);

+ 126 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/settings/UpdateSecuritySettingsActionTests.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.core.security.action.settings;
+
+import org.elasticsearch.test.ESTestCase;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import static org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction.ALLOWED_SETTING_KEYS;
+import static org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction.MAIN_INDEX_NAME;
+import static org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction.PROFILES_INDEX_NAME;
+import static org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction.TOKENS_INDEX_NAME;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.matchesRegex;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+public class UpdateSecuritySettingsActionTests extends ESTestCase {
+
+    public void testValidateSettingsEmpty() {
+        var req = new UpdateSecuritySettingsAction.Request(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap());
+        var ex = req.validate();
+        assertThat(ex, notNullValue());
+        assertThat(ex.getMessage(), containsString("No settings given to update"));
+        assertThat(ex.validationErrors(), hasSize(1));
+    }
+
+    public void testAllowedSettingsOk() {
+        Map<String, Object> allAllowedSettingsMap = new HashMap<>();
+        for (String allowedSetting : ALLOWED_SETTING_KEYS) {
+            Map<String, Object> allowedSettingMap = Map.of(allowedSetting, randomAlphaOfLength(5));
+            allAllowedSettingsMap.put(allowedSetting, randomAlphaOfLength(5));
+            var req = new UpdateSecuritySettingsAction.Request(allowedSettingMap, Collections.emptyMap(), Collections.emptyMap());
+            assertThat(req.validate(), nullValue());
+
+            req = new UpdateSecuritySettingsAction.Request(Collections.emptyMap(), allowedSettingMap, Collections.emptyMap());
+            assertThat(req.validate(), nullValue());
+
+            req = new UpdateSecuritySettingsAction.Request(Collections.emptyMap(), Collections.emptyMap(), allowedSettingMap);
+            assertThat(req.validate(), nullValue());
+        }
+
+        var req = new UpdateSecuritySettingsAction.Request(allAllowedSettingsMap, allAllowedSettingsMap, allAllowedSettingsMap);
+        assertThat(req.validate(), nullValue());
+    }
+
+    public void testDisallowedSettingsFailsValidation() {
+        String disallowedSetting = "index."
+            + randomValueOtherThanMany((value) -> ALLOWED_SETTING_KEYS.contains("index." + value), () -> randomAlphaOfLength(5));
+        Map<String, Object> disallowedSettingMap = Map.of(disallowedSetting, randomAlphaOfLength(5));
+        Map<String, Object> validOrEmptySettingMap = randomFrom(
+            Collections.emptyMap(),
+            Map.of(randomFrom(ALLOWED_SETTING_KEYS), randomAlphaOfLength(5))
+        );
+        {
+            var req = new UpdateSecuritySettingsAction.Request(validOrEmptySettingMap, disallowedSettingMap, validOrEmptySettingMap);
+            List<String> errors = req.validate().validationErrors();
+            assertThat(errors, hasSize(1));
+            for (String errorMsg : errors) {
+                assertThat(
+                    errorMsg,
+                    matchesRegex(
+                        "illegal settings for index \\["
+                            + Pattern.quote(TOKENS_INDEX_NAME)
+                            + "\\]: \\["
+                            + disallowedSetting
+                            + "\\], these settings may not be configured. Only the following settings may be configured for that index.*"
+                    )
+                );
+            }
+        }
+
+        {
+            var req = new UpdateSecuritySettingsAction.Request(disallowedSettingMap, validOrEmptySettingMap, disallowedSettingMap);
+            List<String> errors = req.validate().validationErrors();
+            assertThat(errors, hasSize(2));
+            for (String errorMsg : errors) {
+                assertThat(
+                    errorMsg,
+                    matchesRegex(
+                        "illegal settings for index \\[("
+                            + Pattern.quote(MAIN_INDEX_NAME)
+                            + "|"
+                            + Pattern.quote(PROFILES_INDEX_NAME)
+                            + ")\\]: \\["
+                            + disallowedSetting
+                            + "\\], these settings may not be configured. Only the following settings may be configured for that index.*"
+                    )
+                );
+            }
+        }
+
+        {
+            var req = new UpdateSecuritySettingsAction.Request(disallowedSettingMap, disallowedSettingMap, disallowedSettingMap);
+            List<String> errors = req.validate().validationErrors();
+            assertThat(errors, hasSize(3));
+            for (String errorMsg : errors) {
+                assertThat(
+                    errorMsg,
+                    matchesRegex(
+                        "illegal settings for index \\[("
+                            + Pattern.quote(MAIN_INDEX_NAME)
+                            + "|"
+                            + Pattern.quote(TOKENS_INDEX_NAME)
+                            + "|"
+                            + Pattern.quote(PROFILES_INDEX_NAME)
+                            + ")\\]: \\["
+                            + disallowedSetting
+                            + "\\], these settings may not be configured. Only the following settings may be configured for that index.*"
+                    )
+                );
+            }
+        }
+    }
+
+}

+ 6 - 2
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/PrivilegeTests.java

@@ -53,6 +53,8 @@ import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccount
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction;
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsAction;
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountNodesCredentialsAction;
+import org.elasticsearch.xpack.core.security.action.settings.GetSecuritySettingsAction;
+import org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction;
 import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction;
 import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
 import org.elasticsearch.xpack.core.security.action.user.GetUsersAction;
@@ -284,7 +286,8 @@ public class PrivilegeTests extends ESTestCase {
             GetServiceAccountCredentialsAction.NAME,
             GetUsersAction.NAME,
             HasPrivilegesAction.NAME,
-            GetUserPrivilegesAction.NAME
+            GetUserPrivilegesAction.NAME,
+            GetSecuritySettingsAction.NAME
         );
         verifyClusterActionAllowed(
             ClusterPrivilegeResolver.READ_SECURITY,
@@ -319,7 +322,8 @@ public class PrivilegeTests extends ESTestCase {
             DelegatePkiAuthenticationAction.NAME,
             ActivateProfileAction.NAME,
             SetProfileEnabledAction.NAME,
-            UpdateProfileDataAction.NAME
+            UpdateProfileDataAction.NAME,
+            UpdateSecuritySettingsAction.NAME
         );
     }
 

+ 2 - 0
x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java

@@ -247,6 +247,8 @@ public class Constants {
         "cluster:admin/xpack/security/service_account/credential/get[n]",
         "cluster:admin/xpack/security/service_account/token/create",
         "cluster:admin/xpack/security/service_account/token/delete",
+        "cluster:admin/xpack/security/settings/get",
+        "cluster:admin/xpack/security/settings/update",
         "cluster:admin/xpack/security/token/create",
         "cluster:admin/xpack/security/token/invalidate",
         "cluster:admin/xpack/security/token/refresh",

+ 101 - 0
x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/SecuritySettingsIT.java

@@ -0,0 +1,101 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.security;
+
+import org.apache.http.util.EntityUtils;
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
+import org.elasticsearch.test.XContentTestUtils;
+import org.junit.Before;
+
+import java.io.IOException;
+
+import static org.elasticsearch.test.XContentTestUtils.createJsonMapView;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+public class SecuritySettingsIT extends SecurityInBasicRestTestCase {
+
+    @Before
+    public void setupClient() throws IOException {
+        // Poke the Main index and Profiles index, but leave the Tokens index un-created
+        var userReq = new Request("PUT", "/_security/user/jacknich");
+        userReq.setJsonEntity("""
+            {
+                "password" : "l0ng-r4nd0m-p@ssw0rd",
+                "roles" : [ "admin", "other_role1" ],
+                "full_name" : "Jack Nicholson",
+                "email" : "jacknich@example.com"
+            }
+            """);
+        assertOK(adminClient().performRequest(userReq));
+        var profileReq = new Request("POST", "_security/profile/_activate");
+        profileReq.setJsonEntity("""
+            {
+                "grant_type": "password",
+                "username": "jacknich",
+                "password" : "l0ng-r4nd0m-p@ssw0rd"
+            }
+            """);
+        assertOK(adminClient().performRequest(profileReq));
+    }
+
+    /**
+     * This test only checks the main and profiles indices. Making sure the tokens index is not trivial;
+     * this test should be updated to do so (and also un-mute the docs test for this API).
+     * @throws IOException
+     */
+    public void testBasicWorkflow() throws IOException {
+        Request req = new Request("PUT", "/_security/settings");
+        req.setJsonEntity("""
+            {
+                "security": {
+                    "index.auto_expand_replicas": "0-all"
+                },
+                "security-profile": {
+                        "index.auto_expand_replicas": "0-all"
+                }
+            }
+            """);
+        Response resp = adminClient().performRequest(req);
+        assertOK(resp);
+        Request getRequest = new Request("GET", "/_security/settings");
+        Response getResp = adminClient().performRequest(getRequest);
+        assertOK(getResp);
+        final XContentTestUtils.JsonMapView mapView = createJsonMapView(getResp.getEntity().getContent());
+        assertThat(mapView.get("security.index.auto_expand_replicas"), equalTo("0-all"));
+    }
+
+    public void testNoUpdatesThrowsException() throws IOException {
+        Request req = new Request("PUT", "/_security/settings");
+        req.setJsonEntity("{}");
+        ResponseException ex = expectThrows(ResponseException.class, () -> adminClient().performRequest(req));
+        assertThat(EntityUtils.toString(ex.getResponse().getEntity()), containsString("No settings given to update"));
+    }
+
+    public void testDisallowedSettingThrowsException() throws IOException {
+        Request req = new Request("PUT", "/_security/settings");
+        req.setJsonEntity("{\"security\": {\"index.max_ngram_diff\": 0}}"); // Disallowed setting chosen arbitrarily
+        ResponseException ex = expectThrows(ResponseException.class, () -> adminClient().performRequest(req));
+        assertThat(
+            EntityUtils.toString(ex.getResponse().getEntity()),
+            containsString("illegal settings for index [security]: " + "[index.max_ngram_diff], these settings may not be configured.")
+        );
+    }
+
+    public void testIndexDoesntExistThrowsException() throws IOException {
+        Request req = new Request("PUT", "/_security/settings");
+        req.setJsonEntity("{\"security-tokens\": {\"index.auto_expand_replicas\": \"0-all\"}}");
+        ResponseException ex = expectThrows(ResponseException.class, () -> adminClient().performRequest(req));
+        assertThat(
+            EntityUtils.toString(ex.getResponse().getEntity()),
+            containsString("the [.security-tokens] index is not in use on this system yet")
+        );
+    }
+}

+ 2 - 1
x-pack/plugin/security/src/main/java/module-info.java

@@ -63,7 +63,8 @@ module org.elasticsearch.security {
     exports org.elasticsearch.xpack.security.action.service to org.elasticsearch.server;
     exports org.elasticsearch.xpack.security.action.token to org.elasticsearch.server;
     exports org.elasticsearch.xpack.security.action.user to org.elasticsearch.server;
-    exports org.elasticsearch.xpack.security.operator to org.elasticsearch.internal.security;
+    exports org.elasticsearch.xpack.security.action.settings to org.elasticsearch.server;
+    exports org.elasticsearch.xpack.security.operator to org.elasticsearch.internal.operator, org.elasticsearch.internal.security;
 
     exports org.elasticsearch.xpack.security.authc to org.elasticsearch.xcontent;
 

+ 11 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

@@ -158,6 +158,8 @@ import org.elasticsearch.xpack.core.security.action.service.DeleteServiceAccount
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction;
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountCredentialsAction;
 import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountNodesCredentialsAction;
+import org.elasticsearch.xpack.core.security.action.settings.GetSecuritySettingsAction;
+import org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction;
 import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction;
 import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction;
 import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction;
@@ -246,6 +248,8 @@ import org.elasticsearch.xpack.security.action.service.TransportDeleteServiceAcc
 import org.elasticsearch.xpack.security.action.service.TransportGetServiceAccountAction;
 import org.elasticsearch.xpack.security.action.service.TransportGetServiceAccountCredentialsAction;
 import org.elasticsearch.xpack.security.action.service.TransportGetServiceAccountNodesCredentialsAction;
+import org.elasticsearch.xpack.security.action.settings.TransportGetSecuritySettingsAction;
+import org.elasticsearch.xpack.security.action.settings.TransportUpdateSecuritySettingsAction;
 import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction;
 import org.elasticsearch.xpack.security.action.token.TransportInvalidateTokenAction;
 import org.elasticsearch.xpack.security.action.token.TransportRefreshTokenAction;
@@ -352,6 +356,8 @@ import org.elasticsearch.xpack.security.rest.action.service.RestCreateServiceAcc
 import org.elasticsearch.xpack.security.rest.action.service.RestDeleteServiceAccountTokenAction;
 import org.elasticsearch.xpack.security.rest.action.service.RestGetServiceAccountAction;
 import org.elasticsearch.xpack.security.rest.action.service.RestGetServiceAccountCredentialsAction;
+import org.elasticsearch.xpack.security.rest.action.settings.RestGetSecuritySettingsAction;
+import org.elasticsearch.xpack.security.rest.action.settings.RestUpdateSecuritySettingsAction;
 import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction;
 import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction;
 import org.elasticsearch.xpack.security.rest.action.user.RestGetUserPrivilegesAction;
@@ -1358,6 +1364,8 @@ public class Security extends Plugin
             new ActionHandler<>(UpdateProfileDataAction.INSTANCE, TransportUpdateProfileDataAction.class),
             new ActionHandler<>(SuggestProfilesAction.INSTANCE, TransportSuggestProfilesAction.class),
             new ActionHandler<>(SetProfileEnabledAction.INSTANCE, TransportSetProfileEnabledAction.class),
+            new ActionHandler<>(GetSecuritySettingsAction.INSTANCE, TransportGetSecuritySettingsAction.class),
+            new ActionHandler<>(UpdateSecuritySettingsAction.INSTANCE, TransportUpdateSecuritySettingsAction.class),
             usageAction,
             infoAction
         ).filter(Objects::nonNull).toList();
@@ -1442,7 +1450,9 @@ public class Security extends Plugin
             new RestUpdateProfileDataAction(settings, getLicenseState()),
             new RestSuggestProfilesAction(settings, getLicenseState()),
             new RestEnableProfileAction(settings, getLicenseState()),
-            new RestDisableProfileAction(settings, getLicenseState())
+            new RestDisableProfileAction(settings, getLicenseState()),
+            new RestGetSecuritySettingsAction(settings, getLicenseState()),
+            new RestUpdateSecuritySettingsAction(settings, getLicenseState())
         ).filter(Objects::nonNull).toList();
     }
 

+ 125 - 0
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/settings/TransportGetSecuritySettingsAction.java

@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.security.action.settings;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.master.TransportMasterNodeAction;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.block.ClusterBlockException;
+import org.elasticsearch.cluster.block.ClusterBlockLevel;
+import org.elasticsearch.cluster.metadata.IndexAbstraction;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xpack.core.security.action.settings.GetSecuritySettingsAction;
+import org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS;
+import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_PROFILE_ALIAS;
+import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_TOKENS_ALIAS;
+
+public class TransportGetSecuritySettingsAction extends TransportMasterNodeAction<
+    GetSecuritySettingsAction.Request,
+    GetSecuritySettingsAction.Response> {
+
+    @Inject
+    public TransportGetSecuritySettingsAction(
+        TransportService transportService,
+        ClusterService clusterService,
+        ThreadPool threadPool,
+        ActionFilters actionFilters,
+        IndexNameExpressionResolver indexNameExpressionResolver
+    ) {
+        super(
+            GetSecuritySettingsAction.NAME,
+            transportService,
+            clusterService,
+            threadPool,
+            actionFilters,
+            GetSecuritySettingsAction.Request::new,
+            indexNameExpressionResolver,
+            GetSecuritySettingsAction.Response::new,
+            ThreadPool.Names.SAME
+        );
+    }
+
+    @Override
+    protected void masterOperation(
+        Task task,
+        GetSecuritySettingsAction.Request request,
+        ClusterState state,
+        ActionListener<GetSecuritySettingsAction.Response> listener
+    ) {
+        listener.onResponse(
+            new GetSecuritySettingsAction.Response(
+                getFilteredSettingsForIndex(SECURITY_MAIN_ALIAS, state),
+                getFilteredSettingsForIndex(SECURITY_TOKENS_ALIAS, state),
+                getFilteredSettingsForIndex(SECURITY_PROFILE_ALIAS, state)
+            )
+        );
+    }
+
+    /**
+     * Filters the settings to only those settable by the user (using the update security settings API).
+     */
+    private static Settings getFilteredSettingsForIndex(String indexName, ClusterState state) {
+        // Check the indices lookup to resolve the alias
+
+        return resolveConcreteIndex(indexName, state).map(idx -> state.metadata().index(idx))
+            .map(IndexMetadata::getSettings)
+            .map(settings -> {
+                Settings.Builder builder = Settings.builder();
+                for (String settingName : UpdateSecuritySettingsAction.ALLOWED_SETTING_KEYS) {
+                    if (settings.hasValue(settingName)) {
+                        builder.put(settingName, settings.get(settingName));
+                    }
+                }
+                return builder.build();
+            })
+            .orElse(Settings.EMPTY);
+    }
+
+    static Optional<Index> resolveConcreteIndex(String indexAbstractionName, ClusterState state) {
+        // Don't use the indexNameExpressionResolver here so we don't trigger a system index deprecation warning
+        IndexAbstraction abstraction = state.metadata().getIndicesLookup().get(indexAbstractionName);
+        if (abstraction == null) {
+            return Optional.empty();
+        }
+        return Optional.ofNullable(abstraction.getWriteIndex());
+    }
+
+    static String[] resolveConcreteIndices(List<String> indexAbstractionNames, ClusterState state) {
+        return indexAbstractionNames.stream()
+            .map(alias -> resolveConcreteIndex(alias, state).map(Index::getName))
+            .filter(Optional::isPresent)
+            .map(Optional::get)
+            .toArray(String[]::new);
+    }
+
+    @Override
+    protected ClusterBlockException checkBlock(GetSecuritySettingsAction.Request request, ClusterState state) {
+        ClusterBlockException globalBlock = state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
+        if (globalBlock != null) {
+            return globalBlock;
+        }
+
+        String[] indices = resolveConcreteIndices(List.of(SECURITY_MAIN_ALIAS, SECURITY_TOKENS_ALIAS, SECURITY_PROFILE_ALIAS), state);
+        return state.blocks().indicesBlockedException(ClusterBlockLevel.METADATA_READ, indices);
+    }
+
+}

+ 168 - 0
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/settings/TransportUpdateSecuritySettingsAction.java

@@ -0,0 +1,168 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.security.action.settings;
+
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsClusterStateUpdateRequest;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.GroupedActionListener;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.action.support.master.TransportMasterNodeAction;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.block.ClusterBlockException;
+import org.elasticsearch.cluster.block.ClusterBlockLevel;
+import org.elasticsearch.cluster.metadata.IndexAbstraction;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.metadata.MetadataUpdateSettingsService;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import static org.elasticsearch.xpack.security.action.settings.TransportGetSecuritySettingsAction.resolveConcreteIndices;
+import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS;
+import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_PROFILE_ALIAS;
+import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_TOKENS_ALIAS;
+
+public class TransportUpdateSecuritySettingsAction extends TransportMasterNodeAction<
+    UpdateSecuritySettingsAction.Request,
+    AcknowledgedResponse> {
+    private static final Logger logger = LogManager.getLogger(TransportUpdateSecuritySettingsAction.class);
+
+    private final MetadataUpdateSettingsService updateSettingsService;
+
+    @Inject
+    public TransportUpdateSecuritySettingsAction(
+        TransportService transportService,
+        ClusterService clusterService,
+        ThreadPool threadPool,
+        ActionFilters actionFilters,
+        MetadataUpdateSettingsService metadataUpdateSettingsService,
+        IndexNameExpressionResolver indexNameExpressionResolver
+    ) {
+        super(
+            UpdateSecuritySettingsAction.NAME,
+            transportService,
+            clusterService,
+            threadPool,
+            actionFilters,
+            UpdateSecuritySettingsAction.Request::new,
+            indexNameExpressionResolver,
+            AcknowledgedResponse::readFrom,
+            ThreadPool.Names.SAME
+        );
+        this.updateSettingsService = metadataUpdateSettingsService;
+    }
+
+    @Override
+    protected void masterOperation(
+        Task task,
+        UpdateSecuritySettingsAction.Request request,
+        ClusterState state,
+        ActionListener<AcknowledgedResponse> listener
+    ) {
+
+        List<UpdateSettingsClusterStateUpdateRequest> settingsUpdateRequests = Stream.of(
+            createUpdateSettingsRequest(
+                SECURITY_MAIN_ALIAS,
+                Settings.builder().loadFromMap(request.mainIndexSettings()).build(),
+                request.timeout(),
+                request.masterNodeTimeout(),
+                state
+            ),
+            createUpdateSettingsRequest(
+                SECURITY_TOKENS_ALIAS,
+                Settings.builder().loadFromMap(request.tokensIndexSettings()).build(),
+                request.timeout(),
+                request.masterNodeTimeout(),
+                state
+            ),
+            createUpdateSettingsRequest(
+                SECURITY_PROFILE_ALIAS,
+                Settings.builder().loadFromMap(request.profilesIndexSettings()).build(),
+                request.timeout(),
+                request.masterNodeTimeout(),
+                state
+            )
+        ).filter(Optional::isPresent).map(Optional::get).toList();
+        if (settingsUpdateRequests.isEmpty() == false) {
+            ActionListener<AcknowledgedResponse> groupedListener = new GroupedActionListener<>(
+                settingsUpdateRequests.size(),
+                ActionListener.wrap((responses) -> {
+                    listener.onResponse(AcknowledgedResponse.of(responses.stream().allMatch(AcknowledgedResponse::isAcknowledged)));
+                }, listener::onFailure)
+            );
+            settingsUpdateRequests.forEach(req -> updateSettingsService.updateSettings(req, groupedListener));
+        } else {
+            // All settings blocks were empty, which doesn't do anything, so this was probably a mistake
+            assert false : "getting this far with an empty settings block should have been prevented by earlier request validation";
+            throw new IllegalArgumentException("No settings to update");
+        }
+    }
+
+    private Optional<UpdateSettingsClusterStateUpdateRequest> createUpdateSettingsRequest(
+        String indexName,
+        Settings settingsToUpdate,
+        TimeValue timeout,
+        TimeValue masterTimeout,
+        ClusterState state
+    ) {
+        if (settingsToUpdate.isEmpty()) {
+            return Optional.empty();
+        }
+        IndexAbstraction abstraction = state.metadata().getIndicesLookup().get(indexName);
+        if (abstraction == null) {
+            throw new IllegalArgumentException("the [" + indexName + "] index is not in use on this system yet");
+        }
+        Index writeIndex = abstraction.getWriteIndex();
+        if (writeIndex == null) {
+            throw new IllegalStateException(Strings.format("security system alias [%s] exists but does not have a write index"));
+        }
+
+        return Optional.of(
+            new UpdateSettingsClusterStateUpdateRequest().indices(new Index[] { writeIndex })
+                .settings(settingsToUpdate)
+                .ackTimeout(timeout)
+                .masterNodeTimeout(masterTimeout)
+        );
+    }
+
+    @Override
+    protected ClusterBlockException checkBlock(UpdateSecuritySettingsAction.Request request, ClusterState state) {
+        ClusterBlockException globalBlock = state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE);
+        if (globalBlock != null) {
+            return globalBlock;
+        }
+        List<String> indices = new ArrayList<>(3);
+        if (request.mainIndexSettings().isEmpty() == false) {
+            indices.add(SECURITY_MAIN_ALIAS);
+        }
+        if (request.tokensIndexSettings().isEmpty() == false) {
+            indices.add(SECURITY_TOKENS_ALIAS);
+        }
+        if (request.profilesIndexSettings().isEmpty() == false) {
+            indices.add(SECURITY_PROFILE_ALIAS);
+        }
+
+        String[] concreteIndices = resolveConcreteIndices(indices, state);
+        return state.blocks().indicesBlockedException(ClusterBlockLevel.METADATA_WRITE, concreteIndices);
+    }
+}

+ 42 - 0
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/settings/RestGetSecuritySettingsAction.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.security.rest.action.settings;
+
+import org.elasticsearch.client.internal.node.NodeClient;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.license.XPackLicenseState;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.action.RestToXContentListener;
+import org.elasticsearch.xpack.core.security.action.settings.GetSecuritySettingsAction;
+import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
+
+import java.io.IOException;
+import java.util.List;
+
+public class RestGetSecuritySettingsAction extends SecurityBaseRestHandler {
+
+    public RestGetSecuritySettingsAction(Settings settings, XPackLicenseState licenseState) {
+        super(settings, licenseState);
+    }
+
+    @Override
+    public String getName() {
+        return "security_get_settings";
+    }
+
+    @Override
+    public List<Route> routes() {
+        return List.of(Route.builder(RestRequest.Method.GET, "/_security/settings").build());
+    }
+
+    @Override
+    protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
+        GetSecuritySettingsAction.Request req = new GetSecuritySettingsAction.Request();
+        return restChannel -> client.execute(GetSecuritySettingsAction.INSTANCE, req, new RestToXContentListener<>(restChannel));
+    }
+}

+ 42 - 0
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/settings/RestUpdateSecuritySettingsAction.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.security.rest.action.settings;
+
+import org.elasticsearch.client.internal.node.NodeClient;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.license.XPackLicenseState;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.action.RestToXContentListener;
+import org.elasticsearch.xpack.core.security.action.settings.UpdateSecuritySettingsAction;
+import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
+
+import java.io.IOException;
+import java.util.List;
+
+public class RestUpdateSecuritySettingsAction extends SecurityBaseRestHandler {
+
+    public RestUpdateSecuritySettingsAction(Settings settings, XPackLicenseState licenseState) {
+        super(settings, licenseState);
+    }
+
+    @Override
+    public String getName() {
+        return "security_update_settings";
+    }
+
+    @Override
+    public List<Route> routes() {
+        return List.of(Route.builder(RestRequest.Method.PUT, "/_security/settings").build());
+    }
+
+    @Override
+    protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException {
+        UpdateSecuritySettingsAction.Request req = UpdateSecuritySettingsAction.Request.parse(request.contentParser());
+        return restChannel -> client.execute(UpdateSecuritySettingsAction.INSTANCE, req, new RestToXContentListener<>(restChannel));
+    }
+}

+ 103 - 0
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/security/settings/10_update_security_settings.yml

@@ -0,0 +1,103 @@
+---
+setup:
+  - skip:
+      features: headers
+  - do:
+      cluster.health:
+        wait_for_status: yellow
+  - do:
+      security.put_user:
+        username: "joe"
+        body:  >
+          {
+            "password": "s3krit-password",
+            "roles" : [ "token_admin" ]
+          }
+  - do:
+      security.put_role:
+        name: "token_admin"
+        body:  >
+          {
+            "cluster": ["manage_token"],
+            "indices": [
+              {
+                "names": "*",
+                "privileges": ["all"]
+              }
+            ]
+          }
+  - do:
+      security.activate_user_profile:
+        body: >
+          {
+            "grant_type": "password",
+            "username": "joe",
+            "password" : "s3krit-password"
+          }
+  - do:
+      headers:
+        Authorization: "Basic am9lOnMza3JpdC1wYXNzd29yZA=="
+      security.get_token:
+        body:
+          grant_type: "password"
+          username: "joe"
+          password: "s3krit-password"
+---
+teardown:
+  - do:
+      security.delete_user:
+        username: "joe"
+        ignore: 404
+  - do:
+      security.delete_role:
+        name: "token_admin"
+        ignore: 404
+
+---
+"Test update and get security settings API":
+  - do:
+      security.get_settings: {}
+
+  - match: { "security.index.number_of_replicas": "0" }
+  - match: { "security.index.auto_expand_replicas": "0-1" }
+  - match: { "security-tokens.index.auto_expand_replicas": "0-1" }
+  - match: { "security-tokens.index.number_of_replicas": "0" }
+  - match: { "security-profile.index.auto_expand_replicas": "0-1" }
+  - match: { "security-profile.index.number_of_replicas": "0" }
+
+  - do:
+      security.update_settings:
+        body:
+          security:
+              index.auto_expand_replicas: "0-all"
+          security-tokens:
+              index.auto_expand_replicas: "0-all"
+          security-profile:
+              index.auto_expand_replicas: "0-all"
+
+  - do:
+      security.get_settings: { }
+
+  - match: { "security.index.auto_expand_replicas": "0-all" }
+  - match: { "security-tokens.index.auto_expand_replicas": "0-all" }
+  - match: { "security-profile.index.auto_expand_replicas": "0-all" }
+
+  - do:
+      security.update_settings:
+        body:
+          security:
+              index.auto_expand_replicas: null
+              index.number_of_replicas: 1
+          security-tokens:
+              index.auto_expand_replicas: null
+              index.number_of_replicas: 1
+          security-profile:
+              index.auto_expand_replicas: null
+              index.number_of_replicas: 1
+
+  - do:
+      security.get_settings: { }
+
+  - match: { "security.index.number_of_replicas": "1" }
+  - match: { "security-tokens.index.number_of_replicas": "1" }
+  - match: { "security-tokens.index.number_of_replicas": "1" }