Add the Enroll Kibana API (#72207)

This change adds the Enroll Kibana API that enables a Kibana instance to
configure itself to communicate with a secured elasticsearch cluster
This commit is contained in:
Ioannis Kakavas 2021-06-23 22:58:46 +03:00 committed by GitHub
parent 00a1ae1b04
commit 82e7fbda53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 927 additions and 23 deletions

View File

@ -273,5 +273,4 @@ public final class ClusterClient {
return restHighLevelClient.performRequestAsync(componentTemplatesRequest,
ClusterRequestConverters::componentTemplatesExist, options, RestHighLevelClient::convertExistsResponse, listener, emptySet());
}
}

View File

@ -78,6 +78,8 @@ import org.elasticsearch.client.security.PutRoleRequest;
import org.elasticsearch.client.security.PutRoleResponse;
import org.elasticsearch.client.security.PutUserRequest;
import org.elasticsearch.client.security.PutUserResponse;
import org.elasticsearch.client.security.KibanaEnrollmentRequest;
import org.elasticsearch.client.security.KibanaEnrollmentResponse;
import java.io.IOException;
@ -1299,7 +1301,6 @@ public final class SecurityClient {
DelegatePkiAuthenticationResponse::fromXContent, listener, emptySet());
}
/**
* Allows a node to join to a cluster with security features enabled using the Enroll Node API.
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
@ -1321,4 +1322,33 @@ public final class SecurityClient {
return restHighLevelClient.performRequestAsyncAndParseEntity(NodeEnrollmentRequest.INSTANCE, NodeEnrollmentRequest::getRequest,
options, NodeEnrollmentResponse::fromXContent, listener, emptySet());
}
/**
* Allows a kibana instance to configure itself to connect to a secured cluster using the Enroll Kibana API
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public KibanaEnrollmentResponse enrollKibana(RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(
KibanaEnrollmentRequest.INSTANCE,
KibanaEnrollmentRequest::getRequest,
options,
KibanaEnrollmentResponse::fromXContent,
emptySet());
}
/**
* Asynchronously allows a kibana instance to configure itself to connect to a secured cluster using the Enroll Kibana API
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @param listener the listener to be notified upon request completion
*/
public Cancellable enrollKibanaAsync(
RequestOptions options,
ActionListener<KibanaEnrollmentResponse> listener) {
return restHighLevelClient.performRequestAsyncAndParseEntity(
KibanaEnrollmentRequest.INSTANCE,
KibanaEnrollmentRequest::getRequest, options, KibanaEnrollmentResponse::fromXContent, listener, emptySet());
}
}

View File

@ -0,0 +1,25 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.client.security;
import org.apache.http.client.methods.HttpGet;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Validatable;
public final class KibanaEnrollmentRequest implements Validatable {
public static final KibanaEnrollmentRequest INSTANCE = new KibanaEnrollmentRequest();
private KibanaEnrollmentRequest() {
}
public Request getRequest() {
return new Request(HttpGet.METHOD_NAME, "/_security/enroll/kibana");
}
}

View File

@ -0,0 +1,63 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.client.security;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ParseField;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Objects;
public final class KibanaEnrollmentResponse {
private SecureString password;
private String httpCa;
public KibanaEnrollmentResponse(SecureString password, String httpCa) {
this.password = password;
this.httpCa = httpCa;
}
public SecureString getPassword() { return password; }
public String getHttpCa() {
return httpCa;
}
private static final ParseField PASSWORD = new ParseField("password");
private static final ParseField HTTP_CA = new ParseField("http_ca");
@SuppressWarnings("unchecked")
private static final ConstructingObjectParser<KibanaEnrollmentResponse, Void> PARSER =
new ConstructingObjectParser<>(
KibanaEnrollmentResponse.class.getName(), true,
a -> new KibanaEnrollmentResponse(new SecureString(((String) a[0]).toCharArray()), (String) a[1]));
static {
PARSER.declareString(ConstructingObjectParser.constructorArg(), PASSWORD);
PARSER.declareString(ConstructingObjectParser.constructorArg(), HTTP_CA);
}
public static KibanaEnrollmentResponse fromXContent(XContentParser parser) throws IOException {
return PARSER.apply(parser, null);
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KibanaEnrollmentResponse that = (KibanaEnrollmentResponse) o;
return password.equals(that.password) && httpCa.equals(that.httpCa);
}
@Override public int hashCode() {
return Objects.hash(password, httpCa);
}
}

View File

@ -24,6 +24,6 @@ public final class NodeEnrollmentRequest implements Validatable {
}
public Request getRequest() {
return new Request(HttpGet.METHOD_NAME, "/_security/enroll_node");
return new Request(HttpGet.METHOD_NAME, "/_security/enroll/node");
}
}

View File

