Browse Source

Service Accounts - audit for security config change (#72555)

Add security_config_change auditing for create and delete index-based
service account tokens.
Yang Wang 4 years ago
parent
commit
4b2cbb4935

+ 74 - 40
x-pack/docs/en/security/auditing/event-types.asciidoc

@@ -22,10 +22,10 @@ have the necessary <<security-privileges,privilege>> to perform.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T22:30:06,949+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"transport", "event.action":
-"access_denied", "authentication.type":"REALM", "user.name":"user1", 
+"access_denied", "authentication.type":"REALM", "user.name":"user1",
 "user.realm":"default_native", "user.roles":["test_role"], "origin.type":
 "rest", "origin.address":"[::1]:52434", "request.id":"yKOgWn2CRQCKYgZRz3phJw",
-"action":"indices:admin/auto_create", "request.name":"CreateIndexRequest", 
+"action":"indices:admin/auto_create", "request.name":"CreateIndexRequest",
 "indices":["<index-{now/d+1d}>"]}
 ====
 
@@ -48,8 +48,8 @@ privilege is not included by default to avoid cluttering the logs.
 {"type":"audit", "timestamp":"2020-12-30T22:30:06,947+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"transport", "event.action":
 "access_granted", "authentication.type":"REALM", "user.name":"user1", "user
-realm":"default_native", "user.roles":["test_role"], "origin.type":"rest", 
-"origin.address":"[::1]:52434", "request.id":"yKOgWn2CRQCKYgZRz3phJw", 
+realm":"default_native", "user.roles":["test_role"], "origin.type":"rest",
+"origin.address":"[::1]:52434", "request.id":"yKOgWn2CRQCKYgZRz3phJw",
 "action":"indices:data/write/bulk", "request.name":"BulkRequest"}
 ====
 
@@ -64,7 +64,7 @@ Logged when a request is denied due to missing authentication credentials.
 {"type":"audit", "timestamp":"2020-12-30T21:56:43,608+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"rest", "event.action":
 "anonymous_access_denied", "origin.type":"rest", "origin.address":
-"[::1]:50543", "url.path":"/twitter/_async_search", "url.query":"pretty", 
+"[::1]:50543", "url.path":"/twitter/_async_search", "url.query":"pretty",
 "request.method":"POST", "request.id":"TqA9OisyQ8WTl1ivJUV1AA"}
 ====
 
@@ -78,9 +78,9 @@ Logged when the authentication credentials cannot be matched to a known user.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T22:10:15,510+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"rest", "event.action":
-"authentication_failed", "user.name":"elastic", "origin.type":"rest", 
-"origin.address":"[::1]:51504", "url.path":"/_security/user/user1", 
-"url.query":"pretty", "request.method":"POST", 
+"authentication_failed", "user.name":"elastic", "origin.type":"rest",
+"origin.address":"[::1]:51504", "url.path":"/_security/user/user1",
+"url.query":"pretty", "request.method":"POST",
 "request.id":"POv8p_qeTl2tb5xoFl0HIg"}
 ====
 
@@ -96,8 +96,8 @@ Logged when a user successfully authenticates.
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"rest", "event.action":
 "authentication_success", "authentication.type":"REALM", "user.name":
 "elastic", "user.realm":"reserved", "origin.type":"rest", "origin.address":
-"[::1]:51014", "realm":"reserved", "url.path":"/twitter/_search", 
-"url.query":"pretty", "request.method":"POST", 
+"[::1]:51014", "realm":"reserved", "url.path":"/twitter/_search",
+"url.query":"pretty", "request.method":"POST",
 "request.id":"nHV3UMOoSiu-TaSPWCfxGg"}
 ====
 
@@ -112,7 +112,7 @@ disable a native or a built-in user.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T23:17:28,308+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", "event.
-action":"change_disable_user", "request.id":"qvLIgw_eTvyK3cgV-GaLVg", 
+action":"change_disable_user", "request.id":"qvLIgw_eTvyK3cgV-GaLVg",
 "change":{"disable":{"user":{"name":"user1"}}}}
 ====
 
