-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Create enrollment token API #72186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create enrollment token API #72186
Conversation
As part of the security on be default project, we will be offering an enrollment process that allows new nodes to join a cluster, or clients to bootstrap their configuration to communicate with a cluster that is already running with security enabled. This enrollment process is based on the use of enrollment tokens.
API can be called by the startup process or a user with appropriate privileges while elasticsearch is in the enrollment mode to obtain an enrollment token used to enroll a new node to the cluster or configure a new client to communicate with the cluster. Resolve: elastic#71438 Related: elastic#72129
|
Pinging @elastic/es-security (Team:Security) |
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java
Outdated
Show resolved
Hide resolved
jkakavas
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some comments, will take another look on Monday too
server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java
Outdated
Show resolved
Hide resolved
x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java
Outdated
Show resolved
Hide resolved
...ava/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java
Show resolved
Hide resolved
|
@elasticmachine update branch |
|
@jkakavas Refactored the code according to our discussion.
|
...ain/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java
Outdated
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java
Outdated
Show resolved
Hide resolved
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
|
@elasticmachine update branch |
...vileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java
Outdated
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Show resolved
Hide resolved
| builder.endObject(); | ||
| final String jsonString = Strings.toString(builder); | ||
| final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe log the enrollment token pre-encoding in TRACE level ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BigPandaToo ^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we should log this material...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@albertzaharovits what's your take on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually do not log credentials. In this case they are short lived, but on the other hand they are powerful (add a new node). I would not log it.
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
...n/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java
Outdated
Show resolved
Hide resolved
...vileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java
Show resolved
Hide resolved
x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem
Outdated
Show resolved
Hide resolved
| builder.endObject(); | ||
| final String jsonString = Strings.toString(builder); | ||
| final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BigPandaToo ^^
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc
Outdated
Show resolved
Hide resolved
x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc
Outdated
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
...ain/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Outdated
Show resolved
Hide resolved
|
The class is named SecurityDocumentationIT
…On Wednesday, May 12, 2021, Lyudmila Fokina ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In client/rest-high-level/src/test/java/org/elasticsearch/
client/SecurityIT.java
<#72186 (comment)>
:
> @@ -11,6 +11,7 @@
import org.apache.http.client.methods.HttpDelete;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.client.security.AuthenticateResponse;
+import org.elasticsearch.client.security.CreateEnrollmentTokenResponse;
Not sure where it is...
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#72186 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACOOCKDZETT22CDHMKTMKTLTNLAZJANCNFSM43PPMRRA>
.
|
|
@elasticmachine update branch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a very comprehensive review. It is looking good, yet I was a bit opinionated about some parts of it. This API is crucial and sensitive, so it's something we need to take extra care with, IMO.
But this needs tests, most notably integration tests (eg see ApiKeyIntegTests, and maybe ).SecurityDocumentationIT as Ioannis was saying
EDIT:
I would also propose we "shape" this more for node enrollment. It is likely we'll not give kibana a token that can be used to take over the cluster (ie the node enrollment token), but rather a different token. I won't dwell much on this, we can rectify names and docs later.
EDIT2:
We need to decide if we want to expose this enroll API to "unenrolled" clients (this PR contains code for it). I am of the opinion that we should not, because otherwise we have to think about a flow for the client to get the credentials and the cert; ie it is not sufficient to say "use these superuser credentials" for two reasons:
- the client cannot validate the TLS connection (in a sense, it can't because it is not enrolled), which makes it vulnerable to a MITM that can intercept the token.
- if the
superusercredentials can now add new nodes and generate HTTPS node certificates, it changes the threat model, eg superusers can be easily disabled, but a node participation to a cluster and associated HTTP TLS certificates cannot be revoked (at least it is very hard).
| public List<Route> routes() { | ||
| return List.of( | ||
| new Route(POST, "/_security/enrollment_token"), | ||
| new Route(PUT, "/_security/enrollment_token")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: In general I prefer we don't use PUT without a {name} URL part, but I know we're not consistent.
If you're also opinionated about this and don't like the PUT but copied it for consistency, we can remove it.
|
|
||
| @Override | ||
| public String getName() { | ||
| return "cluster_enrolment_token_action"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| return "cluster_enrolment_token_action"; | |
| return "security_create_enrollment_token_action"; |
| /** | ||
| * Rest endpoint to create an enrollment token | ||
| */ | ||
| public class RestCreateEnrollmentTokenAction extends SecurityBaseRestHandler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: make it final.
| @Override | ||
| protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { | ||
| if (Node.ENROLLMENT_ENABLED.get(settings) != true) { | ||
| throw new IllegalStateException("Enrollment mode is not enabled."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IllegalStateExceptions are a bit scary and they look unpolished IMO.
I tend to use them like an assert deep inside the internals. It's true that this API can be thought off as internal, but a little user friendliness can't hurt either, who knows how we might expose it.
My suggestion is to use an ElasticsearchException for this case, with a more detailed message, such as: "This node is not configured to issue enrollment tokens which allow clients and new nodes to communicate and join the cluster"
Also, this translates to HTTP 500, which is OKish, but it signals that the server hit an error when it actually followed the configuration. So in the spirit of improving user friendliness, I would opt for one of 501, 403 or 404 (see RestStatus).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think status code 4xx is more suitable in this scenario than 5xx. As I understand it, we should use 4xx to signal client error which can be rectified by the cilent (either change request parameters or in this case enable enrollment first). I think 400 is suitable for this case even though it is generic. Because this failure is not really about permission (403) or resource being unavailable (404).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @ywangd , 400 looks good to me too.
|
|
||
| <1> The enrollment token contains the following information: | ||
| - IP Address and port number for the interface where the Elasticsearch node is listening for HTTP connections; | ||
| - The fingerprint of the CA certificate that is used to sign the certificate that the Elasticsearch node presents for TLS on the HTTP layer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: mention that this is the SHA-1 fingerprint of the DER encoding for the certificate.
| ActionListener.wrap( | ||
| CreateApiKeyResponse -> { | ||
| try { | ||
| final String address = nodeService.info(false, false, false, false, false, false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use the same output format that nodes info uses, eg NetworkAddress.format(httpInfo.getAddress().publishAddress() .
| return; | ||
| } else { | ||
| final TimeValue expiration = TimeValue.timeValueSeconds(ENROLL_API_KEY_EXPIRATION_SEC); | ||
| final String[] clusterPrivileges = { "enroll" }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use ClusterPrivilegeResolver.ENROLL.getName().
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Show resolved
Hide resolved
...org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java
Show resolved
Hide resolved
ywangd
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I stil need look closer to the client code and tests. Thanks!
| "security.create_enrollment_token":{ | ||
| "documentation":{ | ||
| "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/security-create-enrollment-token.html", | ||
| "description":"Create an enrollment token to allow a new node to enroll in an existing secured elasticsearch cluster, or a client to configure itself to communicate with a secured elasticsearch cluster." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the live discussion, we can remove the "enroll client" bit here for now.
| "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/security-create-enrollment-token.html", | ||
| "description":"Create an enrollment token to allow a new node to enroll in an existing secured elasticsearch cluster, or a client to configure itself to communicate with a secured elasticsearch cluster." | ||
| }, | ||
| "stability":"stable", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be "experimental"?
| ==== {api-description-title} | ||
|
|
||
| The purpose of the create enrollment token API is to generate an enrollment with which user can enroll a new node | ||
| in an existing secured elasticsearch cluster, or a client can configure itself to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, we can remove "enroll client" here.
|
|
||
| @Override | ||
| public String toString() { | ||
| return "get_enrollment_token"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: It is not really necessary to override this method since the request class does not have any field itself and the super class's implementation should be sufficient. But if we are overriding, it's better to have it consistent with the class name, i.e. create_... instead of get_...?
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Base64; | ||
|
|
||
| public class CreateEnrollmentTokenResponseTests extends AbstractXContentTestCase<CreateEnrollmentTokenResponse> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AbstractXContentTestCase is mostly and makes more sense for testing the client side CreateEnrollmentTokenResponse class, where you already have a parser. And you test the client side response class can correctly parse the server side response. For test server side response itself, you can extend AbstractWireSerializingTestCase. But we are not consistent about it.
| builder.startObject(); | ||
| builder.field("adr", address); | ||
| builder.field("fgr", fingerprint); | ||
| builder.field("key", CreateApiKeyResponse.getKey().toString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only the "secret" part of an API key, you need the ID part to be returned as part of the response as well.
| new RestBuilderListener<>(channel) { | ||
| @Override | ||
| public RestResponse buildResponse(CreateEnrollmentTokenResponse response, XContentBuilder builder) throws Exception { | ||
| builder.startObject(); | ||
| builder.field("enrollment_token", response.getEnrollmentToken()); | ||
| builder.endObject(); | ||
| return new BytesRestResponse(RestStatus.OK, builder); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| new RestBuilderListener<>(channel) { | |
| @Override | |
| public RestResponse buildResponse(CreateEnrollmentTokenResponse response, XContentBuilder builder) throws Exception { | |
| builder.startObject(); | |
| builder.field("enrollment_token", response.getEnrollmentToken()); | |
| builder.endObject(); | |
| return new BytesRestResponse(RestStatus.OK, builder); | |
| } | |
| } | |
| new RestToXContentListener<>(channel) |
| --- | ||
| "Test get builtin privileges 7.x": | ||
| - skip: | ||
| version: "7.99.99 - " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| version: "7.99.99 - " | |
| version: "8.0.0 - " |
| generator.generateApiKey(securityContext.getAuthentication(), apiRequest, | ||
| ActionListener.wrap( | ||
| CreateApiKeyResponse -> { | ||
| try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this is not required, the exception is caught in ActionListener#doExecute.
|
After further consideration this PR seems tricky because of the design change discussions that are in progress. Summarizing the design discussions that are in-progress:
|
|
@BigPandaToo As we've discussed, I'm going to approve this PR, even if it's not addressing all the points above, considering that it is following the original design. But I will raise blocker issues that we'll need to sort out before we release. Please resolve all the comments by either fixing or saying that the respective point needs to be discussed further (and fixed in a follow-up). |
|
@elasticmachine update branch |
| @Override | ||
| public List<Route> routes() { | ||
| return List.of( | ||
| new Route(POST, "/_security/enrollment_token"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is what the design doc proposes, but I think it's a mistake.
I think all the enrollment APIs should be under a common URI path.
That is the doc proposes:
GET /_security/enroll_node
POST /_security/enroll_client
POST /_security/enrollment_token
Putting aside the HTTP Method question, think the lack of a common prefix is a problem.
I would propose that everything is under /_security/enroll/ or /_security/enrollment/
e.g.
POST /_security/enroll/node
POST /_security/enroll/client
POST /_security/enroll/token
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️ /_security/enrollment
|
A new one is posted instead: #73573 |
API can be called by the startup process or a user with appropriate
privileges while elasticsearch is in the enrollment mode to obtain an
enrollment token used to enroll a new node to the cluster or configure a
new client to communicate with the cluster.
Resolve: #71438
Related: #72129