diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle
index 17f243da1d3..fbc8a0769ca 100644
--- a/client/rest-high-level/build.gradle
+++ b/client/rest-high-level/build.gradle
@@ -78,6 +78,7 @@ processTestResources {
from({ zipTree(configurations.restSpec.singleFile) }) {
include 'rest-api-spec/api/**'
}
+ from(project(':client:rest-high-level').file('src/test/resources'))
}
dependencyLicenses {
@@ -96,6 +97,7 @@ forbiddenApisMain {
}
File nodeCert = file("./testnode.crt")
File nodeTrustStore = file("./testnode.jks")
+File pkiTrustCert = file("./src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.crt")
integTest.runner {
systemProperty 'tests.rest.cluster.username', System.getProperty('tests.rest.cluster.username', 'test_user')
@@ -113,6 +115,12 @@ testClusters.integTest {
// Truststore settings are not used since TLS is not enabled. Included for testing the get certificates API
setting 'xpack.security.http.ssl.certificate_authorities', 'testnode.crt'
setting 'xpack.security.transport.ssl.truststore.path', 'testnode.jks'
+ setting 'xpack.security.authc.realms.file.default_file.order', '0'
+ setting 'xpack.security.authc.realms.native.default_native.order', '1'
+ setting 'xpack.security.authc.realms.pki.pki1.order', '2'
+ setting 'xpack.security.authc.realms.pki.pki1.certificate_authorities', '[ "testRootCA.crt" ]'
+ setting 'xpack.security.authc.realms.pki.pki1.delegation.enabled', 'true'
+
setting 'indices.lifecycle.poll_interval', '1000ms'
keystore 'xpack.security.transport.ssl.truststore.secure_password', 'testnode'
user username: System.getProperty('tests.rest.cluster.username', 'test_user'),
@@ -120,4 +128,5 @@ testClusters.integTest {
extraConfigFile nodeCert.name, nodeCert
extraConfigFile nodeTrustStore.name, nodeTrustStore
+ extraConfigFile pkiTrustCert.name, pkiTrustCert
}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java
index 8c29cfaae54..a807b798f57 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java
@@ -31,6 +31,8 @@ import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateApiKeyResponse;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.CreateTokenResponse;
+import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
+import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeletePrivilegesResponse;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
@@ -969,4 +971,38 @@ public final class SecurityClient {
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::invalidateApiKey, options,
InvalidateApiKeyResponse::fromXContent, listener, emptySet());
}
+
+ /**
+ * Get an Elasticsearch access token from an {@code X509Certificate} chain. The certificate chain is that of the client from a mutually
+ * authenticated TLS session, and it is validated by the PKI realms with {@code delegation.enabled} toggled to {@code true}.
+ * See the
+ * docs for more details.
+ *
+ * @param request the request containing the certificate chain
+ * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+ * @return the response from the delegate-pki-authentication API key call
+ * @throws IOException in case there is a problem sending the request or parsing back the response
+ */
+ public DelegatePkiAuthenticationResponse delegatePkiAuthentication(DelegatePkiAuthenticationRequest request, RequestOptions options)
+ throws IOException {
+ return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::delegatePkiAuthentication, options,
+ DelegatePkiAuthenticationResponse::fromXContent, emptySet());
+ }
+
+ /**
+ * Asynchronously get an Elasticsearch access token from an {@code X509Certificate} chain. The certificate chain is that of the client
+ * from a mutually authenticated TLS session, and it is validated by the PKI realms with {@code delegation.enabled} toggled to
+ * {@code true}.
+ * See the
+ * docs for more details.
+ *
+ * @param request the request containing the certificate chain
+ * @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 void delegatePkiAuthenticationAsync(DelegatePkiAuthenticationRequest request, RequestOptions options,
+ ActionListener listener) {
+ restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::delegatePkiAuthentication, options,
+ DelegatePkiAuthenticationResponse::fromXContent, listener, emptySet());
+ }
}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java
index 18ecc2cea28..4634ef23dfe 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java
@@ -28,6 +28,7 @@ import org.elasticsearch.client.security.ClearRealmCacheRequest;
import org.elasticsearch.client.security.ClearRolesCacheRequest;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateTokenRequest;
+import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleRequest;
@@ -221,6 +222,12 @@ final class SecurityRequestConverters {
return request;
}
+ static Request delegatePkiAuthentication(DelegatePkiAuthenticationRequest delegatePkiAuthenticationRequest) throws IOException {
+ Request request = new Request(HttpPost.METHOD_NAME, "/_security/delegate_pki");
+ request.setEntity(createEntity(delegatePkiAuthenticationRequest, REQUEST_BODY_CONTENT_TYPE));
+ return request;
+ }
+
static Request invalidateToken(InvalidateTokenRequest invalidateTokenRequest) throws IOException {
Request request = new Request(HttpDelete.METHOD_NAME, "/_security/oauth2/token");
request.setEntity(createEntity(invalidateTokenRequest, REQUEST_BODY_CONTENT_TYPE));
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationRequest.java
new file mode 100644
index 00000000000..c67e692c14d
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationRequest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.Validatable;
+import org.elasticsearch.client.ValidationException;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import static java.util.Collections.unmodifiableList;
+
+public final class DelegatePkiAuthenticationRequest implements Validatable, ToXContentObject {
+
+ private final List x509CertificateChain;
+
+ public DelegatePkiAuthenticationRequest(final List x509CertificateChain) {
+ if (x509CertificateChain == null || x509CertificateChain.isEmpty()) {
+ throw new IllegalArgumentException("certificate chain must not be empty or null");
+ }
+ this.x509CertificateChain = unmodifiableList(x509CertificateChain);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject().startArray("x509_certificate_chain");
+ try {
+ for (X509Certificate cert : x509CertificateChain) {
+ builder.value(Base64.getEncoder().encodeToString(cert.getEncoded()));
+ }
+ } catch (CertificateEncodingException e) {
+ throw new IOException(e);
+ }
+ return builder.endArray().endObject();
+ }
+
+ public List getCertificateChain() {
+ return this.x509CertificateChain;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final DelegatePkiAuthenticationRequest that = (DelegatePkiAuthenticationRequest) o;
+ return Objects.equals(x509CertificateChain, that.x509CertificateChain);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(x509CertificateChain);
+ }
+
+ @Override
+ public Optional validate() {
+ ValidationException validationException = new ValidationException();
+ if (false == isOrderedCertificateChain(x509CertificateChain)) {
+ validationException.addValidationError("certificates chain must be an ordered chain");
+ }
+ return validationException.validationErrors().isEmpty() ? Optional.empty() : Optional.of(validationException);
+ }
+
+ /**
+ * Checks that the {@code X509Certificate} list is ordered, such that the end-entity certificate is first and it is followed by any
+ * certificate authorities'. The check validates that the {@code issuer} of every certificate is the {@code subject} of the certificate
+ * in the next array position. No other certificate attributes are checked.
+ */
+ private static boolean isOrderedCertificateChain(List chain) {
+ for (int i = 1; i < chain.size(); i++) {
+ X509Certificate cert = chain.get(i - 1);
+ X509Certificate issuer = chain.get(i);
+ if (false == cert.getIssuerX500Principal().equals(issuer.getSubjectX500Principal())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponse.java
new file mode 100644
index 00000000000..064a5a9a4e2
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponse.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+
+public final class DelegatePkiAuthenticationResponse {
+
+ private final String accessToken;
+ private final String type;
+ private final TimeValue expiresIn;
+
+ public DelegatePkiAuthenticationResponse(String accessToken, String type, TimeValue expiresIn) {
+ this.accessToken = accessToken;
+ this.type = type;
+ this.expiresIn = expiresIn;
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public TimeValue getExpiresIn() {
+ return expiresIn;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final DelegatePkiAuthenticationResponse that = (DelegatePkiAuthenticationResponse) o;
+ return Objects.equals(accessToken, that.accessToken) &&
+ Objects.equals(type, that.type) &&
+ Objects.equals(expiresIn, that.expiresIn);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(accessToken, type, expiresIn);
+ }
+
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "delegate_pki_response", true,
+ args -> new DelegatePkiAuthenticationResponse((String) args[0], (String) args[1], TimeValue.timeValueSeconds((Long) args[2])));
+
+ static {
+ PARSER.declareString(constructorArg(), new ParseField("access_token"));
+ PARSER.declareString(constructorArg(), new ParseField("type"));
+ PARSER.declareLong(constructorArg(), new ParseField("expires_in"));
+ }
+
+ public static DelegatePkiAuthenticationResponse fromXContent(XContentParser parser) throws IOException {
+ return PARSER.parse(parser, null);
+ }
+}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java
index 55bbbbe14ca..45371fc6c68 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java
@@ -26,6 +26,7 @@ import org.apache.http.client.methods.HttpPut;
import org.elasticsearch.client.security.ChangePasswordRequest;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateTokenRequest;
+import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleRequest;
@@ -59,6 +60,7 @@ import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -68,6 +70,8 @@ import java.util.Map;
import static org.elasticsearch.client.RequestConvertersTests.assertToXContentBody;
import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
public class SecurityRequestConvertersTests extends ESTestCase {
@@ -305,6 +309,18 @@ public class SecurityRequestConvertersTests extends ESTestCase {
assertToXContentBody(createTokenRequest, request.getEntity());
}
+ public void testDelegatePkiAuthentication() throws Exception {
+ X509Certificate mockCertificate = mock(X509Certificate.class);
+ when(mockCertificate.getEncoded()).thenReturn(new byte[0]);
+ DelegatePkiAuthenticationRequest delegatePkiAuthenticationRequest = new DelegatePkiAuthenticationRequest(
+ Arrays.asList(mockCertificate));
+ Request request = SecurityRequestConverters.delegatePkiAuthentication(delegatePkiAuthenticationRequest);
+ assertEquals(HttpPost.METHOD_NAME, request.getMethod());
+ assertEquals("/_security/delegate_pki", request.getEndpoint());
+ assertEquals(0, request.getParameters().size());
+ assertToXContentBody(delegatePkiAuthenticationRequest, request.getEntity());
+ }
+
public void testGetApplicationPrivilege() throws Exception {
final String application = randomAlphaOfLength(6);
final String privilege = randomAlphaOfLength(4);
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java
index 27167ade4da..247c500d48c 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java
@@ -37,6 +37,8 @@ import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateApiKeyResponse;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.CreateTokenResponse;
+import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
+import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeletePrivilegesResponse;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
@@ -77,6 +79,7 @@ import org.elasticsearch.client.security.PutUserRequest;
import org.elasticsearch.client.security.PutUserResponse;
import org.elasticsearch.client.security.RefreshPolicy;
import org.elasticsearch.client.security.TemplateRoleName;
+import org.elasticsearch.client.security.AuthenticateResponse.RealmInfo;
import org.elasticsearch.client.security.support.ApiKey;
import org.elasticsearch.client.security.support.CertificateInfo;
import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpression;
@@ -99,6 +102,11 @@ import org.hamcrest.Matchers;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
@@ -2194,4 +2202,84 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
assertThat(response.getPreviouslyInvalidatedApiKeys().size(), equalTo(0));
}
}
+
+ public void testDelegatePkiAuthentication() throws Exception {
+ final RestHighLevelClient client = highLevelClient();
+ X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt");
+ X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt");
+ {
+ //tag::delegate-pki-request
+ DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(
+ Arrays.asList(clientCertificate, intermediateCA));
+ //end::delegate-pki-request
+ //tag::delegate-pki-execute
+ DelegatePkiAuthenticationResponse response = client.security().delegatePkiAuthentication(request, RequestOptions.DEFAULT);
+ //end::delegate-pki-execute
+ //tag::delegate-pki-response
+ String accessToken = response.getAccessToken(); // <1>
+ //end::delegate-pki-response
+
+ RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder();
+ optionsBuilder.addHeader("Authorization", "Bearer " + accessToken);
+ AuthenticateResponse resp = client.security().authenticate(optionsBuilder.build());
+ User user = resp.getUser();
+ assertThat(user, is(notNullValue()));
+ assertThat(user.getUsername(), is("Elasticsearch Test Client"));
+ RealmInfo authnRealm = resp.getAuthenticationRealm();
+ assertThat(authnRealm, is(notNullValue()));
+ assertThat(authnRealm.getName(), is("pki1"));
+ assertThat(authnRealm.getType(), is("pki"));
+ }
+
+ {
+ DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(
+ Arrays.asList(clientCertificate, intermediateCA));
+ ActionListener listener;
+
+ //tag::delegate-pki-execute-listener
+ listener = new ActionListener() {
+ @Override
+ public void onResponse(DelegatePkiAuthenticationResponse getRolesResponse) {
+ // <1>
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ // <2>
+ }
+ };
+ //end::delegate-pki-execute-listener
+
+ assertNotNull(listener);
+
+ // Replace the empty listener by a blocking listener in test
+ final PlainActionFuture future = new PlainActionFuture<>();
+ listener = future;
+
+ //tag::delegate-pki-execute-async
+ client.security().delegatePkiAuthenticationAsync(request, RequestOptions.DEFAULT, listener); // <1>
+ //end::delegate-pki-execute-async
+
+ final DelegatePkiAuthenticationResponse response = future.get(30, TimeUnit.SECONDS);
+ String accessToken = response.getAccessToken();
+ RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder();
+ optionsBuilder.addHeader("Authorization", "Bearer " + accessToken);
+ AuthenticateResponse resp = client.security().authenticate(optionsBuilder.build());
+ User user = resp.getUser();
+ assertThat(user, is(notNullValue()));
+ assertThat(user.getUsername(), is("Elasticsearch Test Client"));
+ RealmInfo authnRealm = resp.getAuthenticationRealm();
+ assertThat(authnRealm, is(notNullValue()));
+ assertThat(authnRealm.getName(), is("pki1"));
+ assertThat(authnRealm.getType(), is("pki"));
+ }
+ }
+
+ private X509Certificate readCertForPkiDelegation(String certificateName) throws Exception {
+ Path path = getDataPath("/org/elasticsearch/client/security/delegate_pki/" + certificateName);
+ try (InputStream in = Files.newInputStream(path)) {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) factory.generateCertificate(in);
+ }
+ }
}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationRequestTests.java
new file mode 100644
index 00000000000..08c7055e8f4
--- /dev/null
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationRequestTests.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.AbstractRequestTestCase;
+import org.elasticsearch.client.ValidationException;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import javax.security.auth.x500.X500Principal;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DelegatePkiAuthenticationRequestTests extends AbstractRequestTestCase {
+
+ public void testEmptyOrNullCertificateChain() throws Exception {
+ IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
+ new DelegatePkiAuthenticationRequest((List)null);
+ });
+ assertThat(e.getMessage(), is("certificate chain must not be empty or null"));
+ e = expectThrows(IllegalArgumentException.class, () -> {
+ new DelegatePkiAuthenticationRequest(Collections.emptyList());
+ });
+ assertThat(e.getMessage(), is("certificate chain must not be empty or null"));
+ }
+
+ public void testUnorderedCertificateChain() throws Exception {
+ List mockCertChain = new ArrayList<>(2);
+ mockCertChain.add(mock(X509Certificate.class));
+ when(mockCertChain.get(0).getIssuerX500Principal()).thenReturn(new X500Principal("CN=Test, OU=elasticsearch, O=org"));
+ mockCertChain.add(mock(X509Certificate.class));
+ when(mockCertChain.get(1).getSubjectX500Principal()).thenReturn(new X500Principal("CN=Not Test, OU=elasticsearch, O=org"));
+ DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(mockCertChain);
+ Optional ve = request.validate();
+ assertThat(ve.isPresent(), is(true));
+ assertThat(ve.get().validationErrors().size(), is(1));
+ assertThat(ve.get().validationErrors().get(0), is("certificates chain must be an ordered chain"));
+ }
+
+ @Override
+ protected DelegatePkiAuthenticationRequest createClientTestInstance() {
+ List certificates = randomCertificateList();
+ return new DelegatePkiAuthenticationRequest(certificates);
+ }
+
+ @Override
+ protected org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationRequest doParseToServerInstance(XContentParser parser)
+ throws IOException {
+ return org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationRequest.fromXContent(parser);
+ }
+
+ @Override
+ protected void assertInstances(org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationRequest serverInstance,
+ DelegatePkiAuthenticationRequest clientTestInstance) {
+ assertThat(serverInstance.getCertificateChain(), is(clientTestInstance.getCertificateChain()));
+ }
+
+ private List randomCertificateList() {
+ List certificates = Arrays.asList(randomArray(1, 3, X509Certificate[]::new, () -> {
+ try {
+ return readCertForPkiDelegation(randomFrom("testClient.crt", "testIntermediateCA.crt", "testRootCA.crt"));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ return certificates;
+ }
+
+ private X509Certificate readCertForPkiDelegation(String certificateName) throws Exception {
+ Path path = getDataPath("/org/elasticsearch/client/security/delegate_pki/" + certificateName);
+ try (InputStream in = Files.newInputStream(path)) {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) factory.generateCertificate(in);
+ }
+ }
+}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponseTests.java
new file mode 100644
index 00000000000..2348eef4bd1
--- /dev/null
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponseTests.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.AbstractResponseTestCase;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse;
+
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.is;
+
+public class DelegatePkiAuthenticationResponseTests extends
+ AbstractResponseTestCase {
+
+ @Override
+ protected org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse createServerTestInstance() {
+ return new org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse(randomAlphaOfLength(6),
+ TimeValue.parseTimeValue(randomTimeValue(), getClass().getSimpleName() + ".expiresIn"));
+ }
+
+ @Override
+ protected DelegatePkiAuthenticationResponse doParseToClientInstance(XContentParser parser) throws IOException {
+ return DelegatePkiAuthenticationResponse.fromXContent(parser);
+ }
+
+ @Override
+ protected void assertInstances(org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse serverTestInstance,
+ DelegatePkiAuthenticationResponse clientInstance) {
+ assertThat(serverTestInstance.getAccessToken(), is(clientInstance.getAccessToken()));
+ assertThat(serverTestInstance.getExpiresIn(), is(clientInstance.getExpiresIn()));
+ assertThat(clientInstance.getType(), is("Bearer"));
+ }
+}
diff --git a/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/README.asciidoc b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/README.asciidoc
new file mode 100644
index 00000000000..3230bdde7e2
--- /dev/null
+++ b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/README.asciidoc
@@ -0,0 +1,35 @@
+= Certificate Chain details
+This document details the steps used to create the certificate chain in this directory.
+The chain has a length of 3: the Root CA, the Intermediate CA and the Client Certificate.
+All openssl commands use the same configuration file, albeit different sections of it.
+The OpenSSL Configuration file is located in this directory as `openssl_config.cnf`.
+
+== Instructions on generating self-signed Root CA
+The self-signed Root CA, 'testRootCA.crt', and its associated private key in this directory
+have been generated using the following openssl commands.
+
+[source,shell]
+-----------------------------------------------------------------------------------------------------------
+openssl genrsa -out testRootCA.key 2048
+openssl req -x509 -new -key testRootCA.key -days 1460 -subj "/CN=Elasticsearch Test Root CA/OU=elasticsearch/O=org" -out testRootCA.crt -config ./openssl_config.cnf
+-----------------------------------------------------------------------------------------------------------
+
+== Instructions on generating the Intermediate CA
+The `testIntermediateCA.crt` CA certificate is "issued" by the `testRootCA.crt`.
+
+[source,shell]
+-----------------------------------------------------------------------------------------------------------
+openssl genrsa -out testIntermediateCA.key 2048
+openssl req -new -key testIntermediateCA.key -subj "/CN=Elasticsearch Test Intermediate CA/OU=Elasticsearch/O=org" -out testIntermediateCA.csr -config ./openssl_config.cnf
+openssl x509 -req -in testIntermediateCA.csr -CA testRootCA.crt -CAkey testRootCA.key -CAcreateserial -out testIntermediateCA.crt -days 1460 -sha256 -extensions v3_ca -extfile ./openssl_config.cnf
+-----------------------------------------------------------------------------------------------------------
+
+== Instructions on generating the Client Certificate
+The `testClient.crt` end entity certificate is "issued" by the `testIntermediateCA.crt`.
+
+[source,shell]
+-----------------------------------------------------------------------------------------------------------
+openssl genrsa -out testClient.key 2048
+openssl req -new -key testClient.key -subj "/CN=Elasticsearch Test Client/OU=Elasticsearch/O=org" -out testClient.csr -config ./openssl_config.cnf
+openssl x509 -req -in testClient.csr -CA testIntermediateCA.crt -CAkey testIntermediateCA.key -CAcreateserial -out testClient.crt -days 1460 -sha256 -extensions usr_cert -extfile ./openssl_config.cnf
+-----------------------------------------------------------------------------------------------------------
diff --git a/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/openssl_config.cnf b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/openssl_config.cnf
new file mode 100644
index 00000000000..64ff556f352
--- /dev/null
+++ b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/openssl_config.cnf
@@ -0,0 +1,185 @@
+####################################################################
+# CA Definition
+[ ca ]
+default_ca = CA_default # The default ca section
+
+####################################################################
+# Per the above, this is where we define CA values
+[ CA_default ]
+
+# By default we use "user certificate" extensions when signing
+x509_extensions = usr_cert # The extentions to add to the cert
+
+# Honor extensions requested of us
+copy_extensions = copy
+
+# Comment out the following two lines for the "traditional"
+# (and highly broken) format.
+name_opt = ca_default # Subject Name options
+cert_opt = ca_default # Certificate field options
+
+# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs
+# so this is commented out by default to leave a V1 CRL.
+# crlnumber must also be commented out to leave a V1 CRL.
+#crl_extensions = crl_ext
+default_days = 1460 # how long to certify for
+default_md = sha256 # which md to use.
+preserve = no # keep passed DN ordering
+
+# A few difference way of specifying how similar the request should look
+# For type CA, the listed attributes must be the same, and the optional
+# and supplied fields are just that :-)
+policy = policy_anything
+
+####################################################################
+# The default policy for the CA when signing requests, requires some
+# resemblence to the CA cert
+#
+[ policy_match ]
+countryName = match # Must be the same as the CA
+stateOrProvinceName = match # Must be the same as the CA
+organizationName = match # Must be the same as the CA
+organizationalUnitName = optional # not required
+commonName = supplied # must be there, whatever it is
+emailAddress = optional # not required
+
+####################################################################
+# An alternative policy not referred to anywhere in this file. Can
+# be used by specifying '-policy policy_anything' to ca(8).
+#
+[ policy_anything ]
+countryName = optional
+stateOrProvinceName = optional
+localityName = optional
+organizationName = optional
+organizationalUnitName = optional
+commonName = supplied
+emailAddress = optional
+
+####################################################################
+# This is where we define how to generate CSRs
+[ req ]
+default_bits = 2048
+default_keyfile = privkey.pem
+distinguished_name = req_distinguished_name # where to get DN for reqs
+attributes = req_attributes # req attributes
+x509_extensions = v3_ca # The extentions to add to self signed certs
+req_extensions = v3_req # The extensions to add to req's
+
+# This sets a mask for permitted string types. There are several options.
+# default: PrintableString, T61String, BMPString.
+# pkix : PrintableString, BMPString.
+# utf8only: only UTF8Strings.
+# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
+# MASK:XXXX a literal mask value.
+# WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings
+# so use this option with caution!
+string_mask = nombstr
+
+
+####################################################################
+# Per "req" section, this is where we define DN info
+[ req_distinguished_name ]
+
+0.organizationName = Organization Name (company)
+0.organizationName_default = org
+
+organizationalUnitName = Organizational Unit Name (eg, section)
+organizationalUnitName_default = elasticsearch
+
+commonName = Common Name (hostname, IP, or your name)
+commonName_default = Elasticsearch Test Certificate
+commonName_max = 64
+
+####################################################################
+# We don't want these, but the section must exist
+[ req_attributes ]
+#challengePassword = A challenge password
+#challengePassword_min = 4
+#challengePassword_max = 20
+#unstructuredName = An optional company name
+
+
+####################################################################
+# Extensions for when we sign normal certs (specified as default)
+[ usr_cert ]
+
+# User certs aren't CAs, by definition
+basicConstraints=CA:false
+
+# Here are some examples of the usage of nsCertType. If it is omitted
+# the certificate can be used for anything *except* object signing.
+# This is OK for an SSL server.
+#nsCertType = server
+# For an object signing certificate this would be used.
+#nsCertType = objsign
+# For normal client use this is typical
+#nsCertType = client, email
+# and for everything including object signing:
+#nsCertType = client, email, objsign
+# This is typical in keyUsage for a client certificate.
+#keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+# PKIX recommendations harmless if included in all certificates.
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid,issuer
+
+# This stuff is for subjectAltName and issuerAltname.
+# Import the email address.
+#subjectAltName=email:copy
+# An alternative to produce certificates that aren't
+# deprecated according to PKIX.
+#subjectAltName=email:move
+
+
+####################################################################
+# Extension for requests
+[ v3_req ]
+basicConstraints = CA:FALSE
+
+# PKIX recommendation.
+subjectKeyIdentifier = hash
+
+subjectAltName = @alt_names
+
+####################################################################
+# An alternative section of extensions, not referred to anywhere
+# else in the config. We'll use this via '-extensions v3_ca' when
+# using ca(8) to sign another CA.
+#
+[ v3_ca ]
+
+# PKIX recommendation.
+subjectKeyIdentifier=hash
+authorityKeyIdentifier = keyid,issuer
+
+# This is what PKIX recommends but some broken software chokes on critical
+# extensions.
+#basicConstraints = critical,CA:true
+# So we do this instead.
+basicConstraints = CA:true
+
+# Key usage: this is typical for a CA certificate. However since it will
+# prevent it being used as an test self-signed certificate it is best
+# left out by default.
+# keyUsage = cRLSign, keyCertSign
+
+# Some might want this also
+# nsCertType = sslCA, emailCA
+
+# Include email address in subject alt name: another PKIX recommendation
+#subjectAltName=email:move
+# Copy issuer details
+#issuerAltName=issuer:copy
+
+subjectAltName = @alt_names
+
+[ alt_names ]
+DNS.1 = localhost
+DNS.2 = localhost.localdomain
+DNS.3 = localhost4
+DNS.4 = localhost4.localdomain4
+DNS.5 = localhost6
+DNS.6 = localhost6.localdomain6
+IP.1 = 127.0.0.1
+IP.2 = ::1
diff --git a/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testClient.crt b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testClient.crt
new file mode 100644
index 00000000000..45efce91ef3
--- /dev/null
+++ b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testClient.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDbTCCAlWgAwIBAgIJAIxTS7Qdho9jMA0GCSqGSIb3DQEBCwUAMFMxKzApBgNV
+BAMTIkVsYXN0aWNzZWFyY2ggVGVzdCBJbnRlcm1lZGlhdGUgQ0ExFjAUBgNVBAsT
+DUVsYXN0aWNzZWFyY2gxDDAKBgNVBAoTA29yZzAeFw0xOTA3MTkxMzMzNDFaFw0y
+MzA3MTgxMzMzNDFaMEoxIjAgBgNVBAMTGUVsYXN0aWNzZWFyY2ggVGVzdCBDbGll
+bnQxFjAUBgNVBAsTDUVsYXN0aWNzZWFyY2gxDDAKBgNVBAoTA29yZzCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBANHgMX2aX8t0nj4sGLNuKISmmXIYCj9R
+wRqS7L03l9Nng7kOKnhHu/nXDt7zMRJyHj+q6FAt5khlavYSVCQyrDybRuA5z31g
+OdqXerrjs2OXS5HSHNvoDAnHFsaYX/5geMewVTtc/vqpd7Ph/QtaKfmG2FK0JNQo
+0k24tcgCIcyMtBh6BA70yGBM0OT8GdOgd/d/mA7mRhaxIUMNYQzRYRsp4hMnnWoO
+TkR5Q8KSO3MKw9dPSpPe8EnwtJE10S3s5aXmgytru/xQqrFycPBNj4KbKVmqMP0G
+60CzXik5pr2LNvOFz3Qb6sYJtqeZF+JKgGWdaTC89m63+TEnUHqk0lcCAwEAAaNN
+MEswCQYDVR0TBAIwADAdBgNVHQ4EFgQU/+aAD6Q4mFq1vpHorC25/OY5zjcwHwYD
+VR0jBBgwFoAU8siFCiMiYZZm/95qFC75AG/LRE0wDQYJKoZIhvcNAQELBQADggEB
+AIRpCgDLpvXcgDHUk10uhxev21mlIbU+VP46ANnCuj0UELhTrdTuWvO1PAI4z+Wb
+DUxryQfOOXO9R6D0dE5yR56L/J7d+KayW34zU7yRDZM7+rXpocdQ1Ex8mjP9HJ/B
+f56YZTBQJpXeDrKow4FvtkI3bcIMkqmbG16LHQXeG3RS4ds4S4wCnE2nA6vIn9y+
+4R999q6y1VSBORrYULcDWxS54plHLEdiMr1vVallg82AGobS9GMcTL2U4Nx5IYZG
+7sbTk3LrDxVpVg/S2wLofEdOEwqCeHug/iOihNLJBabEW6z4TDLJAVW5KCY1Dfhk
+YlBfHn7vxKkfKoCUK/yLWWI=
+-----END CERTIFICATE-----
diff --git a/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testClient.key b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testClient.key
new file mode 100644
index 00000000000..186e6f86745
--- /dev/null
+++ b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testClient.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA0eAxfZpfy3SePiwYs24ohKaZchgKP1HBGpLsvTeX02eDuQ4q
+eEe7+dcO3vMxEnIeP6roUC3mSGVq9hJUJDKsPJtG4DnPfWA52pd6uuOzY5dLkdIc
+2+gMCccWxphf/mB4x7BVO1z++ql3s+H9C1op+YbYUrQk1CjSTbi1yAIhzIy0GHoE
+DvTIYEzQ5PwZ06B393+YDuZGFrEhQw1hDNFhGyniEyedag5ORHlDwpI7cwrD109K
+k97wSfC0kTXRLezlpeaDK2u7/FCqsXJw8E2PgpspWaow/QbrQLNeKTmmvYs284XP
+dBvqxgm2p5kX4kqAZZ1pMLz2brf5MSdQeqTSVwIDAQABAoIBAQDAjP767Ioc4LZZ
+9h0HafaUlUDMs4+bPkd7OPcoNnv+AceRHZULW0zz0EIdfGM2OCrWYNfYz/Op0hpK
+/s/hkfgBdriU+ZUKwyDxEu8Pzd6EbYdwlqPRgdihk92qgJv5hsro8jeQSibJFHf1
+Ok3tf2BpRTTs08fCOl2P3vowMPyPa5Ho9bf4lzP8IsR2BZvoaev3za9ZWR6ZDzE6
+EWkBBNgIU4aPn1IJ6dz2+rVtN6+xXET0eYSBEac3xMQaPWLEX0EDBYPW1d+mUva/
+3lJvTrs3g8oyiTyVu0l9Yxdgox1mtgmrqqwxJ6XuouzImuXMMDXaz0K/E/+u2yPF
+V6kRvWuJAoGBAPOnEgBC3ezl+x+47cgbwpy97uZhZmV9HkMrSH9DKDwC+t57TdGX
+ypt2S/IS/vbPupFv0aHaWmJ6SN/HyTN4znwuulV3kE8mEpQzIPbluWfgQzT6ukJe
++YFI/+IXwIRBLA7khtfo01LGHSmLTENsnd/aoRySY3K6zJz36Ys3vFdjAoGBANyC
+7rF5YjPdgsAgOT7EboNGkc8UuW/Sh3xRp0c4Y+PBenf60yA5XkRJLYR4sZDjWTr0
+aKBY7Y8r+59U+bBrwUuhhoW08JZ/SBWja05+4DhH0ToA3vtbPv9lRyQfkF1DdBkn
+XpyM2vaJE5M454acwnKJ81AyoueYtZ8pD3Q7c219AoGAJ+F1wdMwDgGKvCOB0Boz
+HYK9IrpYj04OcQIZqLLuV/xI4befAiptQEr5nVLcprtTl1CNKIfb+Xh4iyBhX2pr
+qcngN/MNDNd3fQhtYdwyH72GYpqTeB+hiTbQo0ot+bfNJVbkd1ylkkvZJB6nyfVy
+VdysOEgBvRq0OREfCemCi28CgYEAoF1EE6NQDKICTZDhsMkQCb5PmcbbmPwFdh63
+xW64DlGNrCWoVt4BtS12wck4cUM1iE9oq3wgv6df5Z7ZuziSKVt9xk0xTnGgTcQ7
+7KkOjT+FZGZvw2K3bOsNkrK1vW2pyAU+pCE3uGU17DJNBjOIod27Kk649C61ntsw
+lvoJVs0CgYBLr9pzBRPyD5/lM9hm2EI7ITa+fVcu3V3bJfXENHKzpb0lB2fhl0PI
+swpiU8RUEKWyjBuHsdQdxg7AgFi/7s+SX7KLo4cudDRd73iiXYdNGB7R0/MAG8Jl
+/lMXn14noS4trA8fNGGg/2fANTBtLTbOX9i4s7clAo8ETywQ33owug==
+-----END RSA PRIVATE KEY-----
diff --git a/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testIntermediateCA.crt b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testIntermediateCA.crt
new file mode 100644
index 00000000000..7d8781b8889
--- /dev/null
+++ b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testIntermediateCA.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEBTCCAu2gAwIBAgIJAIx9twpbtGkCMA0GCSqGSIb3DQEBCwUAMEsxIzAhBgNV
+BAMTGkVsYXN0aWNzZWFyY2ggVGVzdCBSb290IENBMRYwFAYDVQQLEw1lbGFzdGlj
+c2VhcmNoMQwwCgYDVQQKEwNvcmcwHhcNMTkwNzE5MTMzMjM0WhcNMjMwNzE4MTMz
+MjM0WjBTMSswKQYDVQQDEyJFbGFzdGljc2VhcmNoIFRlc3QgSW50ZXJtZWRpYXRl
+IENBMRYwFAYDVQQLEw1FbGFzdGljc2VhcmNoMQwwCgYDVQQKEwNvcmcwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCnJ2KTJZnQzOt0uUf+5oLNcvDLnnWY
+LzXZpOOX666Almwx+PVkDxkiGSe0QB9RWJqHSrsP1ryGIeCIzGMOctLt6QA7Peee
+HdrKqOQgN620nDSd2EZ3s0Iddh1Ns/lfTtBJCP/03suaktm7j8EYKAyOlTIUhiKm
+sTFlxPUSKjbtR4wR1ljnKN8X+j/ghr9mWhQrMR9rsGFObU8DQFho2Ti90C4HoMNU
+dy4j+2G3VVpaq4he4/4CbPrWQQ3dKGpzVAngIuAv4eQ/y88EHAFwutxQZWAew4Va
+5y3O112acSb9oC7g0NHQcBnos/WIChF5ki8V3LFnxN7jYvUUk9YxfA8hAgMBAAGj
+geMwgeAwHQYDVR0OBBYEFPLIhQojImGWZv/eahQu+QBvy0RNMB8GA1UdIwQYMBaA
+FM4SyNzpz82ihQ160zrLUVaWfI+1MAwGA1UdEwQFMAMBAf8wgY8GA1UdEQSBhzCB
+hIIJbG9jYWxob3N0ghVsb2NhbGhvc3QubG9jYWxkb21haW6CCmxvY2FsaG9zdDSC
+F2xvY2FsaG9zdDQubG9jYWxkb21haW40ggpsb2NhbGhvc3Q2ghdsb2NhbGhvc3Q2
+LmxvY2FsZG9tYWluNocEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0B
+AQsFAAOCAQEAMkh4nUi2yt5TX+ryBWaaA4/2ZOsxSeec5E1EjemPMUWGzFipV1YY
+k/mpv51E+BbPgtmGMG8Win/PETKYuX8D+zPauFEmJmyJmm5B4mr1406RWERqNDql
+36sOw89G0mDT/wIB4tkNdh830ml+d75aRVVB4X5pFAE8ZzI3g4OW4YxT3ZfUEhDl
+QeGVatobvIaX8KpNSevjFAFuQzSgj61VXI+2+UIRV4tJP2xEqu5ISuArHcGhvNlS
+bU3vZ80tTCa0tHyJrVqaqtQ23MDBzYPj6wJ/pvBQWAgZKnC3qJgXlJ9des117I1g
+J98AXCDGu5LBW/p2C9VpSktpnfzsX4NHqg==
+-----END CERTIFICATE-----
diff --git a/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testIntermediateCA.key b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testIntermediateCA.key
new file mode 100644
index 00000000000..5147725f448
--- /dev/null
+++ b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testIntermediateCA.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEApydikyWZ0MzrdLlH/uaCzXLwy551mC812aTjl+uugJZsMfj1
+ZA8ZIhkntEAfUViah0q7D9a8hiHgiMxjDnLS7ekAOz3nnh3ayqjkIDettJw0ndhG
+d7NCHXYdTbP5X07QSQj/9N7LmpLZu4/BGCgMjpUyFIYiprExZcT1Eio27UeMEdZY
+5yjfF/o/4Ia/ZloUKzEfa7BhTm1PA0BYaNk4vdAuB6DDVHcuI/tht1VaWquIXuP+
+Amz61kEN3Shqc1QJ4CLgL+HkP8vPBBwBcLrcUGVgHsOFWuctztddmnEm/aAu4NDR
+0HAZ6LP1iAoReZIvFdyxZ8Te42L1FJPWMXwPIQIDAQABAoIBABp4z1C0dL6vpV5v
+9Wn2AaMd3+qvZro6R9H3HiAyMAmnSO1FGz/EcFuJFlOikBMm8BobCLMCdAreFJw1
+mj5wit0ouGOpcyQEYGEWDELZ7oWa825IESjl18OosA1dQlIIvk3Cwh56pk4NkbP1
+mUQFG6/9CthbQeOaTlNqtNEypE5Bc+JGbQaUhRP6tF+Rxnpys2nIJt/Vp9khw0Du
+K7Z6astunhfPDwLFGwHhflc6re1B+mxpLKTDHCcydJo2Kuh/LuuEtPkE5Ar4LwQk
+D+/61iZHC4B8/4IkBlAsgCJ1B18L6JdTbSYeVlepkSkJML5t6z+cvt5VcObF7F8X
+pPZn+kECgYEA2NaB0eshWNnHTMRv+sE92DCv0M7uV1eKtaopxOElAKJ/J2gpqcTh
+GzdTVRg1M2LgVNk97ViL5bsXaVStRe085m8oA0bI9WbIoQRUFp40dRFRUjl+4TN0
+pdxXL4VmQMWuwlO6p8/JY8sInnHVCT+2z8lek8P3bdtTQZV9OZQTn0kCgYEAxVe8
+obJdnUSXuRDWg588TW35PNqOTJcerIU6eRKwafvCcrhMoX62Xbv6y6kKXndW/JuW
+AbfSNiAOV+HGUbf8Xc54Xzk2mouoJA0S0tJ040jqOkFOaKIxYQudTU8y9bTXNsAk
+oX3wOhlt2q9xffAK1gYffP5XPXnYnsb8qaMIeRkCgYBM9yaxOgJmJTbGmtscaEbp
+W66sMScMPXhwruuQhFG7/fGgLSrMpaM5I9QiWitYB/qUY1/FxS4y5suSiYnPTjvV
+lxLexttBr6/65yxpstHv06vHwby1dqwqyyDvLyxyRTiYpVuVgP18vG5cvw7c746W
+BmXZkS9cAQN2Pfdq3pJwcQKBgEbCZd2owg5hCPIPyosZbpro4uRiDYIC8bm0b7n3
+7I+j+R3/XWLOt382pv+dlh03N1aORyRIkDReHCaAywaELRZJsTmbnyudBeYfVe+I
+DOduPqYywnWcKo58hqOw0Tnu5Pg5vyi0qo16jrxKCiy5BHmnamT8IbXmWbjc6r28
+uo4JAoGAfAPvPJ2fV5vpzr4LPoVyaSiFj414D+5XYxX6CWpdTryelpP2Rs1VfJ1a
+7EusUtWs26pAKwttDY4yoTvog7rrskgtXzisaoNMDbH/PfsoqjMnnIgakvKmHpUM
+l6E1ecWFExEg5v6yvmxFC7JIUzIYOoysWu3X44G8rQ+vDQNRFZQ=
+-----END RSA PRIVATE KEY-----
diff --git a/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.crt b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.crt
new file mode 100644
index 00000000000..50ba7a21727
--- /dev/null
+++ b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID/TCCAuWgAwIBAgIJAIAPVUXOUQDNMA0GCSqGSIb3DQEBCwUAMEsxIzAhBgNV
+BAMTGkVsYXN0aWNzZWFyY2ggVGVzdCBSb290IENBMRYwFAYDVQQLEw1lbGFzdGlj
+c2VhcmNoMQwwCgYDVQQKEwNvcmcwHhcNMTkwNzE5MTMzMjIwWhcNMjMwNzE4MTMz
+MjIwWjBLMSMwIQYDVQQDExpFbGFzdGljc2VhcmNoIFRlc3QgUm9vdCBDQTEWMBQG
+A1UECxMNZWxhc3RpY3NlYXJjaDEMMAoGA1UEChMDb3JnMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAzIgn8r2kirt90id0uoi6YEGBPx+XDzthLbLsN+M0
+nXhj40OVcGPiww+cre14bJr0M6MG4CvFjRJc92RoVrE8+7XOKt0bgiHeVM+b0LEh
+wVMH9koararPVMo0CjCMN4ChHMOWKBPUNZswvk+pFC+QbTcfgQLycqh+lTB1O6l3
+hPnmunEqhLIj9ke3FwA326igdb+16EbKYVL2c5unNoC5ZMc5Z9bnn4/GNXptkHhy
++SvG7IZKW2pAzei3Df/n47ZhJfQKERUCe9eO7b/ZmTEzAzYj9xucE5lYcpkOZd6g
+IMU3vXe4FeD/BM4sOLkKTtMejiElEecxw8cLI9Nji/0y1wIDAQABo4HjMIHgMB0G
+A1UdDgQWBBTOEsjc6c/NooUNetM6y1FWlnyPtTAfBgNVHSMEGDAWgBTOEsjc6c/N
+ooUNetM6y1FWlnyPtTAMBgNVHRMEBTADAQH/MIGPBgNVHREEgYcwgYSCCWxvY2Fs
+aG9zdIIVbG9jYWxob3N0LmxvY2FsZG9tYWluggpsb2NhbGhvc3Q0ghdsb2NhbGhv
+c3Q0LmxvY2FsZG9tYWluNIIKbG9jYWxob3N0NoIXbG9jYWxob3N0Ni5sb2NhbGRv
+bWFpbjaHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEB
+ACHjwoDJILv77sQ5QN6SoAp6GYqiC9/doDIzDFCd/WP7G8EbaosHM6jM7NbrlK3g
+PNTzuY1pLPoI3YJSO4Al/UfzEffaYSbZC2QZG9F6fUSWhvR+nxzPSXWkjzIInv1j
+pPMgnUl6oJaUbsSR/evtvWNSxrM3LewkRTOoktkXM6SjTUHjdP6ikrkrarrWZgzr
+K30BqGL6kDSv9LkyXe6RSgQDtQe51Yut+lKGCcy8AoEwG/3cjb7XnrWcFsJXjYbf
+4m3QsS8yHU/O/xgyvVHOfki+uGVepzSjdzDMLE1GBkju05NR2eJZ8omj/QiJa0+z
+1d/AOKExvWvo1yQ28ORcwo4=
+-----END CERTIFICATE-----
diff --git a/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.key b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.key
new file mode 100644
index 00000000000..148bbd52bd7
--- /dev/null
+++ b/client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAzIgn8r2kirt90id0uoi6YEGBPx+XDzthLbLsN+M0nXhj40OV
+cGPiww+cre14bJr0M6MG4CvFjRJc92RoVrE8+7XOKt0bgiHeVM+b0LEhwVMH9koa
+rarPVMo0CjCMN4ChHMOWKBPUNZswvk+pFC+QbTcfgQLycqh+lTB1O6l3hPnmunEq
+hLIj9ke3FwA326igdb+16EbKYVL2c5unNoC5ZMc5Z9bnn4/GNXptkHhy+SvG7IZK
+W2pAzei3Df/n47ZhJfQKERUCe9eO7b/ZmTEzAzYj9xucE5lYcpkOZd6gIMU3vXe4
+FeD/BM4sOLkKTtMejiElEecxw8cLI9Nji/0y1wIDAQABAoIBAQC6LMnoPFW1brs1
++3JWhTTZf2btlYzEcbGgjnhU2v0+xaJu8UrrFhEIq4JcE4gFm/rjsecFUPKu2eND
+0eLj3st699+lxsRObRPbMWtMyJ/IQRNDTesA4DV/odtC1zQbJXwCGcrpyjrlXNE+
+unZWiIE32PBVV+BnHBa1KHneCAFiSRLrySAiDAnTIJxB6ufweoxevLoJPPNLlbo7
+H2jv6g1Som/Imjhof4KhD/1Q04Sed2wScSS/7Bz38eO68HG4NMFY+M2/cLzrbflg
+QdeKHNhoIGnSFMEW5TCVlI4qrP8zvPPdZmLOMBT+Ocm3pc5xDAPwFYCe8wH1DVn+
+b3sVpwu5AoGBAOhFA7gUDZjRBkNAqJfbUdhdWSslePQsjeTKsu5rc4gk2aiL4bZ4
+fxG0Dq1hX7FjAmYrGqnsXsbxxDnCkhXGH1lY73kF0Zzwr2Pg1yRHyn1nCinhD4g4
+G2vBr37QtWn4wS/L7V//D3xrcCTG3QgAmvZZ99tYgqlmnUzmawdZ8kQ7AoGBAOFt
+qg7sTSNWVpKkfkyX2NXvBMt5e3Qcwnge2pX+SBgljwjNUwSSMLwxdBDSyDXIhk8W
+s4pJLtMDJsT/2WBKC9WJm9m3gc7yYZznLJ+5YPcieXHGGNXCRldPePhTIjnL591H
+CSXoc3BZ2iKK745BYuPqSuLb2XfE3/hwoaFR4S4VAoGAQ6ywG7dECu2ELJ4vQSe2
+3hq8u1SMvGAq66mfntYR8G4EORagqkDLjUXwLNY9Qnr9nPUcLLxhFQgmS0oEtHFo
+eujtxU5Lt7Vs9OXy6XA9cHJQRMl9dAwc+TWSw5ld8kV3TEzXmevAAFlxcFW82vMK
+M5MdI3zTfTYXyOst7hNoAjcCgYAhz/cgAeWYFU0q9a1UA7qsbAuGEZSo1997cPVM
+ZjWeGZQYt+Np3hudPrWwCE2rc4Zhun/3j/6L+/8GsXGDddfMkbVktJet2ME3bZ1N
+39phdzRMEnCLL3aphewZIy8RCDqhABSpMPKPuYp0f+5qofgZQ300BdHamxcVBp/X
+uJZT+QKBgQDdJQd+QxfCb8BZ11fWtyWJWQWZMmyX2EEbAIMvYQP3xh8PHmw2JoiQ
+VQ103bCkegJ1S7ubrGltdt8pyjN4rrByXJmxCe1Y/LSHIp9w8D3jaiLCRSk1EmBw
+jXjnZoiJn3GV5jmbV10hzrn7jqRcwhYA5zuoE7qb604V7cPZLzHtog==
+-----END RSA PRIVATE KEY-----
diff --git a/client/rest/src/main/java/org/elasticsearch/client/RequestOptions.java b/client/rest/src/main/java/org/elasticsearch/client/RequestOptions.java
index 0b2cdce3d52..43a2a376326 100644
--- a/client/rest/src/main/java/org/elasticsearch/client/RequestOptions.java
+++ b/client/rest/src/main/java/org/elasticsearch/client/RequestOptions.java
@@ -170,10 +170,11 @@ public final class RequestOptions {
/**
* Add the provided header to the request.
*/
- public void addHeader(String name, String value) {
+ public Builder addHeader(String name, String value) {
Objects.requireNonNull(name, "header name cannot be null");
Objects.requireNonNull(value, "header value cannot be null");
this.headers.add(new ReqHeader(name, value));
+ return this;
}
/**
diff --git a/docs/java-rest/high-level/security/delegate-pki-authentication.asciidoc b/docs/java-rest/high-level/security/delegate-pki-authentication.asciidoc
new file mode 100644
index 00000000000..9cb667c24dc
--- /dev/null
+++ b/docs/java-rest/high-level/security/delegate-pki-authentication.asciidoc
@@ -0,0 +1,62 @@
+--
+:api: delegate-pki
+:request: DelegatePkiAuthenticationRequest
+:response: DelegatePkiAuthenticationResponse
+--
+
+[id="{upid}-{api}"]
+=== Delegate PKI Authentication API
+
+This API is called by *smart* proxies to Elasticsearch, such as Kibana, that
+terminate the user's TLS session but that still wish to authenticate the user
+on the Elasticsearch side using a PKI realm, which normally requires users to
+authenticate over TLS directly to Elasticsearch. It implements the exchange of
+the client's {@code X509Certificate} chain from the TLS authentication into an
+Elasticsearch access token.
+
+IMPORTANT: The association between the subject public key in the target
+certificate and the corresponding private key is *not* validated. This is part
+of the TLS authentication process and it is delegated to the proxy calling this
+API. The proxy is *trusted* to have performed the TLS authentication, and this
+API translates that authentication into an Elasticsearch access token.
+
+[id="{upid}-{api}-request"]
+==== Delegate PKI Authentication Request
+
+The request contains the client's {@code X509Certificate} chain. The
+certificate chain is represented as a list where the first element is the
+target certificate containing the subject distinguished name that is requesting
+access. This may be followed by additional certificates, with each subsequent
+certificate being the one used to certify the previous one. The certificate
+chain is validated according to RFC 5280, by sequentially considering the trust
+configuration of every installed {@code PkiRealm} that has {@code
+PkiRealmSettings#DELEGATION_ENABLED_SETTING} set to {@code true} (default is
+{@code false}). A successfully trusted target certificate is also subject to
+the validation of the subject distinguished name according to that respective's
+realm {@code PkiRealmSettings#USERNAME_PATTERN_SETTING}.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SecurityDocumentationIT.java[delegate-pki-request]
+--------------------------------------------------
+
+include::../execution.asciidoc[]
+
+[id="{upid}-{api}-response"]
+==== Delegate PKI Authentication Response
+
+The returned +{response}+ contains the following properties:
+
+`accessToken`:: This is the newly created access token.
+ It can be used to authenticate to the Elasticsearch cluster.
+`type`:: The type of the token, this is always `"Bearer"`.
+`expiresIn`:: The length of time (in seconds) until the token will expire.
+ The token will be considered invalid after that time.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SecurityDocumentationIT.java[delegate-pki-response]
+--------------------------------------------------
+<1> The `accessToken` can be used to authentication to Elasticsearch.
+
+
diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc
index 5af7bb38044..15d104b6a0b 100644
--- a/docs/reference/settings/security-settings.asciidoc
+++ b/docs/reference/settings/security-settings.asciidoc
@@ -857,6 +857,17 @@ Defaults to `20m`.
Specifies the maximum number of user entries that the cache can contain.
Defaults to `100000`.
+`delegation.enabled`::
+Generally, in order for the clients to be authenticated by the PKI realm they
+must connect directly to {es}. That is, they must not pass through proxies
+which terminate the TLS connection. In order to allow for a *trusted* and
+*smart* proxy, such as Kibana, to sit before {es} and terminate TLS
+connections, but still allow clients to be authenticated on {es} by this realm,
+you need to toggle this to `true`. Defaults to `false`. If delegation is
+enabled, then either `truststore.path` or `certificate_authorities` setting
+must be defined. For more details, see <>.
+
[[ref-saml-settings]]
[float]
===== SAML realm settings
diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle
index 1450012601d..acc92e5f075 100644
--- a/x-pack/docs/build.gradle
+++ b/x-pack/docs/build.gradle
@@ -31,6 +31,7 @@ project.copyRestSpec.from(xpackResources) {
testClusters.integTest {
extraConfigFile 'op-jwks.json', xpackProject('test:idp-fixture').file("oidc/op-jwks.json")
+ extraConfigFile 'testClient.crt', xpackProject('plugin:security').file("src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testClient.crt")
setting 'xpack.security.enabled', 'true'
setting 'xpack.security.authc.api_key.enabled', 'true'
setting 'xpack.security.authc.token.enabled', 'true'
@@ -50,6 +51,9 @@ testClusters.integTest {
keystore 'xpack.security.authc.realms.oidc.oidc1.rp.client_secret', 'b07efb7a1cf6ec9462afe7b6d3ab55c6c7880262aa61ac28dded292aca47c9a2'
setting 'xpack.security.authc.realms.oidc.oidc1.rp.response_type', 'id_token'
setting 'xpack.security.authc.realms.oidc.oidc1.claims.principal', 'sub'
+ setting 'xpack.security.authc.realms.pki.pki1.order', '3'
+ setting 'xpack.security.authc.realms.pki.pki1.certificate_authorities', '[ "testClient.crt" ]'
+ setting 'xpack.security.authc.realms.pki.pki1.delegation.enabled', 'true'
user username: 'test_admin'
}
diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc
index 059dbc1e747..d385ef29c46 100644
--- a/x-pack/docs/en/rest-api/security.asciidoc
+++ b/x-pack/docs/en/rest-api/security.asciidoc
@@ -6,6 +6,7 @@ You can use the following APIs to perform security activities.
* <>
* <>
+* <>
* <>
* <>
* <>
@@ -98,6 +99,7 @@ include::security/put-app-privileges.asciidoc[]
include::security/create-role-mappings.asciidoc[]
include::security/create-roles.asciidoc[]
include::security/create-users.asciidoc[]
+include::security/delegate-pki-authentication.asciidoc[]
include::security/delete-app-privileges.asciidoc[]
include::security/delete-role-mappings.asciidoc[]
include::security/delete-roles.asciidoc[]
diff --git a/x-pack/docs/en/rest-api/security/delegate-pki-authentication.asciidoc b/x-pack/docs/en/rest-api/security/delegate-pki-authentication.asciidoc
new file mode 100644
index 00000000000..92d82f1c273
--- /dev/null
+++ b/x-pack/docs/en/rest-api/security/delegate-pki-authentication.asciidoc
@@ -0,0 +1,96 @@
+[role="xpack"]
+[[security-api-delegate-pki-authentication]]
+=== Delegate PKI authentication API
+++++
+Delegate PKI authentication
+++++
+
+Implements the exchange of an {@code X509Certificate} chain into an {es} access
+token.
+
+[[security-api-delegate-pki-authentication-request]]
+==== {api-request-title}
+
+`POST /_security/delegate_pki`
+
+[[security-api-delegate-pki-authentication-prereqs]]
+==== {api-prereq-title}
+
+* To call this API, the (proxy) user must have the `delegate_pki` or the `all`
+cluster privilege. The `kibana_system` built-in role already grants this
+privilege. See {stack-ov}/security-privileges.html[Security privileges].
+
+[[security-api-delegate-pki-authentication-desc]]
+==== {api-description-title}
+
+This API implements the exchange of an _X509Certificate_ chain for an {es}
+access token. The certificate chain is validated, according to RFC 5280, by
+sequentially considering the trust configuration of every installed PKI realm
+that has `delegation.enabled` set to `true` (default is `false`). A
+successfully trusted client certificate is also subject to the validation of
+the subject distinguished name according to that respective's realm
+`username_pattern`.
+
+This API is called by *smart* and *trusted* proxies, such as {kib}, which
+terminate the user's TLS session but still want to authenticate the user
+by using a PKI realm--as if the user connected directly to {es}. For more
+details, see <>.
+
+IMPORTANT: The association between the subject public key in the target
+certificate and the corresponding private key is *not* validated. This is part
+of the TLS authentication process and it is delegated to the proxy that calls
+this API. The proxy is *trusted* to have performed the TLS authentication and
+this API translates that authentication into an {es} access token.
+
+[[security-api-delegate-pki-authentication-request-body]]
+==== {api-request-body-title}
+
+`x509_certificate_chain`::
+(Required, list of strings) The _X509Certificate_ chain, which is represented as
+an ordered string array. Each string in the array is a base64-encoded
+(Section 4 of RFC4648 - not base64url-encoded) of the certificate's DER encoding.
++
+The first element is the target certificate contains the subject distinguished
+name that is requesting access. This may be followed by additional certificates;
+each subsequent certificate is used to certify the previous one.
+
+
+[[security-api-delegate-pki-authentication-response-body]]
+==== {api-response-body-title}
+
+`access_token`::
+(string) An access token associated to the subject distinguished name of the
+client's certificate.
+
+`expires_in`::
+(time units) The amount of time (in seconds) that the token expires in.
+
+`type`::
+(string) The type of token.
+
+[[security-api-delegate-pki-authentication-example]]
+==== {api-examples-title}
+
+The following is an example request:
+
+[source, js]
+------------------------------------------------------------
+POST /_security/delegate_pki
+{
+ "x509_certificate_chain": ["MIIDbTCCAlWgAwIBAgIJAIxTS7Qdho9jMA0GCSqGSIb3DQEBCwUAMFMxKzApBgNVBAMTIkVsYXN0aWNzZWFyY2ggVGVzdCBJbnRlcm1lZGlhdGUgQ0ExFjAUBgNVBAsTDUVsYXN0aWNzZWFyY2gxDDAKBgNVBAoTA29yZzAeFw0xOTA3MTkxMzMzNDFaFw0yMzA3MTgxMzMzNDFaMEoxIjAgBgNVBAMTGUVsYXN0aWNzZWFyY2ggVGVzdCBDbGllbnQxFjAUBgNVBAsTDUVsYXN0aWNzZWFyY2gxDDAKBgNVBAoTA29yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHgMX2aX8t0nj4sGLNuKISmmXIYCj9RwRqS7L03l9Nng7kOKnhHu/nXDt7zMRJyHj+q6FAt5khlavYSVCQyrDybRuA5z31gOdqXerrjs2OXS5HSHNvoDAnHFsaYX/5geMewVTtc/vqpd7Ph/QtaKfmG2FK0JNQo0k24tcgCIcyMtBh6BA70yGBM0OT8GdOgd/d/mA7mRhaxIUMNYQzRYRsp4hMnnWoOTkR5Q8KSO3MKw9dPSpPe8EnwtJE10S3s5aXmgytru/xQqrFycPBNj4KbKVmqMP0G60CzXik5pr2LNvOFz3Qb6sYJtqeZF+JKgGWdaTC89m63+TEnUHqk0lcCAwEAAaNNMEswCQYDVR0TBAIwADAdBgNVHQ4EFgQU/+aAD6Q4mFq1vpHorC25/OY5zjcwHwYDVR0jBBgwFoAU8siFCiMiYZZm/95qFC75AG/LRE0wDQYJKoZIhvcNAQELBQADggEBAIRpCgDLpvXcgDHUk10uhxev21mlIbU+VP46ANnCuj0UELhTrdTuWvO1PAI4z+WbDUxryQfOOXO9R6D0dE5yR56L/J7d+KayW34zU7yRDZM7+rXpocdQ1Ex8mjP9HJ/Bf56YZTBQJpXeDrKow4FvtkI3bcIMkqmbG16LHQXeG3RS4ds4S4wCnE2nA6vIn9y+4R999q6y1VSBORrYULcDWxS54plHLEdiMr1vVallg82AGobS9GMcTL2U4Nx5IYZG7sbTk3LrDxVpVg/S2wLofEdOEwqCeHug/iOihNLJBabEW6z4TDLJAVW5KCY1DfhkYlBfHn7vxKkfKoCUK/yLWWI="] <1>
+}
+------------------------------------------------------------
+// CONSOLE
+<1> A one element certificate chain.
+
+Which returns the following response:
+
+[source,js]
+--------------------------------------------------
+{
+ "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
+ "type" : "Bearer",
+ "expires_in" : 1200
+}
+--------------------------------------------------
+// TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
diff --git a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc
index b6afc70715a..6ee26fa778d 100644
--- a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc
+++ b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc
@@ -66,6 +66,7 @@ A successful call returns an object with "cluster" and "index" fields.
"cluster" : [
"all",
"create_snapshot",
+ "delegate_pki",
"manage",
"manage_api_key",
"manage_ccr",
diff --git a/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc
index 58144d0b23c..a3fc1a6c0b0 100644
--- a/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc
+++ b/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc
@@ -2,26 +2,39 @@
[[configuring-pki-realm]]
=== Configuring a PKI realm
-You can configure {es} to use Public Key Infrastructure (PKI) certificates
-to authenticate users. This requires clients to present X.509 certificates.
+You can configure {es} to use Public Key Infrastructure (PKI) certificates to
+authenticate users. This requires clients connecting directly to {es} to
+present X.509 certificates. The certificates must first be accepted for
+authentication on the SSL/TLS layer on {es}. Only then they are optionally
+further validated by a PKI realm.
-NOTE: You cannot use PKI certificates to authenticate users in {kib}.
+Users may also use PKI certificates to authenticate to {kib}, however this
+requires some <>. On
+{es}, this configuration enables {kib} to act as a proxy for SSL/TLS
+authentication and to submit the client certificates to {es} for further
+validation by a PKI realm.
+
+For more general information, see {stack-ov}/pki-realm.html[PKI user authentication].
+
+[float]
+[role="xpack"]
+[[pki-realm-for-direct-clients]]
+==== PKI authentication for clients connecting directly to {es}
To use PKI in {es}, you configure a PKI realm, enable client authentication on
-the desired network layers (transport or http), and map the Distinguished Names
-(DNs) from the user certificates to roles in the
-<> or role-mapping file.
+the desired network layers (transport or http), and map the Distinguished Name
+(DN) from the Subject field in the user certificate to roles by using the
+<> or the role-mapping file.
You can also use a combination of PKI and username/password authentication. For
example, you can enable SSL/TLS on the transport layer and define a PKI realm to
require transport clients to authenticate with X.509 certificates, while still
-authenticating HTTP traffic using username and password credentials. You can
-also set `xpack.security.transport.ssl.client_authentication` to `optional` to
+authenticating HTTP traffic using username and password credentials. You can
+also set `xpack.security.transport.ssl.client_authentication` to `optional` to
allow clients without certificates to authenticate with other credentials.
-IMPORTANT: You must enable SSL/TLS and enable client authentication to use PKI.
-
-For more information, see {stack-ov}/pki-realm.html[PKI User Authentication].
+IMPORTANT: You must enable SSL/TLS with client authentication to use PKI when
+clients connect directly to {es}.
. Add a realm configuration for a `pki` realm to `elasticsearch.yml` under the
`xpack.security.authc.realms.pki` namespace.
@@ -43,17 +56,19 @@ xpack:
order: 1
------------------------------------------------------------
-With this configuration, any certificate trusted by the SSL/TLS layer is accepted
-for authentication. The username is the common name (CN) extracted from the DN
-of the certificate.
+With this configuration, any certificate trusted by the {es} SSL/TLS layer is
+accepted for authentication. The username is the common name (CN) extracted
+from the DN in the Subject field of the end-entity certificate. This
+configuration does not permit PKI authentication to {kib}.
IMPORTANT: When you configure realms in `elasticsearch.yml`, only the
realms you specify are used for authentication. If you also want to use the
`native` or `file` realms, you must include them in the realm chain.
-If you want to use something other than the CN of the DN as the username, you
-can specify a regex to extract the desired username. For example, the regex in
-the following configuration extracts the email address from the DN:
+If you want to use something other than the CN of the Subject DN as the
+username, you can specify a regex to extract the desired username. The regex is
+applied on the Subject DN. For example, the regex in the following
+configuration extracts the email address from the Subject DN:
[source, yaml]
------------------------------------------------------------
@@ -65,23 +80,29 @@ xpack:
pki1:
username_pattern: "EMAILADDRESS=(.*?)(?:,|$)"
------------------------------------------------------------
+
+NOTE: If the regex is too restrictive and does not match the Subject DN of the
+client's certificate, then the realm does not authenticate the certificate.
+
--
-. Restart {es}.
+. Restart {es} because realm configuration is not reloaded automatically. If
+you're following through with the next steps, you might wish to hold the
+restart for last.
-. <>.
+. <>.
. Enable client authentication on the desired network layers (transport or http).
+
--
-The PKI realm relies on the TLS settings of the node's network interface. The
-realm can be configured to be more restrictive than the underlying network
-connection - that is, it is possible to configure the node such that some
-connections are accepted by the network interface but then fail to be
-authenticated by the PKI realm. However, the reverse is not possible. The PKI
-realm cannot authenticate a connection that has been refused by the network
-interface.
+When clients connect directly to {es} and are not proxy-authenticated, the PKI
+realm relies on the TLS settings of the node's network interface. The realm can
+be configured to be more restrictive than the underlying network connection.
+That is, it is possible to configure the node such that some connections
+are accepted by the network interface but then fail to be authenticated by the
+PKI realm. However, the reverse is not possible. The PKI realm cannot
+authenticate a connection that has been refused by the network interface.
In particular this means:
@@ -96,14 +117,15 @@ In particular this means:
used by the client.
The relevant network interface (transport or http) must be configured to trust
-any certificate that is to be used within the PKI realm. However, it possible to
+any certificate that is to be used within the PKI realm. However, it is possible to
configure the PKI realm to trust only a _subset_ of the certificates accepted
by the network interface. This is useful when the SSL/TLS layer trusts clients
with certificates that are signed by a different CA than the one that signs your
users' certificates.
-To configure the PKI realm with its own truststore, specify the `truststore.path`
-option. For example:
+To configure the PKI realm with its own truststore, specify the
+`truststore.path` option. The path must be located within the Elasticsearch
+configuration directory (ES_PATH_CONF). For example:
[source, yaml]
------------------------------------------------------------
@@ -114,22 +136,33 @@ xpack:
pki:
pki1:
truststore:
- path: "/path/to/pki_truststore.jks"
- password: "x-pack-test-password"
+ path: "pki1_truststore.jks"
+------------------------------------------------------------
+
+If the truststore is password protected, the password should be configured by
+adding the appropriate `secure_password` setting to the {es} keystore. For
+example, the following command adds the password for the example realm above:
+
+[source, shell]
+------------------------------------------------------------
+bin/elasticsearch-keystore add \
+xpack.security.authc.realms.pki.pki1.truststore.secure_password
------------------------------------------------------------
The `certificate_authorities` option can be used as an alternative to the
-`truststore.path` setting.
+`truststore.path` setting, when the certificate files are PEM formatted
+. The setting accepts a list. The two options are exclusive, they cannot be both used
+simultaneously.
--
. Map roles for PKI users.
+
--
-You map roles for PKI users through the
-<> or by using a file stored on
-each node. When a user authenticates against a PKI realm, the privileges for
-that user are the union of all privileges defined by the roles to which the
-user is mapped.
+You map roles for PKI users through the <> or by using a file stored on each node. Both configuration
+options are merged together. When a user authenticates against a PKI realm, the
+privileges for that user are the union of all privileges defined by the roles
+to which the user is mapped.
You identify a user by the distinguished name in their certificate.
For example, the following mapping configuration maps `John Doe` to the
@@ -150,7 +183,11 @@ PUT /_security/role_mapping/users
// CONSOLE
<1> The distinguished name (DN) of a PKI user.
-Or, alternatively, configured in a role-mapping file:
+Or, alternatively, configured inside a role-mapping file. The file's path
+defaults to `ES_PATH_CONF/role_mapping.yml`. You can specify a different path (which must be within
+ES_PATH_CONF) by using the `files.role_mapping` realm setting (e.g.
+`xpack.security.authc.realms.pki.pki1.files.role_mapping`):
+
[source, yaml]
------------------------------------------------------------
user: <1>
@@ -163,7 +200,7 @@ The distinguished name for a PKI user follows X.500 naming conventions which
place the most specific fields (like `cn` or `uid`) at the beginning of the
name, and the most general fields (like `o` or `dc`) at the end of the name.
Some tools, such as _openssl_, may print out the subject name in a different
- format.
+format.
One way that you can determine the correct DN for a certificate is to use the
<> (use the relevant PKI
@@ -179,3 +216,76 @@ NOTE: The PKI realm supports
alternative to role mapping.
--
+
+[float]
+[role="xpack"]
+[[pki-realm-for-proxied-clients]]
+==== PKI authentication for clients connecting to {kib}
+
+By default, the PKI realm relies on the node's network interface to perform the
+SSL/TLS handshake and extract the client certificate. This behaviour requires
+that that clients connect directly to {es} so that their SSL connection is
+terminated by the {es} node. If SSL/TLS authenticatication is to be performed
+by {kib}, the PKI realm must be configured to permit delegation.
+
+Specifically, when clients presenting X.509 certificates connect to {kib},
+{kib} performs the SSL/TLS authentication. {kib} then forwards the client's
+certificate chain, by calling an {es} API, to have them further validated by
+the PKI realms that have been configured for delegation.
+
+To permit authentication delegation for a specific {es} PKI realm, start by
+configuring the realm for the usual case, as detailed in the
+<>
+section. Note that you must explicitly configure a `truststore` (or,
+equivalently `certificate_authorities`) even though it is the same trust
+configuration that you have configured on the network layer. Afterwards,
+simply toggle the `delegation.enabled` realm setting to `true`. This realm is
+now allowed to validate delegated PKI authentication (after restarting {es}).
+
+NOTE: PKI authentication delegation requires that the
+`xpack.security.authc.token.enabled` setting be `true` and that SSL/TLS be
+configured (without SSL/TLS client authentication).
+
+NOTE: {kib} also needs to be configured to allow PKI certificate authentication.
+
+A PKI realm with `delegation.enabled` still works unchanged for clients
+connecting directly to {es}. Directly authenticated users, and users that are PKI
+authenticated by delegation to {kib} both follow the same
+{stack-ov}/mapping-roles.html[role mapping rules] or
+{stack-ov}/realm-chains.html#authorization_realms[authorization realms
+configurations].
+
+However, if you use the <>,
+you can distinguish between users that are authenticated by delegation and
+users that are authenticated directly. The former have the
+extra fields `pki_delegated_by_user` and `pki_delegated_by_realm` in the user's
+metadata. In the common setup, where authentication is delegated to {kib}, the
+values of these fields are `kibana` and `reserved`, respectively. For example,
+the following role mapping rule will assign the `role_for_pki1_direct` role to
+all users that have been authenticated directly by the `pki1` realm, by
+connecting to {es} instead of going through {kib}:
+
+[source,js]
+--------------------------------------------------
+PUT /_security/role_mapping/direct_pki_only
+{
+ "roles" : [ "role_for_pki1_direct" ],
+ "rules" : {
+ "all": [
+ {
+ "field": {"realm.name": "pki1"}
+ },
+ {
+ "field": {
+ "metadata.pki_delegated_by_user": null <1>
+ }
+ }
+ ]
+ },
+ "enabled": true
+}
+--------------------------------------------------
+// CONSOLE
+<1> only when this metadata field is set (it is *not* `null`) the user has been
+authenticated in the delegation scenario.
+
diff --git a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc
index a99e385bd8c..cf8911238a0 100644
--- a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc
+++ b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc
@@ -28,6 +28,11 @@ you are able to map users to both API-managed roles and file-managed roles
NOTE: The PKI, LDAP, Kerberos and SAML realms support using
<> as an alternative to role mapping.
+NOTE: When <> is enabled, the roles
+of the anonymous user are assigned to all the other users as well.
+
+NOTE: Users with no roles assigned will be unauthorized for any action.
+
[[mapping-roles-api]]
==== Using the role mapping API
diff --git a/x-pack/plugin/core/build.gradle b/x-pack/plugin/core/build.gradle
index 06c9e2c109a..de4632e4088 100644
--- a/x-pack/plugin/core/build.gradle
+++ b/x-pack/plugin/core/build.gradle
@@ -49,6 +49,7 @@ dependencies {
testCompile project(path: ':modules:parent-join', configuration: 'runtime')
testCompile project(path: ':modules:lang-mustache', configuration: 'runtime')
testCompile project(path: ':modules:analysis-common', configuration: 'runtime')
+ testCompile project(':client:rest-high-level')
testCompile(project(':x-pack:license-tools')) {
transitive = false
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationAction.java
new file mode 100644
index 00000000000..e8b6c26ff67
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationAction.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.security.action;
+
+import org.elasticsearch.action.ActionType;
+
+/**
+ * ActionType for delegating PKI authentication
+ */
+public class DelegatePkiAuthenticationAction extends ActionType {
+
+ public static final String NAME = "cluster:admin/xpack/security/delegate_pki";
+ public static final DelegatePkiAuthenticationAction INSTANCE = new DelegatePkiAuthenticationAction();
+
+ private DelegatePkiAuthenticationAction() {
+ super(NAME, DelegatePkiAuthenticationResponse::new);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationRequest.java
new file mode 100644
index 00000000000..39ec08d63ff
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationRequest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.security.action;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
+import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+/**
+ * The request object for {@code TransportDelegatePkiAuthenticationAction} containing the certificate chain for the target subject
+ * distinguished name to be granted an access token.
+ */
+public final class DelegatePkiAuthenticationRequest extends ActionRequest implements ToXContentObject {
+
+ private static final ParseField X509_CERTIFICATE_CHAIN_FIELD = new ParseField("x509_certificate_chain");
+
+ public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "delegate_pki_request", false, a -> {
+ @SuppressWarnings("unchecked")
+ List certificates = (List) a[0];
+ return new DelegatePkiAuthenticationRequest(certificates);
+ });
+
+ static {
+ PARSER.declareFieldArray(optionalConstructorArg(), (parser,c) -> {
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(Base64.getDecoder().decode(parser.text()))) {
+ return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(bis);
+ } catch (CertificateException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }, X509_CERTIFICATE_CHAIN_FIELD, ValueType.STRING_ARRAY);
+ }
+
+ public static DelegatePkiAuthenticationRequest fromXContent(XContentParser parser) throws IOException {
+ return PARSER.apply(parser, null);
+ }
+
+ private List certificateChain;
+
+ public DelegatePkiAuthenticationRequest(List certificateChain) {
+ this.certificateChain = Collections.unmodifiableList(certificateChain);
+ }
+
+ public DelegatePkiAuthenticationRequest(StreamInput input) throws IOException {
+ super(input);
+ try {
+ final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ certificateChain = Collections.unmodifiableList(input.readList(in -> {
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(in.readByteArray())) {
+ return (X509Certificate) certificateFactory.generateCertificate(bis);
+ } catch (CertificateException e) {
+ throw new IOException(e);
+ }
+ }));
+ } catch (CertificateException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ ActionRequestValidationException validationException = null;
+ if (certificateChain.isEmpty()) {
+ validationException = addValidationError("certificates chain must not be empty", validationException);
+ } else if (false == CertParsingUtils.isOrderedCertificateChain(certificateChain)) {
+ validationException = addValidationError("certificates chain must be an ordered chain", validationException);
+ }
+ return validationException;
+ }
+
+ public List getCertificateChain() {
+ return certificateChain;
+ }
+
+ @Override
+ public void writeTo(StreamOutput output) throws IOException {
+ super.writeTo(output);
+ output.writeCollection(certificateChain, (out, cert) -> {
+ try {
+ out.writeByteArray(cert.getEncoded());
+ } catch (CertificateEncodingException e) {
+ throw new IOException(e);
+ }
+ });
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DelegatePkiAuthenticationRequest that = (DelegatePkiAuthenticationRequest) o;
+ return Objects.equals(certificateChain, that.certificateChain);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(certificateChain);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject().startArray(X509_CERTIFICATE_CHAIN_FIELD.getPreferredName());
+ try {
+ for (X509Certificate cert : certificateChain) {
+ builder.value(Base64.getEncoder().encodeToString(cert.getEncoded()));
+ }
+ } catch (CertificateEncodingException e) {
+ throw new IOException(e);
+ }
+ return builder.endArray().endObject();
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationResponse.java
new file mode 100644
index 00000000000..4335d8f1cc6
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationResponse.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.security.action;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * The response object for {@code TransportDelegatePkiAuthenticationAction} containing the issued access token.
+ */
+public final class DelegatePkiAuthenticationResponse extends ActionResponse implements ToXContentObject {
+
+ private static final ParseField ACCESS_TOKEN_FIELD = new ParseField("access_token");
+ private static final ParseField TYPE_FIELD = new ParseField("type");
+ private static final ParseField EXPIRES_IN_FIELD = new ParseField("expires_in");
+
+ public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "delegate_pki_response", true, a -> {
+ final String accessToken = (String) a[0];
+ final String type = (String) a[1];
+ if (false == "Bearer".equals(type)) {
+ throw new IllegalArgumentException("Unknown token type [" + type + "], only [Bearer] type permitted");
+ }
+ final Long expiresIn = (Long) a[2];
+ return new DelegatePkiAuthenticationResponse(accessToken, TimeValue.timeValueSeconds(expiresIn));
+ });
+
+ static {
+ PARSER.declareString(ConstructingObjectParser.constructorArg(), ACCESS_TOKEN_FIELD);
+ PARSER.declareString(ConstructingObjectParser.constructorArg(), TYPE_FIELD);
+ PARSER.declareLong(ConstructingObjectParser.constructorArg(), EXPIRES_IN_FIELD);
+ }
+
+ private String accessToken;
+ private TimeValue expiresIn;
+
+ DelegatePkiAuthenticationResponse() { }
+
+ public DelegatePkiAuthenticationResponse(String accessToken, TimeValue expiresIn) {
+ this.accessToken = Objects.requireNonNull(accessToken);
+ // always store expiration in seconds because this is how we "serialize" to JSON and we need to parse back
+ this.expiresIn = TimeValue.timeValueSeconds(Objects.requireNonNull(expiresIn).getSeconds());
+ }
+
+ public DelegatePkiAuthenticationResponse(StreamInput input) throws IOException {
+ super(input);
+ accessToken = input.readString();
+ expiresIn = input.readTimeValue();
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public TimeValue getExpiresIn() {
+ return expiresIn;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(accessToken);
+ out.writeTimeValue(expiresIn);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DelegatePkiAuthenticationResponse that = (DelegatePkiAuthenticationResponse) o;
+ return Objects.equals(accessToken, that.accessToken) &&
+ Objects.equals(expiresIn, that.expiresIn);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(accessToken, expiresIn);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject()
+ .field(ACCESS_TOKEN_FIELD.getPreferredName(), accessToken)
+ .field(TYPE_FIELD.getPreferredName(), "Bearer")
+ .field(EXPIRES_IN_FIELD.getPreferredName(), expiresIn.getSeconds());
+ return builder.endObject();
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java
index cd153c9009e..e9d203a3897 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java
@@ -37,6 +37,10 @@ public final class PkiRealmSettings {
RealmSettings.realmSettingPrefix(TYPE), "cache.max_users",
key -> Setting.intSetting(key, DEFAULT_MAX_USERS, Setting.Property.NodeScope));
+ public static final Setting.AffixSetting DELEGATION_ENABLED_SETTING = Setting.affixKeySetting(
+ RealmSettings.realmSettingPrefix(TYPE), "delegation.enabled",
+ key -> Setting.boolSetting(key, false, Setting.Property.NodeScope));
+
public static final Setting.AffixSetting> TRUST_STORE_PATH;
public static final Setting.AffixSetting> TRUST_STORE_TYPE;
public static final Setting.AffixSetting TRUST_STORE_PASSWORD;
@@ -72,6 +76,7 @@ public final class PkiRealmSettings {
settings.add(USERNAME_PATTERN_SETTING);
settings.add(CACHE_TTL_SETTING);
settings.add(CACHE_MAX_USERS_SETTING);
+ settings.add(DELEGATION_ENABLED_SETTING);
settings.add(TRUST_STORE_PATH);
settings.add(TRUST_STORE_PASSWORD);
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java
index ff84b3fd5a8..ac7977714d7 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java
@@ -19,6 +19,7 @@ import org.elasticsearch.xpack.core.ilm.action.GetLifecycleAction;
import org.elasticsearch.xpack.core.ilm.action.GetStatusAction;
import org.elasticsearch.xpack.core.ilm.action.StartILMAction;
import org.elasticsearch.xpack.core.ilm.action.StopILMAction;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction;
import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
@@ -37,7 +38,7 @@ import java.util.stream.Stream;
*/
public class ClusterPrivilegeResolver {
// shared automatons
- private static final Set MANAGE_SECURITY_PATTERN = Collections.singleton("cluster:admin/xpack/security/*");
+ private static final Set ALL_SECURITY_PATTERN = Collections.singleton("cluster:admin/xpack/security/*");
private static final Set MANAGE_SAML_PATTERN = Collections.unmodifiableSet(
Sets.newHashSet("cluster:admin/xpack/security/saml/*",
InvalidateTokenAction.NAME, RefreshTokenAction.NAME));
@@ -86,8 +87,7 @@ public class ClusterPrivilegeResolver {
new ActionClusterPrivilege("monitor_data_frame_transforms", MONITOR_DATA_FRAME_PATTERN);
public static final NamedClusterPrivilege MONITOR_WATCHER = new ActionClusterPrivilege("monitor_watcher", MONITOR_WATCHER_PATTERN);
public static final NamedClusterPrivilege MONITOR_ROLLUP = new ActionClusterPrivilege("monitor_rollup", MONITOR_ROLLUP_PATTERN);
- public static final NamedClusterPrivilege MANAGE = new ActionClusterPrivilege("manage",
- ALL_CLUSTER_PATTERN, MANAGE_SECURITY_PATTERN);
+ public static final NamedClusterPrivilege MANAGE = new ActionClusterPrivilege("manage", ALL_CLUSTER_PATTERN, ALL_SECURITY_PATTERN);
public static final NamedClusterPrivilege MANAGE_ML = new ActionClusterPrivilege("manage_ml", MANAGE_ML_PATTERN);
public static final NamedClusterPrivilege MANAGE_DATA_FRAME =
new ActionClusterPrivilege("manage_data_frame_transforms", MANAGE_DATA_FRAME_PATTERN);
@@ -100,7 +100,8 @@ public class ClusterPrivilegeResolver {
new ActionClusterPrivilege("manage_ingest_pipelines", MANAGE_INGEST_PIPELINE_PATTERN);
public static final NamedClusterPrivilege TRANSPORT_CLIENT = new ActionClusterPrivilege("transport_client",
TRANSPORT_CLIENT_PATTERN);
- public static final NamedClusterPrivilege MANAGE_SECURITY = new ActionClusterPrivilege("manage_security", MANAGE_SECURITY_PATTERN);
+ public static final NamedClusterPrivilege MANAGE_SECURITY = new ActionClusterPrivilege("manage_security", ALL_SECURITY_PATTERN,
+ Collections.singleton(DelegatePkiAuthenticationAction.NAME));
public static final NamedClusterPrivilege MANAGE_SAML = new ActionClusterPrivilege("manage_saml", MANAGE_SAML_PATTERN);
public static final NamedClusterPrivilege MANAGE_OIDC = new ActionClusterPrivilege("manage_oidc", MANAGE_OIDC_PATTERN);
public static final NamedClusterPrivilege MANAGE_API_KEY = new ActionClusterPrivilege("manage_api_key", MANAGE_API_KEY_PATTERN);
@@ -113,6 +114,8 @@ public class ClusterPrivilegeResolver {
public static final NamedClusterPrivilege READ_ILM = new ActionClusterPrivilege("read_ilm", READ_ILM_PATTERN);
public static final NamedClusterPrivilege MANAGE_SLM = new ActionClusterPrivilege("manage_slm", MANAGE_SLM_PATTERN);
public static final NamedClusterPrivilege READ_SLM = new ActionClusterPrivilege("read_slm", READ_SLM_PATTERN);
+ public static final NamedClusterPrivilege DELEGATE_PKI = new ActionClusterPrivilege("delegate_pki",
+ Sets.newHashSet(DelegatePkiAuthenticationAction.NAME, InvalidateTokenAction.NAME));
private static final Map VALUES = Collections.unmodifiableMap(
Stream.of(
@@ -143,7 +146,8 @@ public class ClusterPrivilegeResolver {
MANAGE_ILM,
READ_ILM,
MANAGE_SLM,
- READ_SLM).collect(Collectors.toMap(cp -> cp.name(), cp -> cp)));
+ READ_SLM,
+ DELEGATE_PKI).collect(Collectors.toMap(cp -> cp.name(), cp -> cp)));
/**
* Resolves a {@link NamedClusterPrivilege} from a given name if it exists.
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java
index 983ac56a226..20968603353 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java
@@ -111,7 +111,7 @@ public class ReservedRolesStore implements BiConsumer, ActionListene
.put(KibanaUser.ROLE_NAME, new RoleDescriptor(KibanaUser.ROLE_NAME,
new String[] {
"monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml", "manage_token", "manage_oidc",
- GetBuiltinPrivilegesAction.NAME
+ GetBuiltinPrivilegesAction.NAME, "delegate_pki"
},
new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder()
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertParsingUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertParsingUtils.java
index 7f16c6097ba..a2def1d2c57 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertParsingUtils.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertParsingUtils.java
@@ -19,6 +19,7 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
+
import java.io.IOException;
import java.io.InputStream;
@@ -289,4 +290,20 @@ public class CertParsingUtils {
}
throw new IllegalStateException("failed to find a X509ExtendedTrustManager");
}
+
+ /**
+ * Checks that the {@code X509Certificate} array is ordered, such that the end-entity certificate is first and it is followed by any
+ * certificate authorities'. The check validates that the {@code issuer} of every certificate is the {@code subject} of the certificate
+ * in the next array position. No other certificate attributes are checked.
+ */
+ public static boolean isOrderedCertificateChain(List chain) {
+ for (int i = 1; i < chain.size(); i++) {
+ X509Certificate cert = chain.get(i - 1);
+ X509Certificate issuer = chain.get(i);
+ if (false == cert.getIssuerX500Principal().equals(issuer.getSubjectX500Principal())) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationRequestTests.java
new file mode 100644
index 00000000000..a89f5d3705c
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationRequestTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.action;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationRequest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.security.auth.x500.X500Principal;
+
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DelegatePkiAuthenticationRequestTests extends AbstractXContentTestCase {
+
+ public void testRequestValidation() {
+ expectThrows(NullPointerException.class, () -> new DelegatePkiAuthenticationRequest((List) null));
+
+ DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(Arrays.asList(new X509Certificate[0]));
+ ActionRequestValidationException ve = request.validate();
+ assertNotNull(ve);
+ assertEquals(1, ve.validationErrors().size());
+ assertThat(ve.validationErrors().get(0), is("certificates chain must not be empty"));
+
+ List mockCertChain = new ArrayList<>(2);
+ mockCertChain.add(mock(X509Certificate.class));
+ when(mockCertChain.get(0).getIssuerX500Principal()).thenReturn(new X500Principal("CN=Test, OU=elasticsearch, O=org"));
+ mockCertChain.add(mock(X509Certificate.class));
+ when(mockCertChain.get(1).getSubjectX500Principal()).thenReturn(new X500Principal("CN=Not Test, OU=elasticsearch, O=org"));
+ request = new DelegatePkiAuthenticationRequest(mockCertChain);
+ ve = request.validate();
+ assertNotNull(ve);
+ assertEquals(1, ve.validationErrors().size());
+ assertThat(ve.validationErrors().get(0), is("certificates chain must be an ordered chain"));
+
+ request = new DelegatePkiAuthenticationRequest(Arrays.asList(randomArray(1, 3, X509Certificate[]::new, () -> {
+ X509Certificate mockX509Certificate = mock(X509Certificate.class);
+ when(mockX509Certificate.getSubjectX500Principal()).thenReturn(new X500Principal("CN=Test, OU=elasticsearch, O=org"));
+ when(mockX509Certificate.getIssuerX500Principal()).thenReturn(new X500Principal("CN=Test, OU=elasticsearch, O=org"));
+ return mockX509Certificate;
+ })));
+ ve = request.validate();
+ assertNull(ve);
+ }
+
+ public void testSerialization() throws Exception {
+ List certificates = randomCertificateList();
+ DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(certificates);
+ try (BytesStreamOutput out = new BytesStreamOutput()) {
+ request.writeTo(out);
+ try (StreamInput in = out.bytes().streamInput()) {
+ final DelegatePkiAuthenticationRequest serialized = new DelegatePkiAuthenticationRequest(in);
+ assertThat(request.getCertificateChain(), is(certificates));
+ assertThat(request, is(serialized));
+ assertThat(request.hashCode(), is(serialized.hashCode()));
+ }
+ }
+ }
+
+ private List randomCertificateList() {
+ List certificates = Arrays.asList(randomArray(1, 3, X509Certificate[]::new, () -> {
+ try {
+ return readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/"
+ + randomFrom("testclient.crt", "testnode.crt", "testnode-ip-only.crt", "openldap.crt", "samba4.crt")));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ return certificates;
+ }
+
+ private X509Certificate readCert(Path path) throws Exception {
+ try (InputStream in = Files.newInputStream(path)) {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) factory.generateCertificate(in);
+ }
+ }
+
+ @Override
+ protected DelegatePkiAuthenticationRequest createTestInstance() {
+ List certificates = randomCertificateList();
+ return new DelegatePkiAuthenticationRequest(certificates);
+ }
+
+ @Override
+ protected DelegatePkiAuthenticationRequest doParseInstance(XContentParser parser) throws IOException {
+ return DelegatePkiAuthenticationRequest.fromXContent(parser);
+ }
+
+ @Override
+ protected boolean supportsUnknownFields() {
+ return false;
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationResponseTests.java
new file mode 100644
index 00000000000..362068053b7
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationResponseTests.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.core.action;
+
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse;
+
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.is;
+
+public class DelegatePkiAuthenticationResponseTests extends AbstractXContentTestCase {
+
+ public void testSerialization() throws Exception {
+ DelegatePkiAuthenticationResponse response = createTestInstance();
+ try (BytesStreamOutput output = new BytesStreamOutput()) {
+ response.writeTo(output);
+ try (StreamInput input = output.bytes().streamInput()) {
+ DelegatePkiAuthenticationResponse serialized = new DelegatePkiAuthenticationResponse(input);
+ assertThat(response.getAccessToken(), is(serialized.getAccessToken()));
+ assertThat(response.getExpiresIn(), is(serialized.getExpiresIn()));
+ assertThat(response, is(serialized));
+ }
+ }
+ }
+
+ @Override
+ protected DelegatePkiAuthenticationResponse createTestInstance() {
+ return new DelegatePkiAuthenticationResponse(randomAlphaOfLengthBetween(0, 10),
+ TimeValue.parseTimeValue(randomTimeValue(), getClass().getSimpleName() + ".expiresIn"));
+ }
+
+ @Override
+ protected DelegatePkiAuthenticationResponse doParseInstance(XContentParser parser) throws IOException {
+ return DelegatePkiAuthenticationResponse.PARSER.apply(parser, null);
+ }
+
+ @Override
+ protected boolean supportsUnknownFields() {
+ return true;
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java
index 4433b9d3750..783500d8123 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java
@@ -111,6 +111,7 @@ import org.elasticsearch.xpack.core.ml.annotations.AnnotationIndex;
import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndexFields;
import org.elasticsearch.xpack.core.ml.notifications.AuditorField;
import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction;
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction;
@@ -227,6 +228,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(snapshotUserRole.cluster().check(AckWatchAction.NAME, request), is(false));
assertThat(snapshotUserRole.cluster().check(ActivateWatchAction.NAME, request), is(false));
assertThat(snapshotUserRole.cluster().check(WatcherServiceAction.NAME, request), is(false));
+ assertThat(snapshotUserRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(snapshotUserRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(randomAlphaOfLengthBetween(8, 24)), is(false));
assertThat(snapshotUserRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), is(false));
@@ -263,6 +265,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(ingestAdminRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(ingestAdminRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
assertThat(ingestAdminRole.cluster().check(MonitoringBulkAction.NAME, request), is(false));
+ assertThat(ingestAdminRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(ingestAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false));
assertThat(ingestAdminRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)),
@@ -322,6 +325,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
// Everything else
assertThat(kibanaRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false));
+ assertThat(kibanaRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(true));
assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false));
assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(".reporting"), is(false));
@@ -391,6 +395,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(kibanaUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(kibanaUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
assertThat(kibanaUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false));
+ assertThat(kibanaUserRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(kibanaUserRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false));
@@ -430,6 +435,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(monitoringUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(monitoringUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
assertThat(monitoringUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false));
+ assertThat(monitoringUserRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(monitoringUserRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false));
@@ -491,6 +497,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(remoteMonitoringAgentRole.cluster().check(AckWatchAction.NAME, request), is(false));
assertThat(remoteMonitoringAgentRole.cluster().check(ActivateWatchAction.NAME, request), is(false));
assertThat(remoteMonitoringAgentRole.cluster().check(WatcherServiceAction.NAME, request), is(false));
+ assertThat(remoteMonitoringAgentRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
// we get this from the cluster:monitor privilege
assertThat(remoteMonitoringAgentRole.cluster().check(WatcherStatsAction.NAME, request), is(true));
@@ -545,6 +552,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(remoteMonitoringAgentRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(remoteMonitoringAgentRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
assertThat(remoteMonitoringAgentRole.cluster().check(MonitoringBulkAction.NAME, request), is(false));
+ assertThat(remoteMonitoringAgentRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(remoteMonitoringAgentRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false));
@@ -641,6 +649,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(reportingUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(reportingUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
assertThat(reportingUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false));
+ assertThat(reportingUserRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(reportingUserRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false));
@@ -681,6 +690,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(dashboardsOnlyUserRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(dashboardsOnlyUserRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
assertThat(dashboardsOnlyUserRole.cluster().check(MonitoringBulkAction.NAME, request), is(false));
+ assertThat(dashboardsOnlyUserRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(dashboardsOnlyUserRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false));
@@ -713,6 +723,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(superuserRole.cluster().check(PutUserAction.NAME, request), is(true));
assertThat(superuserRole.cluster().check(PutRoleAction.NAME, request), is(true));
assertThat(superuserRole.cluster().check(PutIndexTemplateAction.NAME, request), is(true));
+ assertThat(superuserRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(true));
assertThat(superuserRole.cluster().check("internal:admin/foo", request), is(false));
final Settings indexSettings = Settings.builder().put("index.version.created", Version.CURRENT).build();
@@ -784,6 +795,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(logstashSystemRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false));
assertThat(logstashSystemRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(logstashSystemRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
+ assertThat(logstashSystemRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(logstashSystemRole.cluster().check(MonitoringBulkAction.NAME, request), is(true));
assertThat(logstashSystemRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
@@ -812,6 +824,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(beatsAdminRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(beatsAdminRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
assertThat(beatsAdminRole.cluster().check(MonitoringBulkAction.NAME, request), is(false));
+ assertThat(beatsAdminRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(beatsAdminRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
@@ -848,6 +861,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(beatsSystemRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false));
assertThat(beatsSystemRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(beatsSystemRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
+ assertThat(beatsSystemRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(beatsSystemRole.cluster().check(MonitoringBulkAction.NAME, request), is(true));
assertThat(beatsSystemRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
@@ -881,6 +895,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(APMSystemRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false));
assertThat(APMSystemRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(APMSystemRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
+ assertThat(APMSystemRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(APMSystemRole.cluster().check(MonitoringBulkAction.NAME, request), is(true));
assertThat(APMSystemRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
@@ -902,6 +917,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
Role role = Role.builder(roleDescriptor, null).build();
+ assertThat(role.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false));
assertNoAccessAllowed(role, "foo");
@@ -968,6 +984,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(role.cluster().check(UpdateProcessAction.NAME, request), is(false)); // internal use only
assertThat(role.cluster().check(ValidateDetectorAction.NAME, request), is(true));
assertThat(role.cluster().check(ValidateJobConfigAction.NAME, request), is(true));
+ assertThat(role.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
assertNoAccessAllowed(role, "foo");
@@ -1051,6 +1068,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(role.cluster().check(UpdateProcessAction.NAME, request), is(false));
assertThat(role.cluster().check(ValidateDetectorAction.NAME, request), is(false));
assertThat(role.cluster().check(ValidateJobConfigAction.NAME, request), is(false));
+ assertThat(role.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
assertNoAccessAllowed(role, "foo");
@@ -1092,6 +1110,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(role.cluster().check(PutDataFrameTransformAction.NAME, request), is(true));
assertThat(role.cluster().check(StartDataFrameTransformAction.NAME, request), is(true));
assertThat(role.cluster().check(StopDataFrameTransformAction.NAME, request), is(true));
+ assertThat(role.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
assertOnlyReadAllowed(role, ".data-frame-notifications-1");
@@ -1128,6 +1147,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(role.cluster().check(PutDataFrameTransformAction.NAME, request), is(false));
assertThat(role.cluster().check(StartDataFrameTransformAction.NAME, request), is(false));
assertThat(role.cluster().check(StopDataFrameTransformAction.NAME, request), is(false));
+ assertThat(role.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
assertOnlyReadAllowed(role, ".data-frame-notifications-1");
@@ -1165,6 +1185,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(role.cluster().check(ActivateWatchAction.NAME, request), is(true));
assertThat(role.cluster().check(WatcherServiceAction.NAME, request), is(true));
assertThat(role.cluster().check(WatcherStatsAction.NAME, request), is(true));
+ assertThat(role.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false));
@@ -1194,6 +1215,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(role.cluster().check(ActivateWatchAction.NAME, request), is(false));
assertThat(role.cluster().check(WatcherServiceAction.NAME, request), is(false));
assertThat(role.cluster().check(WatcherStatsAction.NAME, request), is(true));
+ assertThat(role.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false));
@@ -1262,6 +1284,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
assertThat(logstashAdminRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false));
assertThat(logstashAdminRole.cluster().check(ClusterRerouteAction.NAME, request), is(false));
assertThat(logstashAdminRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false));
+ assertThat(logstashAdminRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request), is(false));
assertThat(logstashAdminRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false));
@@ -1290,6 +1313,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
Role codeAdminRole = Role.builder(roleDescriptor, null).build();
+ assertThat(codeAdminRole.cluster().check(DelegatePkiAuthenticationAction.NAME, mock(TransportRequest.class)), is(false));
assertThat(codeAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false));
assertThat(codeAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(".reporting"), is(false));
@@ -1316,6 +1340,7 @@ public class ReservedRolesStoreTests extends ESTestCase {
Role codeUserRole = Role.builder(roleDescriptor, null).build();
+ assertThat(codeUserRole.cluster().check(DelegatePkiAuthenticationAction.NAME, mock(TransportRequest.class)), is(false));
assertThat(codeUserRole.indices().allowedIndicesMatcher(SearchAction.NAME).test("foo"), is(false));
assertThat(codeUserRole.indices().allowedIndicesMatcher(SearchAction.NAME).test(".reporting"), is(false));
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheck.java
index 8f5012e1eca..eb12d9ef688 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheck.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheck.java
@@ -31,18 +31,18 @@ class PkiRealmBootstrapCheck implements BootstrapCheck {
}
/**
- * If a PKI realm is enabled, checks to see if SSL and Client authentication are enabled on at
+ * If a PKI realm is enabled, and does not support delegation(default), checks to see if SSL and Client authentication are enabled on at
* least one network communication layer.
*/
@Override
public BootstrapCheckResult check(BootstrapContext context) {
final Settings settings = context.settings();
final Map realms = RealmSettings.getRealmSettings(settings);
- final boolean pkiRealmEnabled = realms.entrySet().stream()
+ final boolean pkiRealmEnabledWithoutDelegation = realms.entrySet().stream()
.filter(e -> PkiRealmSettings.TYPE.equals(e.getKey().getType()))
.map(Map.Entry::getValue)
- .anyMatch(s -> s.getAsBoolean("enabled", true));
- if (pkiRealmEnabled) {
+ .anyMatch(s -> s.getAsBoolean("enabled", true) && (false == s.getAsBoolean("delegation.enabled", false)));
+ if (pkiRealmEnabledWithoutDelegation) {
for (String contextName : getSslContextNames(settings)) {
final SSLConfiguration configuration = sslService.getSSLConfiguration(contextName);
if (sslService.isSSLClientAuthEnabled(configuration)) {
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
index 595c73a1c87..6b7fdadb2d0 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java
@@ -77,6 +77,7 @@ import org.elasticsearch.xpack.core.security.SecurityExtension;
import org.elasticsearch.xpack.core.security.SecurityField;
import org.elasticsearch.xpack.core.security.SecuritySettings;
import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
import org.elasticsearch.xpack.core.security.action.GetApiKeyAction;
import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction;
@@ -136,6 +137,7 @@ import org.elasticsearch.xpack.core.ssl.action.GetCertificateInfoAction;
import org.elasticsearch.xpack.core.ssl.action.TransportGetCertificateInfoAction;
import org.elasticsearch.xpack.core.ssl.rest.RestGetCertificateInfoAction;
import org.elasticsearch.xpack.security.action.TransportCreateApiKeyAction;
+import org.elasticsearch.xpack.security.action.TransportDelegatePkiAuthenticationAction;
import org.elasticsearch.xpack.security.action.TransportGetApiKeyAction;
import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction;
import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter;
@@ -197,6 +199,7 @@ import org.elasticsearch.xpack.security.authz.store.NativeRolesStore;
import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor;
import org.elasticsearch.xpack.security.rest.SecurityRestFilter;
import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction;
+import org.elasticsearch.xpack.security.rest.action.RestDelegatePkiAuthenticationAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestCreateApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction;
@@ -778,7 +781,8 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
new ActionHandler<>(DeletePrivilegesAction.INSTANCE, TransportDeletePrivilegesAction.class),
new ActionHandler<>(CreateApiKeyAction.INSTANCE, TransportCreateApiKeyAction.class),
new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class),
- new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class)
+ new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class),
+ new ActionHandler<>(DelegatePkiAuthenticationAction.INSTANCE, TransportDelegatePkiAuthenticationAction.class)
);
}
@@ -834,7 +838,8 @@ public class Security extends Plugin implements ActionPlugin, IngestPlugin, Netw
new RestDeletePrivilegesAction(settings, restController, getLicenseState()),
new RestCreateApiKeyAction(settings, restController, getLicenseState()),
new RestInvalidateApiKeyAction(settings, restController, getLicenseState()),
- new RestGetApiKeyAction(settings, restController, getLicenseState())
+ new RestGetApiKeyAction(settings, restController, getLicenseState()),
+ new RestDelegatePkiAuthenticationAction(settings, restController, getLicenseState())
);
}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java
new file mode 100644
index 00000000000..0da00b5c013
--- /dev/null
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.security.action;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.message.ParameterizedMessage;
+import org.apache.logging.log4j.util.Supplier;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.HandledTransportAction;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationRequest;
+import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse;
+import org.elasticsearch.xpack.core.security.authc.Authentication;
+import org.elasticsearch.xpack.security.authc.AuthenticationService;
+import org.elasticsearch.xpack.security.authc.TokenService;
+import org.elasticsearch.xpack.security.authc.pki.X509AuthenticationToken;
+
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+
+/**
+ * Implements the exchange of an {@code X509Certificate} chain into an access token. The certificate chain is represented as an array where
+ * the first element is the target certificate containing the subject distinguished name that is requesting access. This may be followed by
+ * additional certificates, with each subsequent certificate being the one used to certify the previous one. The certificate chain is
+ * validated according to RFC 5280, by sequentially considering the trust configuration of every installed {@code PkiRealm} that has
+ * {@code PkiRealmSettings#DELEGATION_ENABLED_SETTING} set to {@code true} (default is {@code false}). A successfully trusted target
+ * certificate is also subject to the validation of the subject distinguished name according to that respective's realm
+ * {@code PkiRealmSettings#USERNAME_PATTERN_SETTING}.
+ *
+ * IMPORTANT: The association between the subject public key in the target certificate and the corresponding private key is not
+ * validated. This is part of the TLS authentication process and it is delegated to the proxy calling this API. The proxy is trusted
+ * to have performed the TLS authentication, and this API translates that authentication into an Elasticsearch access token.
+ */
+public final class TransportDelegatePkiAuthenticationAction
+ extends HandledTransportAction {
+
+ private static final Logger logger = LogManager.getLogger(TransportDelegatePkiAuthenticationAction.class);
+
+ private final ThreadPool threadPool;
+ private final AuthenticationService authenticationService;
+ private final TokenService tokenService;
+
+ @Inject
+ public TransportDelegatePkiAuthenticationAction(ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters,
+ AuthenticationService authenticationService, TokenService tokenService) {
+ super(DelegatePkiAuthenticationAction.NAME, transportService, actionFilters, DelegatePkiAuthenticationRequest::new);
+ this.threadPool = threadPool;
+ this.authenticationService = authenticationService;
+ this.tokenService = tokenService;
+ }
+
+ @Override
+ protected void doExecute(Task task, DelegatePkiAuthenticationRequest request,
+ ActionListener listener) {
+ final ThreadContext threadContext = threadPool.getThreadContext();
+ Authentication delegateeAuthentication = Authentication.getAuthentication(threadContext);
+ if (delegateeAuthentication == null) {
+ listener.onFailure(new IllegalStateException("Delegatee authentication cannot be null"));
+ return;
+ }
+ final X509AuthenticationToken x509DelegatedToken = X509AuthenticationToken
+ .delegated(request.getCertificateChain().toArray(new X509Certificate[0]), delegateeAuthentication);
+ logger.trace("Attempting to authenticate delegated x509Token [{}]", x509DelegatedToken);
+ try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
+ authenticationService.authenticate(DelegatePkiAuthenticationAction.NAME, request, x509DelegatedToken,
+ ActionListener.wrap(authentication -> {
+ assert authentication != null : "authentication should never be null at this point";
+ tokenService.createOAuth2Tokens(authentication, delegateeAuthentication, Collections.emptyMap(), false,
+ ActionListener.wrap(tuple -> {
+ final TimeValue expiresIn = tokenService.getExpirationDelay();
+ listener.onResponse(new DelegatePkiAuthenticationResponse(tuple.v1(), expiresIn));
+ }, listener::onFailure));
+ }, e -> {
+ logger.debug((Supplier>) () -> new ParameterizedMessage("Delegated x509Token [{}] could not be authenticated",
+ x509DelegatedToken), e);
+ listener.onFailure(e);
+ }));
+ }
+ }
+}
diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java
index 58c1927b8a5..29120ab987a 100644
--- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java
+++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java
@@ -20,6 +20,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.watcher.ResourceWatcherService;
+import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.Realm;
@@ -30,6 +31,7 @@ import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.core.ssl.CertParsingUtils;
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
import org.elasticsearch.xpack.security.authc.BytesKey;
+import org.elasticsearch.xpack.security.authc.TokenService;
import org.elasticsearch.xpack.security.authc.support.CachingRealm;
import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport;
import org.elasticsearch.xpack.security.authc.support.UserRoleMapper;
@@ -43,6 +45,8 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -76,6 +80,7 @@ public class PkiRealm extends Realm implements CachingRealm {
private final UserRoleMapper roleMapper;
private final Cache cache;
private DelegatedAuthorizationSupport delegatedRealms;
+ private final boolean delegationEnabled;
public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, NativeRoleMappingStore nativeRoleMappingStore) {
this(config, new CompositeRoleMapper(config, watcherService, nativeRoleMappingStore));
@@ -84,6 +89,7 @@ public class PkiRealm extends Realm implements CachingRealm {
// pkg private for testing
PkiRealm(RealmConfig config, UserRoleMapper roleMapper) {
super(config);
+ this.delegationEnabled = config.getSetting(PkiRealmSettings.DELEGATION_ENABLED_SETTING);
this.trustManager = trustManagers(config);
this.principalPattern = config.getSetting(PkiRealmSettings.USERNAME_PATTERN_SETTING);
this.roleMapper = roleMapper;
@@ -93,6 +99,7 @@ public class PkiRealm extends Realm implements CachingRealm {
.setMaximumWeight(config.getSetting(PkiRealmSettings.CACHE_MAX_USERS_SETTING))
.build();
this.delegatedRealms = null;
+ validateAuthenticationDelegationConfiguration(config);
}
@Override
@@ -141,15 +148,19 @@ public class PkiRealm extends Realm implements CachingRealm {
assert delegatedRealms != null : "Realm has not been initialized correctly";
X509AuthenticationToken token = (X509AuthenticationToken) authToken;
try {
- final BytesKey fingerprint = computeFingerprint(token.credentials()[0]);
+ final BytesKey fingerprint = computeTokenFingerprint(token);
User user = cache.get(fingerprint);
if (user != null) {
+ logger.debug((Supplier>) () -> new ParameterizedMessage("Using cached authentication for DN [{}], as principal [{}]",
+ token.dn(), user.principal()));
if (delegatedRealms.hasDelegation()) {
delegatedRealms.resolve(user.principal(), listener);
} else {
listener.onResponse(AuthenticationResult.success(user));
}
- } else if (isCertificateChainTrusted(trustManager, token, logger) == false) {
+ } else if (false == delegationEnabled && token.isDelegated()) {
+ listener.onResponse(AuthenticationResult.unsuccessful("Realm does not permit delegation for " + token.dn(), null));
+ } else if (false == isCertificateChainTrusted(token)) {
listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " is not trusted", null));
} else {
// parse the principal again after validating the cert chain, and do not rely on the token.principal one, because that could
@@ -188,7 +199,16 @@ public class PkiRealm extends Realm implements CachingRealm {
}
private void buildUser(X509AuthenticationToken token, String principal, ActionListener listener) {
- final Map metadata = Collections.singletonMap("pki_dn", token.dn());
+ final Map metadata;
+ if (token.isDelegated()) {
+ Map delegatedMetadata = new HashMap<>();
+ delegatedMetadata.put("pki_dn", token.dn());
+ delegatedMetadata.put("pki_delegated_by_user", token.getDelegateeAuthentication().getUser().principal());
+ delegatedMetadata.put("pki_delegated_by_realm", token.getDelegateeAuthentication().getAuthenticatedBy().getName());
+ metadata = Collections.unmodifiableMap(delegatedMetadata);
+ } else {
+ metadata = Collections.singletonMap("pki_dn", token.dn());
+ }
final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(principal, token.dn(), Collections.emptySet(), metadata,
this.config);
roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> {
@@ -219,8 +239,13 @@ public class PkiRealm extends Realm implements CachingRealm {
return principal;
}
- private static boolean isCertificateChainTrusted(X509TrustManager trustManager, X509AuthenticationToken token, Logger logger) {
- if (trustManager != null) {
+ private boolean isCertificateChainTrusted(X509AuthenticationToken token) {
+ if (trustManager == null) {
+ // No extra trust managers specified
+ // If the token is NOT delegated then it is authenticated, because the certificate chain has been validated by the TLS channel.
+ // Otherwise, if the token is delegated, then it cannot be authenticated without a trustManager
+ return token.isDelegated() == false;
+ } else {
try {
trustManager.checkClientTrusted(token.credentials(), AUTH_TYPE);
return true;
@@ -233,9 +258,6 @@ public class PkiRealm extends Realm implements CachingRealm {
}
return false;
}
-
- // No extra trust managers specified, so at this point we can be considered authenticated.
- return true;
}
private X509TrustManager trustManagers(RealmConfig realmConfig) {
@@ -314,9 +336,44 @@ public class PkiRealm extends Realm implements CachingRealm {
}
}
- private static BytesKey computeFingerprint(X509Certificate certificate) throws CertificateEncodingException {
+ @Override
+ public void usageStats(ActionListener