@@ -127,7 +127,7 @@ enable a native or a built-in user.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T23:17:34,843+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", "event.
-action":"change_enable_user", "request.id":"BO3QU3qeTb-Ei0G0rUOalQ", 
+action":"change_enable_user", "request.id":"BO3QU3qeTb-Ei0G0rUOalQ",
 "change":{"enable":{"user":{"name":"user1"}}}}
 ====
 
@@ -142,10 +142,26 @@ invoked to change the password of a native or built-in user.
 [source,js]
 {"type":"audit", "timestamp":"2019-12-30T22:19:41,345+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", "event.
-action":"change_password", "request.id":"bz5a1Cc3RrebDMitMGGNCw", 
+action":"change_password", "request.id":"bz5a1Cc3RrebDMitMGGNCw",
 "change":{"password":{"user":{"name":"user1"}}}}
 ====
 
+
+[[event-create-service-token]]
+`create_service_token`::
+Logged when the <<security-api-create-service-token,create service account token API>> is
+invoked to create a new index-based token for a service account.
++
+.Example
+[%collapsible%open]
+====
+[source,js]
+{"type":"audit", "timestamp":"2021-04-30T23:17:42,952+0200", "node.id":
+"0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", "event.
+action":"create_service_token", "request.id":"az9a1Db5QrebDMacQ8yGKc",
+"create":{"service_token":{"namespace":"elastic","service":"fleet-server","name":"token1"}}}`
+====
+
 [[event-connection-denied]]
 `connection_denied`::
 Logged when an incoming TCP connection does not pass the
@@ -157,7 +173,7 @@ Logged when an incoming TCP connection does not pass the
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T21:47:31,526+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"ip_filter", "event.action":
-"connection_denied", "origin.type":"rest", "origin.address":"10.10.0.20", 
+"connection_denied", "origin.type":"rest", "origin.address":"10.10.0.20",
 "transport.profile":".http", "rule":"deny 10.10.0.0/16"}
 ====
 
@@ -172,7 +188,7 @@ for a specific profile.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T21:47:31,526+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"ip_filter", "event.action":
-"connection_granted", "origin.type":"rest", "origin.address":"::1", 
+"connection_granted", "origin.type":"rest", "origin.address":"::1",
 "transport.profile":".http", "rule":"allow ::1,127.0.0.1"}
 ====
 
@@ -209,7 +225,7 @@ to remove one or more application privileges.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-31T00:39:30,246+0200", "node.id":
 "9clhpgjJRR-iKzOw20xBNQ", "event.type":"security_config_change", "event.
-action":"delete_privileges", "request.id":"7wRWVxxqTzCKEspeSP7J8g", 
+action":"delete_privileges", "request.id":"7wRWVxxqTzCKEspeSP7J8g",
 "delete":{"privileges":{"application":"myapp","privileges":["read"]}}}
 ====
 
@@ -224,7 +240,7 @@ delete a role.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-31T00:08:11,678+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", "event.action":
-"delete_role", "request.id":"155IKq3zQdWq-12dgKZRnw", 
+"delete_role", "request.id":"155IKq3zQdWq-12dgKZRnw",
 "delete":{"role":{"name":"my_admin_role"}}}
 ====
 
@@ -239,10 +255,25 @@ is invoked to delete a role mapping.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-31T00:12:09,349+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", "event.
-action":"delete_role_mapping", "request.id":"Stim-DuoSTCWom0S_xhf8g", 
+action":"delete_role_mapping", "request.id":"Stim-DuoSTCWom0S_xhf8g",
 "delete":{"role_mapping":{"name":"mapping1"}}}
 ====
 
+[[event-delete-service-token]]
+`delete_service_token`::
+Logged when the <<security-api-delete-service-token,delete service account token API>> is
+invoked to delete an index-based token for a service account.
++
+.Example
+[%collapsible%open]
+====
+[source,js]
+{"type":"audit", "timestamp":"2021-04-30T23:17:42,952+0200", "node.id":
+"0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", "event.
+action":"delete_service_token", "request.id":"az9a1Db5QrebDMacQ8yGKc",
+"delete":{"service_token":{"namespace":"elastic","service":"fleet-server","name":"token1"}}}
+====
+
 [[event-delete-user]]
 `delete_user`::
 Logged when the <<security-api-delete-user,delete user API>> is invoked to
@@ -253,8 +284,8 @@ delete a specific native user.
 ====
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T22:19:41,345+0200", "node.id":
-"0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", 
-"event.action":"delete_user", "request.id":"au5a1Cc3RrebDMitMGGNCw", 
+"0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change",
+"event.action":"delete_user", "request.id":"au5a1Cc3RrebDMitMGGNCw",
 "delete":{"user":{"name":"jacknich"}}}
 ====
 
@@ -269,7 +300,7 @@ invoked to invalidate one or more API keys.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-31T00:36:30,247+0200", "node.id":
 "9clhpgjJRR-iKzOw20xBNQ", "event.type":"security_config_change", "event.
-action":"invalidate_apikeys", "request.id":"7lyIQU9QTFqSrTxD0CqnTQ", 
+action":"invalidate_apikeys", "request.id":"7lyIQU9QTFqSrTxD0CqnTQ",
 "invalidate":{"apikeys":{"owned_by_authenticated_user":false,
 "user":{"name":"myuser","realm":"native1"}}}}
 ====
@@ -284,8 +315,8 @@ to add or update one or more application privileges.
 ====
 [source,js]
 {"type":"audit", "timestamp":"2020-12-31T00:39:07,779+0200", "node.id":
-"9clhpgjJRR-iKzOw20xBNQ", "event.type":"security_config_change", 
-"event.action":"put_privileges", "request.id":"1X2VVtNgRYO7FmE0nR_BGA", 
+"9clhpgjJRR-iKzOw20xBNQ", "event.type":"security_config_change",
+"event.action":"put_privileges", "request.id":"1X2VVtNgRYO7FmE0nR_BGA",
 "put":{"privileges":[{"application":"myapp","name":"read","actions":
 ["data:read/*","action:login"],"metadata":{"description":"Read access to myapp"}}]}}
 ====
@@ -300,12 +331,12 @@ update a role.
 ====
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T22:27:01,978+0200", "node.id":
-"0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", 
-"event.action":"put_role", "request.id":"tDYQhv5CRMWM4Sc5Zkk2cQ", 
+"0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change",
+"event.action":"put_role", "request.id":"tDYQhv5CRMWM4Sc5Zkk2cQ",
 "put":{"role":{"name":"test_role","role_descriptor":{"cluster":["all"],
 "indices":[{"names":["apm*"],"privileges":["all"],"field_security":
 {"grant":["granted"]},"query":"{\"term\": {\"service.name\": \"bar\"}}"},
-{"names":["apm-all*"],"privileges":["all"],"query":"{\"term\": 
+{"names":["apm-all*"],"privileges":["all"],"query":"{\"term\":
 {\"service.name\": \"bar2\"}}"}],"applications":[],"run_as":[]}}}}
 ====
 
@@ -320,7 +351,7 @@ invoked to create or update a role mapping.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-31T00:11:13,932+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", "event.
-action":"put_role_mapping", "request.id":"kg4h1l_kTDegnLC-0A-XxA", 
+action":"put_role_mapping", "request.id":"kg4h1l_kTDegnLC-0A-XxA",
 "put":{"role_mapping":{"name":"mapping1","roles":["user"],"rules":
 {"field":{"username":"*"}},"enabled":true,"metadata":{"version":1}}}}
 ====
@@ -336,8 +367,8 @@ user's password.
 ====
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T22:10:09,749+0200", "node.id":
-"0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change", 
-"event.action":"put_user", "request.id":"VIiSvhp4Riim_tpkQCVSQA", 
+"0RMNyghkQYCc_gVd1G6tZQ", "event.type":"security_config_change",
+"event.action":"put_user", "request.id":"VIiSvhp4Riim_tpkQCVSQA",
 "put":{"user":{"name":"user1","enabled":false,"roles":["admin","other_role1"],
 "full_name":"Jack Sparrow","email":"jack@blackpearl.com",
 "has_password":true,"metadata":{"cunning":10}}}}
@@ -353,9 +384,9 @@ Logged for every realm that fails to present a valid authentication token.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T22:10:15,510+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"rest", "event.action":
-"realm_authentication_failed", "user.name":"elastic", "origin.type":"rest", 
+"realm_authentication_failed", "user.name":"elastic", "origin.type":"rest",
 "origin.address":"[::1]:51504", "realm":"myTestRealm1", "url.path":
-"/_security/user/user1", "url.query":"pretty", "request.method":"POST", 
+"/_security/user/user1", "url.query":"pretty", "request.method":"POST",
 "request.id":"POv8p_qeTl2tb5xoFl0HIg"}
 ====
 
@@ -371,10 +402,10 @@ another user that they do not have the necessary
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T22:49:34,859+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"transport", "event.action":
-"run_as_denied", "user.name":"user1", "user.run_as.name":"user1", 
-"user.realm":"default_native", "user.run_as.realm":"default_native", 
+"run_as_denied", "user.name":"user1", "user.run_as.name":"user1",
+"user.realm":"default_native", "user.run_as.realm":"default_native",
 "user.roles":["test_role"], "origin.type":"rest", "origin.address":
-"[::1]:52662", "request.id":"RcaSt872RG-R_WJBEGfYXA", 
+"[::1]:52662", "request.id":"RcaSt872RG-R_WJBEGfYXA",
 "action":"indices:data/read/search", "request.name":"SearchRequest", "indices":["alias1"]}
 ====
 
@@ -389,8 +420,8 @@ another user that they have the necessary privileges to do so.
 [source,js]
 {"type":"audit", "timestamp":"2020-12-30T22:44:42,068+0200", "node.id":
 "0RMNyghkQYCc_gVd1G6tZQ", "event.type":"transport", "event.action":
-"run_as_granted", "user.name":"elastic", "user.run_as.name":"user1", 
-"user.realm":"reserved", "user.run_as.realm":"default_native", 
+"run_as_granted", "user.name":"elastic", "user.run_as.name":"user1",
+"user.realm":"reserved", "user.run_as.realm":"default_native",
 "user.roles":["superuser"], "origin.type":"rest", "origin.address":
 "[::1]:52623", "request.id":"dGqPTdEQSX2TAPS3cvc1qA", "action":
 "indices:data/read/search", "request.name":"SearchRequest", "indices":["alias1"]}
@@ -415,10 +446,10 @@ believed to have been tampered with.
 [%collapsible%open]
 ====
 [source,js]
-{"type":"audit", "timestamp":"2019-11-27T22:00:00,947+0200", "node.id": 
-"0RMNyghkQYCc_gVd1G6tZQ", "event.type": "rest", "event.action": 
+{"type":"audit", "timestamp":"2019-11-27T22:00:00,947+0200", "node.id":
+"0RMNyghkQYCc_gVd1G6tZQ", "event.type": "rest", "event.action":
 "tampered_request", "origin.address":"[::1]:50543", "url.path":
-"/twitter/_async_search", "url.query":"pretty", "request.method":"POST", 
+"/twitter/_async_search", "url.query":"pretty", "request.method":"POST",
 "request.id":"TqA9OisyQ8WTl1ivJUV1AA"}
 ====
 
@@ -602,6 +633,8 @@ request bodies of the corresponding security APIs.
 `apikeys`             ::     An object like `{"ids": <string_list>, "name": <string>, "owned_by_authenticated_user":
                              <boolean>, "user":{"name": <string>, "realm": <string>}}`.
 
+`service_token`       ::   An object like `{"namespace":<string>,"service":<string>,"name":<string>}`.
+
 ==== Extra audit event attributes for specific events
 
 There are a few events that have some more attributes in addition to those
@@ -619,6 +652,8 @@ that have been previously described:
                             this instead denotes the name of the _impersonated_ user.
                             If authenticated using an API key, this is
                             the name of the API key owner.
+                            If authenticated using a service account token, this is the
+                            service account principal, i.e. `namespace/service_name`.
   `user.realm`         ::   Name of the realm to which the _effective_ user
                             belongs. If authenticated using an API key, this is
                             the name of the realm to which the API key owner belongs.
@@ -652,7 +687,6 @@ that have been previously described:
                                  This attribute is only provided for authentication using a service account token.
                                  If the request authentication token is invalid or unparsable,
                                  this information might be missing.
-
 * `realm_authentication_failed`:
   `user.name`          ::    The name of the user that failed authentication.
   `realm`              ::    The name of the realm that rejected this authentication.

+ 1 - 1
x-pack/plugin/core/src/main/config/log4j2.properties

@@ -69,7 +69,7 @@ appender.audit_rolling.layout.pattern = {\
 # "url.query" the URI component after the path and before the fragment; it is percent (URL) encoded
 # "request.method" the method of the HTTP request, i.e. one of GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH, TRACE, CONNECT
 # "request.body" the content of the request body entity, JSON escaped
-# "request.id" a synthentic identifier for the incoming request, this is unique per incoming request, and consistent across all audit events generated by that request
+# "request.id" a synthetic identifier for the incoming request, this is unique per incoming request, and consistent across all audit events generated by that request
 # "action" an action is the most granular operation that is authorized and this identifies it in a namespaced way (internal)
 # "request.name" if the event is in connection to a transport message this is the name of the request class, similar to how rest requests are identified by the url path (internal)
 # "indices" the array of indices that the "action" is acting upon

+ 39 - 1
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java

@@ -59,6 +59,10 @@ import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappin
 import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingRequest;
 import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingAction;
 import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequest;
+import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction;
+import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest;
+import org.elasticsearch.xpack.core.security.action.service.DeleteServiceAccountTokenAction;
+import org.elasticsearch.xpack.core.security.action.service.DeleteServiceAccountTokenRequest;
 import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction;
 import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest;
 import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction;
@@ -203,7 +207,7 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener {
     public static final Set<String> SECURITY_CHANGE_ACTIONS = Set.of(PutUserAction.NAME, PutRoleAction.NAME, PutRoleMappingAction.NAME,
             SetEnabledAction.NAME, ChangePasswordAction.NAME, CreateApiKeyAction.NAME, GrantApiKeyAction.NAME, PutPrivilegesAction.NAME,
             DeleteUserAction.NAME, DeleteRoleAction.NAME, DeleteRoleMappingAction.NAME, InvalidateApiKeyAction.NAME,
-            DeletePrivilegesAction.NAME);
+            DeletePrivilegesAction.NAME, CreateServiceAccountTokenAction.NAME, DeleteServiceAccountTokenAction.NAME);
     private static final String FILTER_POLICY_PREFIX = setting("audit.logfile.events.ignore_filters.");
     // because of the default wildcard value (*) for the field filter, a policy with
     // an unspecified filter field will match events that have any value for that
@@ -594,6 +598,12 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener {
                 } else if (msg instanceof DeletePrivilegesRequest) {
                     assert DeletePrivilegesAction.NAME.equals(action);
                     securityChangeLogEntryBuilder(requestId).withRequestBody((DeletePrivilegesRequest) msg).build();
+                } else if (msg instanceof CreateServiceAccountTokenRequest) {
+                    assert CreateServiceAccountTokenAction.NAME.equals(action);
+                    securityChangeLogEntryBuilder(requestId).withRequestBody((CreateServiceAccountTokenRequest) msg).build();
+                } else if (msg instanceof DeleteServiceAccountTokenRequest) {
+                    assert DeleteServiceAccountTokenAction.NAME.equals(action);
+                    securityChangeLogEntryBuilder(requestId).withRequestBody((DeleteServiceAccountTokenRequest) msg).build();
                 } else {
                     throw new IllegalStateException("Unknown message class type [" + msg.getClass().getSimpleName() +
                             "] for the \"security change\" action [" + action + "]");
@@ -1166,6 +1176,34 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener {
             return this;
         }
 
+        LogEntryBuilder withRequestBody(CreateServiceAccountTokenRequest createServiceAccountTokenRequest) throws IOException {
+            logEntry.with(EVENT_ACTION_FIELD_NAME, "create_service_token");
+            XContentBuilder builder = JsonXContent.contentBuilder().humanReadable(true);
+            builder.startObject()
+                .startObject("service_token")
+                .field("namespace", createServiceAccountTokenRequest.getNamespace())
+                .field("service", createServiceAccountTokenRequest.getServiceName())
+                .field("name", createServiceAccountTokenRequest.getTokenName())
+                .endObject() // service_token
+                .endObject();
+            logEntry.with(CREATE_CONFIG_FIELD_NAME, Strings.toString(builder));
+            return this;
+        }
+
+        LogEntryBuilder withRequestBody(DeleteServiceAccountTokenRequest deleteServiceAccountTokenRequest) throws IOException {
+            logEntry.with(EVENT_ACTION_FIELD_NAME, "delete_service_token");
+            XContentBuilder builder = JsonXContent.contentBuilder().humanReadable(true);
+            builder.startObject()
+                .startObject("service_token")
+                .field("namespace", deleteServiceAccountTokenRequest.getNamespace())
+                .field("service", deleteServiceAccountTokenRequest.getServiceName())
+                .field("name", deleteServiceAccountTokenRequest.getTokenName())
+                .endObject() // service_token
+                .endObject();
+            logEntry.with(DELETE_CONFIG_FIELD_NAME, Strings.toString(builder));
+            return this;
+        }
+
         LogEntryBuilder withRestUriAndMethod(RestRequest request) {
             final int queryStringIndex = request.uri().indexOf('?');
             int queryStringLength = request.uri().indexOf('#');

+ 74 - 1
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java

@@ -63,6 +63,10 @@ import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappin
 import org.elasticsearch.xpack.core.security.action.rolemapping.DeleteRoleMappingRequest;
 import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingAction;
 import org.elasticsearch.xpack.core.security.action.rolemapping.PutRoleMappingRequest;
+import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction;
+import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest;
+import org.elasticsearch.xpack.core.security.action.service.DeleteServiceAccountTokenAction;
+import org.elasticsearch.xpack.core.security.action.service.DeleteServiceAccountTokenRequest;
 import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
 import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction;
 import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest;
@@ -1091,6 +1095,70 @@ public class LoggingAuditTrailTests extends ESTestCase {
         assertMsg(generatedDeleteUserAuditEventString, checkedFields.immutableMap());
     }
 
+    public void testSecurityConfigChangeEventFormattingForServiceAccountToken() {
+        final String requestId = randomRequestId();
+        final String[] expectedRoles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4));
+        final AuthorizationInfo authorizationInfo = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, expectedRoles);
+        final Authentication authentication = createAuthentication();
+
+        final String namespace = randomAlphaOfLengthBetween(3, 8);
+        final String serviceName = randomAlphaOfLengthBetween(3, 8);
+        final String tokenName = randomAlphaOfLengthBetween(3, 8);
+        final CreateServiceAccountTokenRequest createServiceAccountTokenRequest = new CreateServiceAccountTokenRequest(
+            namespace, serviceName, tokenName);
+
+        auditTrail.accessGranted(requestId, authentication, CreateServiceAccountTokenAction.NAME,
+            createServiceAccountTokenRequest, authorizationInfo);
+        List<String> output = CapturingLogger.output(logger.getName(), Level.INFO);
+        assertThat(output.size(), is(2));
+        String generatedCreateServiceAccountTokenAuditEventString = output.get(1);
+
+        final String expectedCreateServiceAccountTokenAuditEventString =
+            String.format(Locale.ROOT,
+                "\"create\":{\"service_token\":{\"namespace\":\"%s\",\"service\":\"%s\",\"name\":\"%s\"}}",
+                namespace, serviceName, tokenName);
+        assertThat(generatedCreateServiceAccountTokenAuditEventString, containsString(expectedCreateServiceAccountTokenAuditEventString));
+        generatedCreateServiceAccountTokenAuditEventString =
+            generatedCreateServiceAccountTokenAuditEventString.replace(", " + expectedCreateServiceAccountTokenAuditEventString, "");
+        MapBuilder<String, String> checkedFields = new MapBuilder<>(commonFields);
+        checkedFields.remove(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME);
+        checkedFields.remove(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME);
+        checkedFields.put("type", "audit")
+            .put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, "security_config_change")
+            .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "create_service_token")
+            .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId);
+        assertMsg(generatedCreateServiceAccountTokenAuditEventString, checkedFields.immutableMap());
+        // clear log
+        CapturingLogger.output(logger.getName(), Level.INFO).clear();
+
+        final DeleteServiceAccountTokenRequest deleteServiceAccountTokenRequest =
+            new DeleteServiceAccountTokenRequest(namespace, serviceName, tokenName);
+
+        auditTrail.accessGranted(requestId, authentication, DeleteServiceAccountTokenAction.NAME,
+            deleteServiceAccountTokenRequest, authorizationInfo);
+        output = CapturingLogger.output(logger.getName(), Level.INFO);
+        assertThat(output.size(), is(2));
+        String generatedDeleteServiceAccountTokenAuditEventString = output.get(1);
+
+        final String expectedDeleteServiceAccountTokenAuditEventString =
+            String.format(Locale.ROOT,
+                "\"delete\":{\"service_token\":{\"namespace\":\"%s\",\"service\":\"%s\",\"name\":\"%s\"}}",
+                namespace, serviceName, tokenName);
+        assertThat(generatedDeleteServiceAccountTokenAuditEventString, containsString(expectedDeleteServiceAccountTokenAuditEventString));
+        generatedDeleteServiceAccountTokenAuditEventString =
+            generatedDeleteServiceAccountTokenAuditEventString.replace(", " + expectedDeleteServiceAccountTokenAuditEventString, "");
+        checkedFields = new MapBuilder<>(commonFields);
+        checkedFields.remove(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME);
+        checkedFields.remove(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME);
+        checkedFields.put("type", "audit")
+            .put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, "security_config_change")
+            .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "delete_service_token")
+            .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId);
+        assertMsg(generatedDeleteServiceAccountTokenAuditEventString, checkedFields.immutableMap());
+        // clear log
+        CapturingLogger.output(logger.getName(), Level.INFO).clear();
+    }
+
     public void testAnonymousAccessDeniedTransport() throws Exception {
         final TransportRequest request = randomBoolean() ? new MockRequest(threadContext) : new MockIndicesRequest(threadContext);
 
@@ -1432,6 +1500,9 @@ public class LoggingAuditTrailTests extends ESTestCase {
         final String[] expectedRoles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4));
         final AuthorizationInfo authorizationInfo = () -> Collections.singletonMap(PRINCIPAL_ROLES_FIELD_NAME, expectedRoles);
         final Authentication authentication = createAuthentication();
+        final String namespace = randomAlphaOfLengthBetween(3, 8);
+        final String serviceName = randomAlphaOfLengthBetween(3, 8);
+        final String tokenName = randomAlphaOfLengthBetween(3, 8);
         Tuple<String, TransportRequest> actionAndRequest = randomFrom(new Tuple<>(PutUserAction.NAME, new PutUserRequest()),
                 new Tuple<>(PutRoleAction.NAME, new PutRoleRequest()),
                 new Tuple<>(PutRoleMappingAction.NAME, new PutRoleMappingRequest()),
@@ -1444,7 +1515,9 @@ public class LoggingAuditTrailTests extends ESTestCase {
                 new Tuple<>(DeleteRoleAction.NAME, new DeleteRoleRequest()),
                 new Tuple<>(DeleteRoleMappingAction.NAME, new DeleteRoleMappingRequest()),
                 new Tuple<>(InvalidateApiKeyAction.NAME, new InvalidateApiKeyRequest()),
-                new Tuple<>(DeletePrivilegesAction.NAME, new DeletePrivilegesRequest())
+                new Tuple<>(DeletePrivilegesAction.NAME, new DeletePrivilegesRequest()),
+                new Tuple<>(CreateServiceAccountTokenAction.NAME, new CreateServiceAccountTokenRequest(namespace, serviceName, tokenName)),
+                new Tuple<>(DeleteServiceAccountTokenAction.NAME, new DeleteServiceAccountTokenRequest(namespace, serviceName, tokenName))
         );
         auditTrail.accessGranted(requestId, authentication, actionAndRequest.v1(), actionAndRequest.v2(), authorizationInfo);
         List<String> output = CapturingLogger.output(logger.getName(), Level.INFO);