Skip to content

Commit c88f93b

Browse files
authored
Service Accounts - features required for Fleet integration (#71514)
* Service Accounts - Initial bootstrap plumbing for essential classes (#70391) This PR is the initial effort to add essential classes for service accounts to lay down the foundation of future works. The classes are wired in places, but not yet been used. Also intentionally left out the actual credential store implementation. It is a good first commit which does not bring in too many changes. * Service Accounts - New CLI tool for managing file tokens (#70454) This is the second PR for service accounts. It adds a new CLI tool elasticsearch-service-tokens to manage file tokens. The file tokens are stored in the service_tokens file under the config directory. Out of the planned create, remove and list sub-commands, this PR only implements the create function since it is the most important one. The other two sub-commands will be handled in separate PRs. * Service Accounts - Authentication with file tokens (#70543) This the 3rd PR for service accounts. It adds support for authentication with file tokens. It also adds a cache for performance so that expensive pbkdf2 hashing does not have to be performed on every request. Adding a cache comes with its own housekeeping work around invalidation. This PR ensures that cache gets invalidated when underlying token file is changed. It does not implement APIs for active invalidation. It will be handled in a separate PR after the API token is in place. * [Test] Service Account - fix test assumption * [Test] Service Accounts - handle token names with leading hyphen (#70983) The CLI tool needs an option terminator (--) for another option names that begin with a hyphen. Otherwise it errors out with message of "not recognized option". The service account token name can begin with a hyphen. Hence we need to use -- when it is the case. An example of equivalent command line is ./bin/elasticsearch-service-tokens create elastic/fleet -- -lead-with-hyphen. * Service Accounts - Fleet integration (#70724) This PR implements rest of the pieces needed for Fleet integration, including: * Get service account role descriptor for authorization * API for creating service account token and storing in the security index * API for list tokens for a service account * New named privilege for manage service account * Mandate HTTP TLS for both service account auth and service account related APIs * Tests for API key related operations using service account * [Test] Service Accounts - Remove colon from invalid token name generator (#71099) The colon character is interpreted as the separate between token name and token secret. So if a token name contains a colon, it is in theory invalid. But the parser takes only the part before the colon as the token name and thus consider it as a valid token name. Subsequent authentication will still fail. But for tests, this generates a different exception and fails the expectation. This PR removes the colon char from being used to generate invalid token names for simplicity. * Fix for 7.x quirks
1 parent b4c9271 commit c88f93b

File tree

79 files changed

+5545
-214
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+5545
-214
lines changed

qa/os/src/test/java/org/elasticsearch/packaging/test/ArchiveTests.java

+2
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,8 @@ public void test94ElasticsearchNodeExecuteCliNotEsHomeWorkDir() throws Exception
459459
assertThat(result.stdout, containsString("Sets the passwords for reserved users"));
460460
result = sh.run(bin.usersTool + " -h");
461461
assertThat(result.stdout, containsString("Manages elasticsearch file users"));
462+
result = sh.run(bin.serviceTokensTool + " -h");
463+
assertThat(result.stdout, containsString("Manages elasticsearch service account file-tokens"));
462464
};
463465

464466
Platforms.onLinux(action);

qa/os/src/test/java/org/elasticsearch/packaging/util/Archives.java

+1
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ private static void verifyDefaultInstallation(Installation es, Distribution dist
199199
"elasticsearch-sql-cli",
200200
"elasticsearch-syskeygen",
201201
"elasticsearch-users",
202+
"elasticsearch-service-tokens",
202203
"x-pack-env",
203204
"x-pack-security-env",
204205
"x-pack-watcher-env"

qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java

+1
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ private static void verifyDefaultInstallation(Installation es) {
472472
"elasticsearch-sql-cli",
473473
"elasticsearch-syskeygen",
474474
"elasticsearch-users",
475+
"elasticsearch-service-tokens",
475476
"x-pack-env",
476477
"x-pack-security-env",
477478
"x-pack-watcher-env"

qa/os/src/test/java/org/elasticsearch/packaging/util/Installation.java

+1
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,6 @@ public class Executables {
189189
public final Executable sqlCli = new Executable("elasticsearch-sql-cli");
190190
public final Executable syskeygenTool = new Executable("elasticsearch-syskeygen");
191191
public final Executable usersTool = new Executable("elasticsearch-users");
192+
public final Executable serviceTokensTool = new Executable("elasticsearch-service-tokens");
192193
}
193194
}

qa/os/src/test/java/org/elasticsearch/packaging/util/Packages.java

+1
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ private static void verifyDefaultInstallation(Installation es, Distribution dist
227227
"elasticsearch-sql-cli",
228228
"elasticsearch-syskeygen",
229229
"elasticsearch-users",
230+
"elasticsearch-service-tokens",
230231
"x-pack-env",
231232
"x-pack-security-env",
232233
"x-pack-watcher-env"

x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
[role="xpack"]
23
[[security-api-get-builtin-privileges]]
34
=== Get builtin privileges API
@@ -83,6 +84,7 @@ A successful call returns an object with "cluster" and "index" fields.
8384
"manage_rollup",
8485
"manage_saml",
8586
"manage_security",
87+
"manage_service_account",
8688
"manage_slm",
8789
"manage_token",
8890
"manage_transform",

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java

+20
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,26 @@ private XPackSettings() {
202202
public static final Setting<Boolean> DIAGNOSE_TRUST_EXCEPTIONS_SETTING = Setting.boolSetting(
203203
"xpack.security.ssl.diagnose.trust", true, Setting.Property.NodeScope);
204204

205+
// TODO: This setting of hashing algorithm can share code with the one for password when pbkdf2_stretch is the default for both
206+
public static final Setting<String> SERVICE_TOKEN_HASHING_ALGORITHM = new Setting<>(
207+
new Setting.SimpleKey("xpack.security.authc.service_token_hashing.algorithm"),
208+
(s) -> "PBKDF2_STRETCH",
209+
Function.identity(),
210+
v -> {
211+
if (Hasher.getAvailableAlgoStoredHash().contains(v.toLowerCase(Locale.ROOT)) == false) {
212+
throw new IllegalArgumentException("Invalid algorithm: " + v + ". Valid values for password hashing are " +
213+
Hasher.getAvailableAlgoStoredHash().toString());
214+
} else if (v.regionMatches(true, 0, "pbkdf2", 0, "pbkdf2".length())) {
215+
try {
216+
SecretKeyFactory.getInstance("PBKDF2withHMACSHA512");
217+
} catch (NoSuchAlgorithmException e) {
218+
throw new IllegalArgumentException(
219+
"Support for PBKDF2WithHMACSHA512 must be available in order to use any of the " +
220+
"PBKDF2 algorithms for the [xpack.security.authc.service_token_hashing.algorithm] setting.", e);
221+
}
222+
}
223+
}, Property.NodeScope);
224+
205225
public static final List<String> DEFAULT_SUPPORTED_PROTOCOLS;
206226

207227
static {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.action.ActionType;
11+
12+
public class CreateServiceAccountTokenAction extends ActionType<CreateServiceAccountTokenResponse> {
13+
14+
public static final String NAME = "cluster:admin/xpack/security/service_account/token/create";
15+
public static final CreateServiceAccountTokenAction INSTANCE = new CreateServiceAccountTokenAction();
16+
17+
private CreateServiceAccountTokenAction() {
18+
super(NAME, CreateServiceAccountTokenResponse::new);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.action.ActionRequest;
11+
import org.elasticsearch.action.ActionRequestValidationException;
12+
import org.elasticsearch.action.support.WriteRequest;
13+
import org.elasticsearch.common.Strings;
14+
import org.elasticsearch.common.io.stream.StreamInput;
15+
import org.elasticsearch.common.io.stream.StreamOutput;
16+
17+
import java.io.IOException;
18+
import java.util.Objects;
19+
20+
import static org.elasticsearch.action.ValidateActions.addValidationError;
21+
22+
public class CreateServiceAccountTokenRequest extends ActionRequest {
23+
24+
private final String namespace;
25+
private final String serviceName;
26+
private final String tokenName;
27+
private WriteRequest.RefreshPolicy refreshPolicy = WriteRequest.RefreshPolicy.WAIT_UNTIL;
28+
29+
public CreateServiceAccountTokenRequest(String namespace, String serviceName, String tokenName) {
30+
this.namespace = namespace;
31+
this.serviceName = serviceName;
32+
this.tokenName = tokenName;
33+
}
34+
35+
public CreateServiceAccountTokenRequest(StreamInput in) throws IOException {
36+
super(in);
37+
this.namespace = in.readString();
38+
this.serviceName = in.readString();
39+
this.tokenName = in.readString();
40+
this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in);
41+
}
42+
43+
public String getNamespace() {
44+
return namespace;
45+
}
46+
47+
public String getServiceName() {
48+
return serviceName;
49+
}
50+
51+
public String getTokenName() {
52+
return tokenName;
53+
}
54+
55+
public WriteRequest.RefreshPolicy getRefreshPolicy() {
56+
return refreshPolicy;
57+
}
58+
59+
public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
60+
this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null");
61+
}
62+
63+
@Override
64+
public boolean equals(Object o) {
65+
if (this == o)
66+
return true;
67+
if (o == null || getClass() != o.getClass())
68+
return false;
69+
CreateServiceAccountTokenRequest that = (CreateServiceAccountTokenRequest) o;
70+
return Objects.equals(namespace, that.namespace) && Objects.equals(serviceName, that.serviceName)
71+
&& Objects.equals(tokenName, that.tokenName) && refreshPolicy == that.refreshPolicy;
72+
}
73+
74+
@Override
75+
public int hashCode() {
76+
return Objects.hash(namespace, serviceName, tokenName, refreshPolicy);
77+
}
78+
79+
@Override
80+
public void writeTo(StreamOutput out) throws IOException {
81+
super.writeTo(out);
82+
out.writeString(namespace);
83+
out.writeString(serviceName);
84+
out.writeString(tokenName);
85+
refreshPolicy.writeTo(out);
86+
}
87+
88+
@Override
89+
public ActionRequestValidationException validate() {
90+
ActionRequestValidationException validationException = null;
91+
if (Strings.isNullOrEmpty(namespace)) {
92+
validationException = addValidationError("service account namespace is required", validationException);
93+
}
94+
95+
if (Strings.isNullOrEmpty(serviceName)) {
96+
validationException = addValidationError("service account service-name is required", validationException);
97+
}
98+
99+
if (Strings.isNullOrEmpty(tokenName)) {
100+
validationException = addValidationError("service account token name is required", validationException);
101+
} else {
102+
if (tokenName.length() > 256) {
103+
validationException = addValidationError(
104+
"service account token name may not be more than 256 characters long", validationException);
105+
}
106+
if (tokenName.equals(tokenName.trim()) == false) {
107+
validationException = addValidationError(
108+
"service account token name may not begin or end with whitespace", validationException);
109+
}
110+
if (tokenName.startsWith("_")) {
111+
validationException = addValidationError(
112+
"service account token name may not begin with an underscore", validationException);
113+
}
114+
}
115+
return validationException;
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.action.ActionResponse;
11+
import org.elasticsearch.common.Nullable;
12+
import org.elasticsearch.common.io.stream.StreamInput;
13+
import org.elasticsearch.common.io.stream.StreamOutput;
14+
import org.elasticsearch.common.settings.SecureString;
15+
import org.elasticsearch.common.xcontent.ToXContentObject;
16+
import org.elasticsearch.common.xcontent.XContentBuilder;
17+
18+
import java.io.IOException;
19+
import java.util.Objects;
20+
21+
public class CreateServiceAccountTokenResponse extends ActionResponse implements ToXContentObject {
22+
23+
@Nullable
24+
private final String name;
25+
@Nullable
26+
private final SecureString value;
27+
28+
private CreateServiceAccountTokenResponse(boolean created, String name, SecureString value) {
29+
this.name = name;
30+
this.value = value;
31+
}
32+
33+
public CreateServiceAccountTokenResponse(StreamInput in) throws IOException {
34+
super(in);
35+
this.name = in.readOptionalString();
36+
this.value = in.readOptionalSecureString();
37+
}
38+
39+
public String getName() {
40+
return name;
41+
}
42+
43+
public SecureString getValue() {
44+
return value;
45+
}
46+
47+
@Override
48+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
49+
builder.startObject()
50+
.field("created", true)
51+
.field("token")
52+
.startObject()
53+
.field("name", name)
54+
.field("value", value.toString())
55+
.endObject()
56+
.endObject();
57+
return builder;
58+
}
59+
60+
@Override
61+
public void writeTo(StreamOutput out) throws IOException {
62+
out.writeOptionalString(name);
63+
out.writeOptionalSecureString(value);
64+
}
65+
66+
@Override
67+
public boolean equals(Object o) {
68+
if (this == o)
69+
return true;
70+
if (o == null || getClass() != o.getClass())
71+
return false;
72+
CreateServiceAccountTokenResponse that = (CreateServiceAccountTokenResponse) o;
73+
return Objects.equals(name, that.name) && Objects.equals(value, that.value);
74+
}
75+
76+
@Override
77+
public int hashCode() {
78+
return Objects.hash(name, value);
79+
}
80+
81+
public static CreateServiceAccountTokenResponse created(String name, SecureString value) {
82+
return new CreateServiceAccountTokenResponse(true, name, value);
83+
}
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.action.ActionType;
11+
12+
public class GetServiceAccountTokensAction extends ActionType<GetServiceAccountTokensResponse> {
13+
14+
public static final String NAME = "cluster:admin/xpack/security/service_account/token/get";
15+
public static final GetServiceAccountTokensAction INSTANCE = new GetServiceAccountTokensAction();
16+
17+
public GetServiceAccountTokensAction() {
18+
super(NAME, GetServiceAccountTokensResponse::new);
19+
}
20+
}

0 commit comments

Comments
 (0)