@ -386,5 +386,4 @@ public class ClusterClientIT extends ESRestHighLevelClientTestCase {
assertFalse(exist);
}
}

View File

@ -34,6 +34,7 @@ import org.elasticsearch.client.security.user.privileges.GlobalPrivilegesTests;
import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
import org.elasticsearch.client.security.user.privileges.IndicesPrivilegesTests;
import org.elasticsearch.client.security.user.privileges.Role;
import org.elasticsearch.client.security.KibanaEnrollmentResponse;
import org.elasticsearch.core.CharArrays;
import java.io.IOException;
@ -208,6 +209,17 @@ public class SecurityIT extends ESRestHighLevelClientTestCase {
assertThat(nodesAddresses.size(), equalTo(1));
}
@AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys")
public void testEnrollKibana() throws Exception {
KibanaEnrollmentResponse kibanaResponse =
execute(highLevelClient().security()::enrollKibana, highLevelClient().security()::enrollKibanaAsync, RequestOptions.DEFAULT);
assertThat(kibanaResponse, notNullValue());
assertThat(kibanaResponse.getHttpCa()
, endsWith("OWFyeGNmcwovSDJReE1tSG1leXJRaWxYbXJPdk9PUDFTNGRrSTFXbFJLOFdaN3c9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"));
assertNotNull(kibanaResponse.getPassword());
assertThat(kibanaResponse.getPassword().toString().length(), equalTo(14));
}
private void deleteUser(User user) throws IOException {
final Request deleteUserRequest = new Request(HttpDelete.METHOD_NAME, "/_security/user/" + user.getUsername());
highLevelClient().getLowLevelClient().performRequest(deleteUserRequest);

View File

@ -100,6 +100,7 @@ import org.elasticsearch.client.security.user.privileges.Role;
import org.elasticsearch.client.security.user.privileges.Role.ClusterPrivilegeName;
import org.elasticsearch.client.security.user.privileges.Role.IndexPrivilegeName;
import org.elasticsearch.client.security.user.privileges.UserIndicesPrivileges;
import org.elasticsearch.client.security.KibanaEnrollmentResponse;
import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.SecureString;
@ -2894,7 +2895,6 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
// <1>
}
@Override
public void onFailure(Exception e) {
// <2>
@ -2911,6 +2911,47 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
}
}
@AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys")
public void testKibanaEnrollment() throws Exception {
RestHighLevelClient client = highLevelClient();
{
// tag::kibana-enrollment-execute
KibanaEnrollmentResponse response = client.security().enrollKibana(RequestOptions.DEFAULT);
// end::kibana-enrollment-execute
// tag::kibana-enrollment-response
SecureString password = response.getPassword(); // <1>
String httoCa = response.getHttpCa(); // <2>
// end::kibana-enrollment-response
assertThat(password.length(), equalTo(14));
}
{
// tag::kibana-enrollment-execute-listener
ActionListener<KibanaEnrollmentResponse> listener =
new ActionListener<KibanaEnrollmentResponse>() {
@Override
public void onResponse(KibanaEnrollmentResponse response) {
// <1>
}
@Override
public void onFailure(Exception e) {
// <2>
}};
// end::kibana-enrollment-execute-listener
final CountDownLatch latch = new CountDownLatch(1);
listener = new LatchedActionListener<>(listener, latch);
// tag::kibana-enrollment-execute-async
client.security().enrollKibanaAsync(RequestOptions.DEFAULT, listener);
// end::kibana-enrollment-execute-async
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
}
private X509Certificate readCertForPkiDelegation(String certificateName) throws Exception {
Path path = getDataPath("/org/elasticsearch/client/security/delegate_pki/" + certificateName);
try (InputStream in = Files.newInputStream(path)) {

View File

@ -0,0 +1,66 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.client.security;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
import java.io.IOException;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;
public class KibanaErnollmentResponseTests extends ESTestCase {
public void testFromXContent() throws IOException {
final String password = randomAlphaOfLength(14);
final String httpCa = randomAlphaOfLength(50);
final List<String> nodesAddresses = randomList(2, 10, () -> buildNewFakeTransportAddress().toString());
final XContentType xContentType = randomFrom(XContentType.values());
final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
builder.startObject().field("password", password).field("http_ca", httpCa).field("nodes_addresses", nodesAddresses).endObject();
BytesReference xContent = BytesReference.bytes(builder);
final KibanaEnrollmentResponse response = KibanaEnrollmentResponse.fromXContent(createParser(xContentType.xContent(), xContent));
assertThat(response.getPassword(), equalTo(password));
assertThat(response.getHttpCa(), equalTo(httpCa));
}
public void testEqualsHashCode() {
final SecureString password = new SecureString(randomAlphaOfLength(14).toCharArray());
final String httpCa = randomAlphaOfLength(50);
KibanaEnrollmentResponse kibanaEnrollmentResponse = new KibanaEnrollmentResponse(password, httpCa);
EqualsHashCodeTestUtils.checkEqualsAndHashCode(kibanaEnrollmentResponse,
(original) -> new KibanaEnrollmentResponse(original.getPassword(), original.getHttpCa()));
EqualsHashCodeTestUtils.checkEqualsAndHashCode(kibanaEnrollmentResponse,
(original) -> new KibanaEnrollmentResponse(original.getPassword(), original.getHttpCa()),
KibanaErnollmentResponseTests::mutateTestItem);
}
private static KibanaEnrollmentResponse mutateTestItem(KibanaEnrollmentResponse original) {
switch (randomIntBetween(0, 1)) {
case 0:
return new KibanaEnrollmentResponse(new SecureString(randomAlphaOfLength(14).toCharArray()),
original.getHttpCa());
case 1:
return new KibanaEnrollmentResponse(original.getPassword(), randomAlphaOfLength(51));
default:
return new KibanaEnrollmentResponse(original.getPassword(),
original.getHttpCa());
}
}
}

View File

@ -0,0 +1,47 @@
--
:api: kibana-enrollment
:request: KibanaEnrollmentRequest
:response: KibanaEnrollmentResponse
--
[id="{upid}-{api}"]
=== Enroll Kibana API
Allows a kibana instance to configure itself to communicate with a secured {es} cluster.
include::../execution.asciidoc[]
[id="{upid}-{api}-response"]
==== Enroll Kibana Response
The returned +{response}+ allows to retrieve information about the
executed operation as follows:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests-file}[{api-kibana-response]
--------------------------------------------------
<1> The password for the `kibana_system` user
<2> The CA certificate that has signed the certificate that the cluster uses for TLS on the HTTP layer,
as a Base64 encoded string of the ASN.1 DER encoding of the certificate.
[id="{upid}-{api}-execute-async"]
==== Asynchronous Execution
This request can be executed asynchronously using the `security().enrollClientAsync()`
method:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests-file}[{api}-execute-async]
--------------------------------------------------
A typical listener for a `KibanaEnrollmentResponse` looks like:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests-file}[{api}-execute-listener]
--------------------------------------------------
<1> Called when the execution is successfully completed. The response is
provided as an argument
<2> Called in case of failure. The raised exception is provided as an argument

View File

@ -0,0 +1,24 @@
{
"security.enroll_kibana":{
"documentation":{
"url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/security-api-enroll-kibana.html",
"description":"Allows a kibana instance to configure itself to communicate with a secured elasticsearch cluster."
},
"stability":"stable",
"visibility":"public",
"headers":{
"accept": [ "application/json"],
"content_type": ["application/json"]
},
"url":{
"paths":[
{
"path":"/_security/enroll/kibana",
"methods":[
"GET"
]
}
]
}
}
}

View File

@ -13,7 +13,7 @@
"url":{
"paths":[
{
"path":"/_security/enroll_node",
"path":"/_security/enroll/node",
"methods":[
"GET"
]

View File

@ -129,6 +129,7 @@ security enabled or to allow a client to configure itself to communicate with
a secured {es} cluster
* <<security-api-node-enrollment, Enroll a new node>>
* <<security-api-kibana-enrollment, Enroll a new kibana instance>>
include::security/authenticate.asciidoc[]
@ -139,6 +140,7 @@ include::security/clear-privileges-cache.asciidoc[]
include::security/clear-api-key-cache.asciidoc[]
include::security/clear-service-token-caches.asciidoc[]
include::security/create-api-keys.asciidoc[]
include::security/enroll-kibana.asciidoc[]
include::security/put-app-privileges.asciidoc[]
include::security/create-role-mappings.asciidoc[]
include::security/create-roles.asciidoc[]

View File

@ -0,0 +1,47 @@
[[security-api-kibana-enrollment]]
=== Enroll {kib} API
++++
<titleabbrev>Enroll {kib}</titleabbrev>
++++
Enables a {kib} instance to configure itself for communication with a secured {es} cluster.
[[security-api-kibana-enrollment-request]]
==== {api-request-title}
`GET /_security/enroll/kibana`
[[security-api-kibana-enrollment-prereqs]]
==== {api-prereq-title}
[[security-api-kibana-enrollment-desc]]
==== {api-description-title}
The purpose of the enroll kibana API is to allow a kibana instance to configure itself to
communicate with an {es} cluster that is already configured with security features
enabled.
[[security-api-client-enrollment-examples]]
==== {api-examples-title}
The following example shows how to enroll a {kib} instance.
[source,console]
----
GET /_security/enroll/kibana
----
// TEST[skip:we need to enable HTTP TLS for the docs cluster]
The API returns the following response:
[source,console_result]
----
{
"password" : "longsecurepassword", <1>
"http_ca" : "MIIJlAIBAzCCCVoGCSqGSIb3....vsDfsA3UZBAjEPfhubpQysAICCAA=", <2>
}
----
<1> The password for the `kibana_system` user.
<2> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer.
The certificate is returned as a Base64 encoded string of the ASN.1 DER encoding of the certificate

View File

@ -9,12 +9,7 @@ Allows a new node to join an existing cluster with security features enabled.
[[security-api-node-enrollment-api-request]]
==== {api-request-title}
`GET /_security/enroll_node`
[[security-api-node-enrollment-api-prereqs]]
==== {api-prereq-title}
* You must have the `enroll` <<privileges-list-cluster,cluster privilege>> to use this API.
`GET /_security/enroll/node`
[[security-api-node-enrollment-api-desc]]
==== {api-description-title}
@ -32,7 +27,7 @@ caller to generate valid signed certificates for the HTTP layer of all nodes in
[source,console]
--------------------------------------------------
GET /security/enroll_node
GET /security/enroll/node
--------------------------------------------------
// TEST[skip:Determine behavior for keystore with multiple keys]
The API returns a response such as

View File

@ -0,0 +1,20 @@
/*
* 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.enrollment;
import org.elasticsearch.action.ActionType;
public final class KibanaEnrollmentAction extends ActionType<KibanaEnrollmentResponse> {
public static final String NAME = "cluster:admin/xpack/security/enroll/kibana";
public static final KibanaEnrollmentAction INSTANCE = new KibanaEnrollmentAction();
private KibanaEnrollmentAction() {
super(NAME, KibanaEnrollmentResponse::new);
}
}

View File

@ -0,0 +1,29 @@
/*
* 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.enrollment;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import java.io.IOException;
public final class KibanaEnrollmentRequest extends ActionRequest {
public KibanaEnrollmentRequest() {
}
public KibanaEnrollmentRequest(StreamInput in) throws IOException {
super(in);
}
@Override public ActionRequestValidationException validate() {
return null;
}
}

View File

@ -0,0 +1,85 @@
/*
* 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.enrollment;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ParseField;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Objects;
public final class KibanaEnrollmentResponse extends ActionResponse implements ToXContentObject {
private static final ParseField PASSWORD = new ParseField("password");
private static final ParseField HTTP_CA = new ParseField("http_ca");
@SuppressWarnings("unchecked")
private static final ConstructingObjectParser<KibanaEnrollmentResponse, Void> PARSER =
new ConstructingObjectParser<>(
KibanaEnrollmentResponse.class.getName(), true,
a -> new KibanaEnrollmentResponse(new SecureString(((String) a[0]).toCharArray()), (String) a[1]));
static {
PARSER.declareString(ConstructingObjectParser.constructorArg(), PASSWORD);
PARSER.declareString(ConstructingObjectParser.constructorArg(), HTTP_CA);
}
private final SecureString password;
private final String httpCa;
public KibanaEnrollmentResponse(StreamInput in) throws IOException {
super(in);
password = in.readSecureString();
httpCa = in.readString();
}
public KibanaEnrollmentResponse(SecureString password, String httpCa) {
this.password = password;
this.httpCa = httpCa;
}
public SecureString getPassword() { return password; }
public String getHttpCa() {
return httpCa;
}
@Override public XContentBuilder toXContent(
XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(PASSWORD.getPreferredName(), password.toString());
builder.field(HTTP_CA.getPreferredName(), httpCa);
return builder.endObject();
}
@Override public void writeTo(StreamOutput out) throws IOException {
out.writeSecureString(password);
out.writeString(httpCa);
}
public static KibanaEnrollmentResponse fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KibanaEnrollmentResponse response = (KibanaEnrollmentResponse) o;
return password.equals(response.password) && httpCa.equals(response.httpCa);
}
@Override public int hashCode() {
return Objects.hash(password, httpCa);
}
}

View File

@ -11,7 +11,7 @@ import org.elasticsearch.action.ActionType;
public final class NodeEnrollmentAction extends ActionType<NodeEnrollmentResponse> {
public static final String NAME = "cluster:admin/xpack/security/enrollment/enroll/node";
public static final String NAME = "cluster:admin/xpack/security/enroll/node";
public static final NodeEnrollmentAction INSTANCE = new NodeEnrollmentAction();
private NodeEnrollmentAction() {

View File

@ -33,8 +33,10 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* A key configuration that is backed by a {@link KeyStore}
@ -127,6 +129,24 @@ public class StoreKeyConfig extends KeyConfig {
return certificates;
}
/**
* Returns all certificates that can be found in the keystore, either as part of a PrivateKeyEntry or a TrustedCertificateEntry.
* Duplicates are removed.
*/
public Collection<X509Certificate> x509Certificates(Environment environment) throws GeneralSecurityException, IOException {
final KeyStore trustStore = getStore(CertParsingUtils.resolvePath(keyStorePath, environment), keyStoreType, keyStorePassword);
final Set<X509Certificate> certificates = new HashSet<>();
final Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
final Certificate certificate = trustStore.getCertificate(alias);
if (certificate instanceof X509Certificate) {
certificates.add((X509Certificate) certificate);
}
}
return certificates;
}
@Override
List<Path> filesToMonitor(@Nullable Environment environment) {
if (keyStorePath == null) {

View File

@ -0,0 +1,47 @@
/*
* 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.enrollment;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractXContentTestCase;
import java.io.IOException;
import static org.hamcrest.Matchers.is;
public class KibanaEnrollmentResponseTests extends AbstractXContentTestCase<KibanaEnrollmentResponse> {
@Override protected KibanaEnrollmentResponse createTestInstance() {
return new KibanaEnrollmentResponse(
new SecureString(randomAlphaOfLength(14).toCharArray()),
randomAlphaOfLength(50));
}
@Override protected KibanaEnrollmentResponse doParseInstance(XContentParser parser) throws IOException {
return KibanaEnrollmentResponse.fromXContent(parser);
}
@Override protected boolean supportsUnknownFields() {
return false;
}
public void testSerialization() throws IOException{
KibanaEnrollmentResponse response = createTestInstance();
try (BytesStreamOutput out = new BytesStreamOutput()) {
response.writeTo(out);
try (StreamInput in = out.bytes().streamInput()) {
KibanaEnrollmentResponse serialized = new KibanaEnrollmentResponse(in);
assertThat(response.getHttpCa(), is(serialized.getHttpCa()));
assertThat(response.getPassword(), is(serialized.getPassword()));
}
}
}
}

View File

@ -175,7 +175,8 @@ public class Constants {
"cluster:admin/xpack/security/api_key/invalidate",
"cluster:admin/xpack/security/cache/clear",
"cluster:admin/xpack/security/delegate_pki",
"cluster:admin/xpack/security/enrollment/enroll/node",
"cluster:admin/xpack/security/enroll/node",
"cluster:admin/xpack/security/enroll/kibana",
"cluster:admin/xpack/security/oidc/authenticate",
"cluster:admin/xpack/security/oidc/logout",
"cluster:admin/xpack/security/oidc/prepare",

View File

@ -92,6 +92,7 @@ import org.elasticsearch.xpack.core.security.action.GetApiKeyAction;
import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction;
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.enrollment.NodeEnrollmentAction;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction;
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction;
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction;
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction;
@ -163,6 +164,7 @@ import org.elasticsearch.xpack.security.action.TransportGetApiKeyAction;
import org.elasticsearch.xpack.security.action.TransportGrantApiKeyAction;
import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction;
import org.elasticsearch.xpack.security.action.enrollment.TransportNodeEnrollmentAction;
import org.elasticsearch.xpack.security.action.enrollment.TransportKibanaEnrollmentAction;
import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter;
import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction;
import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectLogoutAction;
@ -247,6 +249,7 @@ import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestGrantApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.enrollment.RestNodeEnrollmentAction;
import org.elasticsearch.xpack.security.rest.action.enrollment.RestKibanaEnrollAction;
import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction;
import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction;
import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectAuthenticateAction;
@ -888,6 +891,7 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin,
new ActionHandler<>(DeleteServiceAccountTokenAction.INSTANCE, TransportDeleteServiceAccountTokenAction.class),
new ActionHandler<>(GetServiceAccountCredentialsAction.INSTANCE, TransportGetServiceAccountCredentialsAction.class),
new ActionHandler<>(GetServiceAccountAction.INSTANCE, TransportGetServiceAccountAction.class),
new ActionHandler<>(KibanaEnrollmentAction.INSTANCE, TransportKibanaEnrollmentAction.class),
new ActionHandler<>(NodeEnrollmentAction.INSTANCE, TransportNodeEnrollmentAction.class),
usageAction,
infoAction);
@ -954,6 +958,7 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin,
new RestDeleteServiceAccountTokenAction(settings, getLicenseState()),
new RestGetServiceAccountCredentialsAction(settings, getLicenseState()),
new RestGetServiceAccountAction(settings, getLicenseState()),
new RestKibanaEnrollAction(settings, getLicenseState()),
new RestNodeEnrollmentAction(settings, getLicenseState())
);
}

View File

@ -0,0 +1,123 @@
/*
* 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.enrollment;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.OriginSettingClient;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.env.Environment;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentRequest;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentResponse;
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.ChangePasswordRequestBuilder;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.ssl.KeyConfig;
import org.elasticsearch.xpack.core.ssl.SSLService;
import org.elasticsearch.xpack.core.ssl.StoreKeyConfig;
import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
public class TransportKibanaEnrollmentAction extends HandledTransportAction<KibanaEnrollmentRequest, KibanaEnrollmentResponse> {
private static final Logger logger = LogManager.getLogger(TransportKibanaEnrollmentAction.class);
private final Environment environment;
private final Client client;
private final SSLService sslService;
@Inject public TransportKibanaEnrollmentAction(
TransportService transportService,
Client client,
SSLService sslService,
Environment environment,
ActionFilters actionFilters) {
super(KibanaEnrollmentAction.NAME, transportService, actionFilters, KibanaEnrollmentRequest::new);
this.environment = environment;
// Should we use a specific origin for this ? Are we satisfied with the auditability of the change password request as-is ?
this.client = new OriginSettingClient(client, SECURITY_ORIGIN);
this.sslService = sslService;
}
@Override protected void doExecute(Task task, KibanaEnrollmentRequest request, ActionListener<KibanaEnrollmentResponse> listener) {
final KeyConfig keyConfig = sslService.getHttpTransportSSLConfiguration().keyConfig();
if (keyConfig instanceof StoreKeyConfig == false) {
listener.onFailure(new ElasticsearchException(
"Unable to enroll kibana instance. Elasticsearch node HTTP layer SSL configuration is not configured with a keystore"));
return;
}
List<X509Certificate> caCertificates;
try {
caCertificates = ((StoreKeyConfig) keyConfig).x509Certificates(environment)
.stream()
.filter(x509Certificate -> x509Certificate.getBasicConstraints() != -1)
.collect(Collectors.toList());
} catch (Exception e) {
listener.onFailure(new ElasticsearchException("Unable to enroll kibana instance. Cannot retrieve CA certificate " +
"for the HTTP layer of the Elasticsearch node.", e));
return;
}
if (caCertificates.size() != 1) {
listener.onFailure(new ElasticsearchException(
"Unable to enroll kibana instance. Elasticsearch node HTTP layer SSL configuration Keystore " +
"[xpack.security.http.ssl.keystore] doesn't contain a single PrivateKey entry where the associated " +
"certificate is a CA certificate"));
} else {
String httpCa;
try {
httpCa = Base64.getUrlEncoder().encodeToString(caCertificates.get(0).getEncoded());
} catch (CertificateEncodingException cee) {
listener.onFailure(new ElasticsearchException(
"Unable to enroll kibana instance. Elasticsearch node HTTP layer SSL configuration uses a malformed CA certificate",
cee));
return;
}
final char[] password = generateKibanaSystemPassword();
final ChangePasswordRequest changePasswordRequest =
new ChangePasswordRequestBuilder(client).username("kibana_system")
.password(password, Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(environment.settings())))
.request();
client.execute(ChangePasswordAction.INSTANCE, changePasswordRequest, ActionListener.wrap(response -> {
logger.debug("Successfully set the password for user [kibana_system] during kibana enrollment");
listener.onResponse(new KibanaEnrollmentResponse(new SecureString(password), httpCa));
}, e -> listener.onFailure(new ElasticsearchException("Failed to set the password for user [kibana_system]", e))));
}
}
private char[] generateKibanaSystemPassword() {
final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray();
final SecureRandom secureRandom = new SecureRandom();
int passwordLength = 14;
char[] characters = new char[passwordLength];
for (int i = 0; i < passwordLength; ++i) {
characters[i] = passwordChars[secureRandom.nextInt(passwordChars.length)];
}
return characters;
}
}

View File

@ -20,6 +20,7 @@ import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction;
import org.elasticsearch.xpack.core.security.action.enrollment.NodeEnrollmentAction;
import org.elasticsearch.xpack.core.ssl.KeyConfig;
import org.elasticsearch.xpack.core.ssl.SSLService;
@ -70,11 +71,9 @@ public class CreateEnrollmentToken {
return this.create(user, password, NodeEnrollmentAction.NAME);
}
// TBD: Awaiting Enroll Kibana API to be merged
//
/*public String createKibanaEnrollmentToken(String user, SecureString password) throws Exception {
// return this.create(user, password, KibanaEnrollmentAction.NAME);
}*/
public String createKibanaEnrollmentToken(String user, SecureString password) throws Exception {
return this.create(user, password, KibanaEnrollmentAction.NAME);
}
protected String create(String user, SecureString password, String action) throws Exception {
if (XPackSettings.ENROLLMENT_ENABLED.get(environment.settings()) != true) {

View File

@ -0,0 +1,61 @@
/*
* 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.enrollment;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentRequest;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentResponse;
import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler;
import java.io.IOException;
import java.util.List;
public class RestKibanaEnrollAction extends SecurityBaseRestHandler {
/**
* @param settings the node's settings
* @param licenseState the license state that will be used to determine if security is licensed
*/
public RestKibanaEnrollAction(Settings settings, XPackLicenseState licenseState) {
super(settings, licenseState);
}
@Override public String getName() {
return "kibana_enroll_action";
}
@Override public List<Route> routes() {
return List.of(new Route(RestRequest.Method.GET, "/_security/enroll/kibana"));
}
@Override protected RestChannelConsumer innerPrepareRequest(
RestRequest request, NodeClient client) throws IOException {
try (XContentParser parser = request.contentParser()) {
return restChannel -> client.execute(
KibanaEnrollmentAction.INSTANCE, new KibanaEnrollmentRequest(),
new RestBuilderListener<>(restChannel) {
@Override public RestResponse buildResponse(
KibanaEnrollmentResponse kibanaEnrollmentResponse, XContentBuilder builder) throws Exception {
kibanaEnrollmentResponse.toXContent(builder, channel.request());
return new BytesRestResponse(RestStatus.OK, builder);
}
});
}
}
}

View File

@ -40,7 +40,7 @@ public final class RestNodeEnrollmentAction extends SecurityBaseRestHandler {
@Override public List<Route> routes() {
return List.of(
new Route(RestRequest.Method.GET, "_security/enroll_node")
new Route(RestRequest.Method.GET, "_security/enroll/node")
);
}

View File

@ -0,0 +1,120 @@
/*
* 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.enrollment;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.ValidationException;
import org.elasticsearch.common.settings.MockSecureSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.Transport;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentRequest;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentResponse;
import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction;
import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest;
import org.elasticsearch.xpack.core.ssl.SSLConfiguration;
import org.elasticsearch.xpack.core.ssl.SSLService;
import org.junit.Before;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TransportKibanaEnrollmentActionTests extends ESTestCase {
private List<ChangePasswordRequest> changePasswordRequests;
private TransportKibanaEnrollmentAction action;
private Client client;
private Path httpCaPath;
@Before @SuppressWarnings("unchecked") public void setup() throws Exception {
changePasswordRequests = new ArrayList<>();
final Environment env = mock(Environment.class);
final Path tempDir = createTempDir();
httpCaPath = tempDir.resolve("httpCa.p12");
Files.copy(getDataPath("/org/elasticsearch/xpack/security/action/enrollment/httpCa.p12"), httpCaPath);
when(env.configFile()).thenReturn(tempDir);
final MockSecureSettings secureSettings = new MockSecureSettings();
secureSettings.setString("keystore.secure_password", "password");
final Settings settings = Settings.builder()
.put("keystore.path", "httpCa.p12")
.setSecureSettings(secureSettings)
.build();
when(env.settings()).thenReturn(settings);
final SSLService sslService = mock(SSLService.class);
final SSLConfiguration sslConfiguration = new SSLConfiguration(settings);
when(sslService.getHttpTransportSSLConfiguration()).thenReturn(sslConfiguration);
final ThreadContext threadContext = new ThreadContext(settings);
final ThreadPool threadPool = mock(ThreadPool.class);
when(threadPool.getThreadContext()).thenReturn(threadContext);
client = mock(Client.class);
when(client.threadPool()).thenReturn(threadPool);
doAnswer(invocation -> {
ChangePasswordRequest changePasswordRequest = (ChangePasswordRequest) invocation.getArguments()[1];
changePasswordRequests.add(changePasswordRequest);
ActionListener<ActionResponse.Empty> listener = (ActionListener) invocation.getArguments()[2];
listener.onResponse(ActionResponse.Empty.INSTANCE);
return null;
}).when(client).execute(eq(ChangePasswordAction.INSTANCE), any(), any());
final TransportService transportService = new TransportService(Settings.EMPTY,
mock(Transport.class),
threadPool,
TransportService.NOOP_TRANSPORT_INTERCEPTOR,
x -> null,
null,
Collections.emptySet());
action = new TransportKibanaEnrollmentAction(transportService, client, sslService, env, mock(ActionFilters.class));
}
public void testKibanaEnrollment() {
final KibanaEnrollmentRequest request = new KibanaEnrollmentRequest();
final PlainActionFuture<KibanaEnrollmentResponse> future = new PlainActionFuture<>();
action.doExecute(mock(Task.class), request, future);
final KibanaEnrollmentResponse response = future.actionGet();
assertThat(response.getHttpCa(), startsWith("MIIDSjCCAjKgAwIBAgIVALCgZXvbceUrjJaQMheDCX0kXnRJMA0GCSqGSIb3DQEBCwUAMDQxMjAw" +
"BgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2VuZXJhdGVkIENBMB4XDTIxMDQyODEyNTY0MVoXDTI0MDQyNzEyNTY0MVowNDEyMDAGA1UEA" +
"xMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCCJbOU4JvxDD_F"));
assertNotNull(response.getPassword());
assertThat(changePasswordRequests.size(), equalTo(1));
}
public void testKibanaEnrollmentFailedPasswordChange() {
// Override change password mock
doAnswer(invocation -> {
ActionListener<ActionResponse.Empty> listener = (ActionListener) invocation.getArguments()[2];
listener.onFailure(new ValidationException());
return null;
}).when(client).execute(eq(ChangePasswordAction.INSTANCE), any(), any());
final KibanaEnrollmentRequest request = new KibanaEnrollmentRequest();
final PlainActionFuture<KibanaEnrollmentResponse> future = new PlainActionFuture<>();
action.doExecute(mock(Task.class), request, future);
ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet);
assertThat(e.getDetailedMessage(), containsString("Failed to set the password for user [kibana_system]"));
}
}

View File

@ -128,7 +128,7 @@ public class CreateEnrollmentTokenTests extends ESTestCase {
Map<String, String> infoNode = getDecoded(tokenNode);
assertEquals("8.0.0", infoNode.get("ver"));
assertEquals("[192.168.0.1:9201, 172.16.254.1:9202, [2001:db8:0:1234:0:567:8:1]:9203]", infoNode.get("adr"));
assertEquals("598a35cd831ee6bb90e79aa80d6b073cda88b41d", infoNode.get("fgr"));
assertEquals("ecdc64cebdfa501b771bcf43eb38b43dc3a90d78", infoNode.get("fgr"));
assertEquals("DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", infoNode.get("key"));
final String tokenKibana = createEnrollmentToken.createNodeEnrollmentToken("elastic", new SecureString("elastic"));
@ -136,7 +136,7 @@ public class CreateEnrollmentTokenTests extends ESTestCase {
Map<String, String> infoKibana = getDecoded(tokenKibana);
assertEquals("8.0.0", infoKibana.get("ver"));
assertEquals("[192.168.0.1:9201, 172.16.254.1:9202, [2001:db8:0:1234:0:567:8:1]:9203]", infoKibana.get("adr"));
assertEquals("598a35cd831ee6bb90e79aa80d6b073cda88b41d", infoKibana.get("fgr"));
assertEquals("ecdc64cebdfa501b771bcf43eb38b43dc3a90d78", infoKibana.get("fgr"));
assertEquals("DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", infoKibana.get("key"));
}

View File

@ -0,0 +1,44 @@
== Instructions for generating needed keystores
The keystores in this directory are supposed to mimic the PKCS12 keystores that the elasticsearch
startup script will auto-generate for a node. The transport.p12 contain a single PrivateKeyEntry for the
nodes key and certificate for the transport layer.
The httpCa.p12 keystore contains:
- A PrivateKeyEntry for the node's key and certificate for the HTTP layer
- A PrivateKeyEntry for the CA's key and certificate
- A TrustedCertificateEntry for the CA's certificate
=== Generate CA keystore
[source,shell]
-----------------------------------------------------------------------------------------------------------
$ES_HOME/bin/elasticsearch-certutil ca --out ca.p12 --pass "password"
-----------------------------------------------------------------------------------------------------------
=== Generate the transport layer keystore
[source,shell]
-----------------------------------------------------------------------------------------------------------
$ES_HOME/bin/elasticsearch-certutil cert --out transport.p12 --ca ca.p12 --ca-pass "password"
-----------------------------------------------------------------------------------------------------------
=== Generate the HTTP layer keystore
[source,shell]
-----------------------------------------------------------------------------------------------------------
$ES_HOME/bin/elasticsearch-certutil cert --out httpCa.p12 --ca ca.p12 --ca-pass password \
--dns=localhost --dns=localhost.localdomain --dns=localhost4 --dns=localhost4.localdomain4 \
--dns=localhost6 --dns=localhost6.localdomain6 \
--ip=127.0.0.1 --ip=0:0:0:0:0:0:0:1
-----------------------------------------------------------------------------------------------------------
Change the alias of the TrustedCertificateEntry so that it won't clash with the CA PrivateKeyEntry
[source,shell]
-----------------------------------------------------------------------------------------------------------
keytool -changealias -alias ca -destalias cacert -keystore httpCa.p12
-----------------------------------------------------------------------------------------------------------
Import the CA PrivateKeyEntry
[source,shell]
-----------------------------------------------------------------------------------------------------------
keytool -importkeystore -srckeystore ca.p12 -destkeystore httpCa.p12
-----------------------------------------------------------------------------------------------------------