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> listener) { + super.usageStats(ActionListener.wrap(stats -> { + stats.put("has_truststore", trustManager != null); + stats.put("has_authorization_realms", delegatedRealms != null && delegatedRealms.hasDelegation()); + stats.put("has_default_username_pattern", PkiRealmSettings.DEFAULT_USERNAME_PATTERN.equals(principalPattern.pattern())); + stats.put("is_authentication_delegated", delegationEnabled); + listener.onResponse(stats); + }, listener::onFailure)); + } + + private void validateAuthenticationDelegationConfiguration(RealmConfig config) { + if (delegationEnabled) { + List exceptionMessages = new ArrayList<>(2); + if (this.trustManager == null) { + exceptionMessages.add("a trust configuration (" + + config.getConcreteSetting(PkiRealmSettings.CAPATH_SETTING).getKey() + " or " + + config.getConcreteSetting(PkiRealmSettings.TRUST_STORE_PATH).getKey() + ")"); + } + if (false == TokenService.isTokenServiceEnabled(config.settings())) { + exceptionMessages.add("that the token service be also enabled (" + + XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey() + ")"); + } + if (false == exceptionMessages.isEmpty()) { + String message = "PKI realms with delegation enabled require " + exceptionMessages.get(0); + if (exceptionMessages.size() == 2) { + message = message + " and " + exceptionMessages.get(1); + } + throw new IllegalStateException(message); + } + } + } + + static BytesKey computeTokenFingerprint(X509AuthenticationToken token) throws CertificateEncodingException { MessageDigest digest = MessageDigests.sha256(); - digest.update(certificate.getEncoded()); + for (X509Certificate certificate : token.credentials()) { + digest.update(certificate.getEncoded()); + } return new BytesKey(digest.digest()); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java index 30722dbb8a4..57e05c43142 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java @@ -5,21 +5,38 @@ */ package org.elasticsearch.xpack.security.authc.pki; +import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Objects; public class X509AuthenticationToken implements AuthenticationToken { private final String dn; private final X509Certificate[] credentials; + private final Authentication delegateeAuthentication; private String principal; public X509AuthenticationToken(X509Certificate[] certificates) { + this(certificates, null); + } + + private X509AuthenticationToken(X509Certificate[] certificates, Authentication delegateeAuthentication) { this.credentials = Objects.requireNonNull(certificates); + if (false == CertParsingUtils.isOrderedCertificateChain(Arrays.asList(certificates))) { + throw new IllegalArgumentException("certificates chain array is not ordered"); + } this.dn = certificates.length == 0 ? "" : certificates[0].getSubjectX500Principal().toString(); this.principal = this.dn; + this.delegateeAuthentication = delegateeAuthentication; + } + + public static X509AuthenticationToken delegated(X509Certificate[] certificates, Authentication delegateeAuthentication) { + Objects.requireNonNull(delegateeAuthentication); + return new X509AuthenticationToken(certificates, delegateeAuthentication); } @Override @@ -44,4 +61,12 @@ public class X509AuthenticationToken implements AuthenticationToken { public void clearCredentials() { // noop } + + public boolean isDelegated() { + return delegateeAuthentication != null; + } + + public Authentication getDelegateeAuthentication() { + return delegateeAuthentication; + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java new file mode 100644 index 00000000000..c63d965a3b7 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java @@ -0,0 +1,81 @@ +/* + * 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.rest.action; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction; +import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationRequest; +import org.elasticsearch.xpack.security.action.TransportDelegatePkiAuthenticationAction; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse; +import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +/** + * Implements the exchange of an {@code X509Certificate} chain into an access token. The chain is represented as an ordered string array. + * Each string in the array is a base64-encoded (Section 4 of RFC4648 - not base64url-encoded) DER PKIX certificate value. + * See also {@link TransportDelegatePkiAuthenticationAction}. + */ +public final class RestDelegatePkiAuthenticationAction extends SecurityBaseRestHandler { + + protected Logger logger = LogManager.getLogger(RestDelegatePkiAuthenticationAction.class); + + public RestDelegatePkiAuthenticationAction(Settings settings, RestController controller, XPackLicenseState xPackLicenseState) { + super(settings, xPackLicenseState); + controller.registerHandler(POST, "/_security/delegate_pki", this); + } + + @Override + protected Exception checkFeatureAvailable(RestRequest request) { + Exception failedFeature = super.checkFeatureAvailable(request); + if (failedFeature != null) { + return failedFeature; + } else if (Realms.isRealmTypeAvailable(licenseState.allowedRealmType(), PkiRealmSettings.TYPE)) { + return null; + } else { + logger.info("The '{}' realm is not available under the current license", PkiRealmSettings.TYPE); + return LicenseUtils.newComplianceException(PkiRealmSettings.TYPE); + } + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final DelegatePkiAuthenticationRequest delegatePkiRequest = DelegatePkiAuthenticationRequest.fromXContent(parser); + return channel -> client.execute(DelegatePkiAuthenticationAction.INSTANCE, delegatePkiRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(DelegatePkiAuthenticationResponse delegatePkiResponse, XContentBuilder builder) + throws Exception { + delegatePkiResponse.toXContent(builder, channel.request()); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } + + @Override + public String getName() { + return "delegate_pki_action"; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java index bd9d58e6ea5..07199dc3975 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java @@ -18,6 +18,7 @@ import org.elasticsearch.action.admin.indices.get.GetIndexRequest; import org.elasticsearch.action.admin.indices.get.GetIndexResponse; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.Client; +import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.health.ClusterHealthStatus; @@ -534,4 +535,10 @@ public abstract class SecurityIntegTestCase extends ESIntegTestCase { protected static Hasher getFastStoredHashAlgoForTests() { return Hasher.resolve(randomFrom("pbkdf2", "pbkdf2_1000", "bcrypt", "bcrypt9")); } + + protected class TestRestHighLevelClient extends RestHighLevelClient { + public TestRestHighLevelClient() { + super(getRestClient(), client -> {}, Collections.emptyList()); + } + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java index 2c62ce71d45..4e770e1376b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java @@ -89,6 +89,17 @@ public class PkiRealmBootstrapCheckTests extends AbstractBootstrapCheckTestCase assertFalse(runCheck(settings, env).isFailure()); } + public void testBootstrapCheckWithDelegationEnabled() throws Exception { + Settings settings = Settings.builder() + .put("xpack.security.authc.realms.pki.test_pki.enabled", true) + .put("xpack.security.authc.realms.pki.test_pki.delegation.enabled", true) + .put("xpack.security.transport.ssl.client_authentication", "none") + .put("path.home", createTempDir()) + .build(); + Environment env = TestEnvironment.newEnvironment(settings); + assertFalse(runCheck(settings, env).isFailure()); + } + public void testBootstrapCheckWithClosedSecuredSetting() throws Exception { final boolean expectFail = randomBoolean(); final MockSecureSettings secureSettings = new MockSecureSettings(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmSettingsTests.java index 6061469d700..d5e123c2313 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/RealmSettingsTests.java @@ -210,6 +210,9 @@ public class RealmSettingsTests extends ESTestCase { } else { builder.putList("certificate_authorities", generateRandomStringArray(5, 32, false, false)); } + if (randomBoolean()) { + builder.put("delegation.enabled", randomBoolean()); + } return builder; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java new file mode 100644 index 00000000000..10ec1d55591 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java @@ -0,0 +1,338 @@ +/* + * 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.authc.pki; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.ValidationException; +import org.elasticsearch.client.security.AuthenticateResponse; +import org.elasticsearch.client.security.PutRoleMappingRequest; +import org.elasticsearch.client.security.RefreshPolicy; +import org.elasticsearch.client.security.AuthenticateResponse.RealmInfo; +import org.elasticsearch.client.security.DeleteRoleMappingRequest; +import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression; +import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest; +import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse; +import org.elasticsearch.client.security.InvalidateTokenRequest; +import org.elasticsearch.client.security.InvalidateTokenResponse; +import org.elasticsearch.client.security.user.User; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.SecurityIntegTestCase; +import org.elasticsearch.test.SecuritySettingsSourceField; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheRequestBuilder; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.junit.Before; +import org.elasticsearch.test.SecuritySettingsSource; + +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.Collections; +import java.util.Arrays; + +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.emptyCollectionOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.startsWith; + +public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase { + + @Override + public Settings nodeSettings(int nodeOrdinal) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal)) + .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true) + // pki1 does not allow delegation + .put("xpack.security.authc.realms.pki.pki1.order", "1") + .putList("xpack.security.authc.realms.pki.pki1.certificate_authorities", + getDataPath("/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt").toString()) + .put("xpack.security.authc.realms.pki.pki1.files.role_mapping", getDataPath("role_mapping.yml")) + // pki2 allows delegation but has a non-matching username pattern + .put("xpack.security.authc.realms.pki.pki2.order", "2") + .putList("xpack.security.authc.realms.pki.pki2.certificate_authorities", + getDataPath("/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt").toString()) + .put("xpack.security.authc.realms.pki.pki2.username_pattern", "CN=MISMATCH(.*?)(?:,|$)") + .put("xpack.security.authc.realms.pki.pki2.delegation.enabled", true) + .put("xpack.security.authc.realms.pki.pki2.files.role_mapping", getDataPath("role_mapping.yml")) + // pki3 allows delegation and the username pattern (default) matches + .put("xpack.security.authc.realms.pki.pki3.order", "3") + .putList("xpack.security.authc.realms.pki.pki3.certificate_authorities", + getDataPath("/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt").toString()) + .put("xpack.security.authc.realms.pki.pki3.delegation.enabled", true) + .put("xpack.security.authc.realms.pki.pki3.files.role_mapping", getDataPath("role_mapping.yml")) + .build(); + } + + @Override + protected String configUsers() { + final String usersPasswdHashed = new String(Hasher.resolve( + randomFrom("pbkdf2", "pbkdf2_1000", "bcrypt", "bcrypt9")).hash(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + return super.configUsers() + + "user_manage:" + usersPasswdHashed + "\n" + + "user_manage_security:" + usersPasswdHashed + "\n" + + "user_delegate_pki:" + usersPasswdHashed + "\n" + + "user_all:" + usersPasswdHashed + "\n" + + "kibana_system:" + usersPasswdHashed + "\n"; + } + + @Override + protected String configRoles() { + return super.configRoles() + "\n" + + "role_manage:\n" + + " cluster: [ manage ]\n" + + "\n" + + "role_manage_security:\n" + + " cluster: [ manage_security ]\n" + + "\n" + + "role_delegate_pki:\n" + + " cluster: [ delegate_pki ]\n" + + "\n" + + "role_all:\n" + + " cluster: [ all ]\n"; + } + + @Override + protected String configUsersRoles() { + return super.configUsersRoles() + "\n" + + "role_manage:user_manage\n" + + "role_manage_security:user_manage_security\n" + + "role_delegate_pki:user_delegate_pki\n" + + "role_all:user_all\n" + + "kibana_system:kibana_system\n"; + } + + @Override + protected boolean transportSSLEnabled() { + return true; + } + + @Override + protected boolean addMockHttpTransport() { + return false; // enable http + } + + @Before + void clearRealmCache() { + new ClearRealmCacheRequestBuilder(client()).get(); + } + + public void testDelegateThenAuthenticate() throws Exception { + final X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); + final X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); + final X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt"); + DelegatePkiAuthenticationRequest delegatePkiRequest; + // trust root is optional + if (randomBoolean()) { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA)); + } else { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA)); + } + + try (RestHighLevelClient restClient = new TestRestHighLevelClient()) { + for (String delegateeUsername : Arrays.asList("user_all", "user_delegate_pki", "kibana_system")) { + // delegate + RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", + basicAuthHeaderValue(delegateeUsername, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security().delegatePkiAuthentication(delegatePkiRequest, + optionsBuilder.build()); + String token = delegatePkiResponse.getAccessToken(); + assertThat(token, is(notNullValue())); + // authenticate + optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", "Bearer " + token); + AuthenticateResponse resp = restClient.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("pki3")); + assertThat(authnRealm.getType(), is("pki")); + } + } + } + + public void testTokenInvalidate() throws Exception { + final X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); + final X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); + final X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt"); + DelegatePkiAuthenticationRequest delegatePkiRequest; + // trust root is optional + if (randomBoolean()) { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA)); + } else { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA)); + } + + try (RestHighLevelClient restClient = new TestRestHighLevelClient()) { + String delegateeUsername = randomFrom("user_all", "user_delegate_pki", "kibana_system"); + // delegate + RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", + basicAuthHeaderValue(delegateeUsername, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security().delegatePkiAuthentication(delegatePkiRequest, + optionsBuilder.build()); + String token = delegatePkiResponse.getAccessToken(); + assertThat(token, is(notNullValue())); + // authenticate + optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", "Bearer " + token); + AuthenticateResponse resp = restClient.security().authenticate(optionsBuilder.build()); + User user = resp.getUser(); + assertThat(user, is(notNullValue())); + assertThat(user.getUsername(), is("Elasticsearch Test Client")); + assertThat(user.getMetadata().get("pki_dn"), is(notNullValue())); + assertThat(user.getMetadata().get("pki_dn"), is("O=org, OU=Elasticsearch, CN=Elasticsearch Test Client")); + assertThat(user.getMetadata().get("pki_delegated_by_user"), is(notNullValue())); + assertThat(user.getMetadata().get("pki_delegated_by_user"), is(delegateeUsername)); + assertThat(user.getMetadata().get("pki_delegated_by_realm"), is(notNullValue())); + assertThat(user.getMetadata().get("pki_delegated_by_realm"), is("file")); + // no roles because no role mappings + assertThat(user.getRoles(), is(emptyCollectionOf(String.class))); + RealmInfo authnRealm = resp.getAuthenticationRealm(); + assertThat(authnRealm, is(notNullValue())); + assertThat(authnRealm.getName(), is("pki3")); + assertThat(authnRealm.getType(), is("pki")); + // invalidate + InvalidateTokenRequest invalidateRequest = new InvalidateTokenRequest(token, null, null, null); + optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", + basicAuthHeaderValue(delegateeUsername, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + InvalidateTokenResponse invalidateResponse = restClient.security().invalidateToken(invalidateRequest, optionsBuilder.build()); + assertThat(invalidateResponse.getInvalidatedTokens(), is(1)); + assertThat(invalidateResponse.getErrorsCount(), is(0)); + // failed authenticate + ElasticsearchStatusException e1 = expectThrows(ElasticsearchStatusException.class, () -> restClient.security() + .authenticate(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + token).build())); + assertThat(e1.getMessage(), is("Elasticsearch exception [type=security_exception, reason=token expired]")); + } + } + + public void testDelegateUnauthorized() throws Exception { + final X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); + final X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); + final X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt"); + DelegatePkiAuthenticationRequest delegatePkiRequest; + // trust root is optional + if (randomBoolean()) { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA)); + } else { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA)); + } + try (RestHighLevelClient restClient = new TestRestHighLevelClient()) { + for (String delegateeUsername : Arrays.asList("user_manage", "user_manage_security")) { + RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", + basicAuthHeaderValue(delegateeUsername, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> { + restClient.security().delegatePkiAuthentication(delegatePkiRequest, optionsBuilder.build()); + }); + assertThat(e.getMessage(), startsWith("Elasticsearch exception [type=security_exception, reason=action" + + " [cluster:admin/xpack/security/delegate_pki] is unauthorized for user")); + } + } + } + + public void testDelegatePkiWithRoleMapping() throws Exception { + X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); + X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); + X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt"); + DelegatePkiAuthenticationRequest delegatePkiRequest; + // trust root is optional + if (randomBoolean()) { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA)); + } else { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA)); + } + final RequestOptions.Builder testUserOptionsBuilder = RequestOptions.DEFAULT.toBuilder(); + testUserOptionsBuilder.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME, + new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()))); + final RequestOptions testUserOptions = testUserOptionsBuilder.build(); + try (RestHighLevelClient restClient = new TestRestHighLevelClient()) { + // put role mappings for delegated PKI + PutRoleMappingRequest request = new PutRoleMappingRequest("role_by_delegated_user", true, + Collections.singletonList("role_by_delegated_user"), Collections.emptyList(), + new FieldRoleMapperExpression("metadata.pki_delegated_by_user", "test_user"), null, RefreshPolicy.IMMEDIATE); + restClient.security().putRoleMapping(request, testUserOptions); + request = new PutRoleMappingRequest("role_by_delegated_realm", true, Collections.singletonList("role_by_delegated_realm"), + Collections.emptyList(), new FieldRoleMapperExpression("metadata.pki_delegated_by_realm", "file"), null, + RefreshPolicy.IMMEDIATE); + restClient.security().putRoleMapping(request, testUserOptions); + // delegate + DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security().delegatePkiAuthentication(delegatePkiRequest, + testUserOptions); + // authenticate + AuthenticateResponse resp = restClient.security().authenticate(RequestOptions.DEFAULT.toBuilder() + .addHeader("Authorization", "Bearer " + delegatePkiResponse.getAccessToken()).build()); + User user = resp.getUser(); + assertThat(user, is(notNullValue())); + assertThat(user.getUsername(), is("Elasticsearch Test Client")); + assertThat(user.getMetadata().get("pki_dn"), is(notNullValue())); + assertThat(user.getMetadata().get("pki_dn"), is("O=org, OU=Elasticsearch, CN=Elasticsearch Test Client")); + assertThat(user.getMetadata().get("pki_delegated_by_user"), is(notNullValue())); + assertThat(user.getMetadata().get("pki_delegated_by_user"), is("test_user")); + assertThat(user.getMetadata().get("pki_delegated_by_realm"), is(notNullValue())); + assertThat(user.getMetadata().get("pki_delegated_by_realm"), is("file")); + // assert roles + assertThat(user.getRoles(), containsInAnyOrder("role_by_delegated_user", "role_by_delegated_realm")); + RealmInfo authnRealm = resp.getAuthenticationRealm(); + assertThat(authnRealm, is(notNullValue())); + assertThat(authnRealm.getName(), is("pki3")); + assertThat(authnRealm.getType(), is("pki")); + // delete role mappings for delegated PKI + restClient.security().deleteRoleMapping(new DeleteRoleMappingRequest("role_by_delegated_user", RefreshPolicy.IMMEDIATE), + testUserOptions); + restClient.security().deleteRoleMapping(new DeleteRoleMappingRequest("role_by_delegated_realm", RefreshPolicy.IMMEDIATE), + testUserOptions); + } + } + + public void testIncorrectCertChain() throws Exception { + X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); + X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); + X509Certificate bogusCertificate = readCertForPkiDelegation("bogus.crt"); + RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME, + new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()))); + try (RestHighLevelClient restClient = new TestRestHighLevelClient()) { + // incomplete cert chain + DelegatePkiAuthenticationRequest delegatePkiRequest1 = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate)); + ElasticsearchStatusException e1 = expectThrows(ElasticsearchStatusException.class, + () -> restClient.security().delegatePkiAuthentication(delegatePkiRequest1, optionsBuilder.build())); + assertThat(e1.getMessage(), is("Elasticsearch exception [type=security_exception, reason=unable to authenticate user" + + " [O=org, OU=Elasticsearch, CN=Elasticsearch Test Client] for action [cluster:admin/xpack/security/delegate_pki]]")); + // swapped order + DelegatePkiAuthenticationRequest delegatePkiRequest2 = new DelegatePkiAuthenticationRequest( + Arrays.asList(intermediateCA, clientCertificate)); + ValidationException e2 = expectThrows(ValidationException.class, + () -> restClient.security().delegatePkiAuthentication(delegatePkiRequest2, optionsBuilder.build())); + assertThat(e2.getMessage(), is("Validation Failed: 1: certificates chain must be an ordered chain;")); + // bogus certificate + DelegatePkiAuthenticationRequest delegatePkiRequest3 = new DelegatePkiAuthenticationRequest(Arrays.asList(bogusCertificate)); + ElasticsearchStatusException e3 = expectThrows(ElasticsearchStatusException.class, + () -> restClient.security().delegatePkiAuthentication(delegatePkiRequest3, optionsBuilder.build())); + assertThat(e3.getMessage(), startsWith("Elasticsearch exception [type=security_exception, reason=unable to authenticate user")); + } + } + + private X509Certificate readCertForPkiDelegation(String certName) throws Exception { + Path path = getDataPath("/org/elasticsearch/xpack/security/action/pki_delegation/" + certName); + try (InputStream in = Files.newInputStream(path)) { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(in); + } + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index e5eb265979a..132c22846cb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -17,6 +17,8 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.InternalRealmsSettings; import org.elasticsearch.xpack.core.security.authc.Realm; @@ -25,6 +27,7 @@ import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.support.NoOpLogger; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.BytesKey; import org.elasticsearch.xpack.security.authc.support.MockLookupRealm; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.junit.Before; @@ -40,6 +43,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @@ -47,6 +51,7 @@ import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; @@ -72,14 +77,18 @@ public class PkiRealmTests extends ESTestCase { when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); } - public void testTokenSupport() { + public void testTokenSupport() throws Exception { RealmConfig config = new RealmConfig(new RealmConfig.RealmIdentifier("pki", "my_pki"), globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); PkiRealm realm = new PkiRealm(config, mock(UserRoleMapper.class)); + assertRealmUsageStats(realm, false, false, true, false); assertThat(realm.supports(null), is(false)); assertThat(realm.supports(new UsernamePasswordToken("", new SecureString(new char[0]))), is(false)); - assertThat(realm.supports(new X509AuthenticationToken(new X509Certificate[0])), is(true)); + X509AuthenticationToken token = randomBoolean() + ? X509AuthenticationToken.delegated(new X509Certificate[0], mock(Authentication.class)) + : new X509AuthenticationToken(new X509Certificate[0]); + assertThat(realm.supports(token), is(true)); } public void testExtractToken() throws Exception { @@ -92,6 +101,7 @@ public class PkiRealmTests extends ESTestCase { X509AuthenticationToken token = realm.token(threadContext); assertThat(token, is(notNullValue())); assertThat(token.dn(), is("CN=Elasticsearch Test Node, OU=elasticsearch, O=org")); + assertThat(token.isDelegated(), is(false)); } public void testAuthenticateBasedOnCertToken() throws Exception { @@ -114,7 +124,6 @@ public class PkiRealmTests extends ESTestCase { final String expectedUsername = PkiRealm.getPrincipalFromSubjectDN(Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), token, NoOpLogger.INSTANCE); final AuthenticationResult result = authenticate(token, realm); - final PlainActionFuture future; assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); User user = result.getUser(); assertThat(user, is(notNullValue())); @@ -199,6 +208,7 @@ public class PkiRealmTests extends ESTestCase { X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); UserRoleMapper roleMapper = buildRoleMapper(); PkiRealm realm = buildRealm(roleMapper, settings); + assertRealmUsageStats(realm, false, false, false, false); threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); X509AuthenticationToken token = realm.token(threadContext); @@ -218,6 +228,7 @@ public class PkiRealmTests extends ESTestCase { X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); UserRoleMapper roleMapper = buildRoleMapper(); PkiRealm realm = buildRealm(roleMapper, settings); + assertRealmUsageStats(realm, false, false, false, false); threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); X509AuthenticationToken token = realm.token(threadContext); @@ -238,6 +249,7 @@ public class PkiRealmTests extends ESTestCase { .build(); ThreadContext threadContext = new ThreadContext(globalSettings); PkiRealm realm = buildRealm(roleMapper, settings); + assertRealmUsageStats(realm, true, false, true, false); threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); @@ -249,6 +261,97 @@ public class PkiRealmTests extends ESTestCase { assertThat(user.roles().length, is(0)); } + public void testAuthenticationDelegationFailsWithoutTokenServiceAndTruststore() throws Exception { + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.delegation.enabled", true) + .build(); + IllegalStateException e = expectThrows(IllegalStateException.class, + () -> new PkiRealm(new RealmConfig(new RealmConfig.RealmIdentifier("pki", "my_pki"), settings, + TestEnvironment.newEnvironment(globalSettings), threadContext), mock(UserRoleMapper.class))); + assertThat(e.getMessage(), + is("PKI realms with delegation enabled require a trust configuration " + + "(xpack.security.authc.realms.pki.my_pki.certificate_authorities or " + + "xpack.security.authc.realms.pki.my_pki.truststore.path)" + + " and that the token service be also enabled (xpack.security.authc.token.enabled)")); + } + + public void testAuthenticationDelegationFailsWithoutTruststore() throws Exception { + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.delegation.enabled", true) + .put("xpack.security.authc.token.enabled", true) + .build(); + IllegalStateException e = expectThrows(IllegalStateException.class, + () -> new PkiRealm(new RealmConfig(new RealmConfig.RealmIdentifier("pki", "my_pki"), settings, + TestEnvironment.newEnvironment(globalSettings), threadContext), mock(UserRoleMapper.class))); + assertThat(e.getMessage(), + is("PKI realms with delegation enabled require a trust configuration " + + "(xpack.security.authc.realms.pki.my_pki.certificate_authorities " + + "or xpack.security.authc.realms.pki.my_pki.truststore.path)")); + } + + public void testAuthenticationDelegationSuccess() throws Exception { + X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); + Authentication mockAuthentication = mock(Authentication.class); + User mockUser = mock(User.class); + when(mockUser.principal()).thenReturn("mockup_delegate_username"); + RealmRef mockRealmRef = mock(RealmRef.class); + when(mockRealmRef.getName()).thenReturn("mockup_delegate_realm"); + when(mockAuthentication.getUser()).thenReturn(mockUser); + when(mockAuthentication.getAuthenticatedBy()).thenReturn(mockRealmRef); + X509AuthenticationToken delegatedToken = X509AuthenticationToken.delegated(new X509Certificate[] { certificate }, + mockAuthentication); + + UserRoleMapper roleMapper = buildRoleMapper(); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.security.authc.realms.pki.my_pki.truststore.secure_password", "testnode"); + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.truststore.path", + getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks")) + .put("xpack.security.authc.realms.pki.my_pki.delegation.enabled", true) + .put("xpack.security.authc.token.enabled", true) + .setSecureSettings(secureSettings) + .build(); + PkiRealm realmWithDelegation = buildRealm(roleMapper, settings); + assertRealmUsageStats(realmWithDelegation, true, false, true, true); + + AuthenticationResult result = authenticate(delegatedToken, realmWithDelegation); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), is(notNullValue())); + assertThat(result.getUser().principal(), is("Elasticsearch Test Node")); + assertThat(result.getUser().roles(), is(notNullValue())); + assertThat(result.getUser().roles().length, is(0)); + assertThat(result.getUser().metadata().get("pki_delegated_by_user"), is("mockup_delegate_username")); + assertThat(result.getUser().metadata().get("pki_delegated_by_realm"), is("mockup_delegate_realm")); + } + + public void testAuthenticationDelegationFailure() throws Exception { + X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); + X509AuthenticationToken delegatedToken = X509AuthenticationToken.delegated(new X509Certificate[] { certificate }, + mock(Authentication.class)); + + UserRoleMapper roleMapper = buildRoleMapper(); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("xpack.security.authc.realms.pki.my_pki.truststore.secure_password", "testnode"); + Settings settings = Settings.builder() + .put(globalSettings) + .put("xpack.security.authc.realms.pki.my_pki.truststore.path", + getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks")) + .setSecureSettings(secureSettings) + .build(); + PkiRealm realmNoDelegation = buildRealm(roleMapper, settings); + assertRealmUsageStats(realmNoDelegation, true, false, true, false); + + AuthenticationResult result = authenticate(delegatedToken, realmNoDelegation); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); + assertThat(result.getUser(), is(nullValue())); + assertThat(result.getMessage(), containsString("Realm does not permit delegation for")); + } + public void testVerificationFailsUsingADifferentTruststore() throws Exception { X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); UserRoleMapper roleMapper = buildRoleMapper(); @@ -262,6 +365,7 @@ public class PkiRealmTests extends ESTestCase { .build(); ThreadContext threadContext = new ThreadContext(settings); PkiRealm realm = buildRealm(roleMapper, settings); + assertRealmUsageStats(realm, true, false, true, false); threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); @@ -374,6 +478,7 @@ public class PkiRealmTests extends ESTestCase { .build(); final UserRoleMapper roleMapper = buildRoleMapper(Collections.emptySet(), token.dn()); final PkiRealm pkiRealm = buildRealm(roleMapper, realmSettings, otherRealm); + assertRealmUsageStats(pkiRealm, false, true, true, false); AuthenticationResult result = authenticate(token, pkiRealm); assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); @@ -388,6 +493,50 @@ public class PkiRealmTests extends ESTestCase { assertThat(result.getUser(), sameInstance(lookupUser2)); } + public void testX509AuthenticationTokenOrdered() throws Exception { + X509Certificate[] mockCertChain = new X509Certificate[2]; + mockCertChain[0] = mock(X509Certificate.class); + when(mockCertChain[0].getIssuerX500Principal()).thenReturn(new X500Principal("CN=Test, OU=elasticsearch, O=org")); + mockCertChain[1] = mock(X509Certificate.class); + when(mockCertChain[1].getSubjectX500Principal()).thenReturn(new X500Principal("CN=Not Test, OU=elasticsearch, O=org")); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new X509AuthenticationToken(mockCertChain)); + assertThat(e.getMessage(), is("certificates chain array is not ordered")); + } + + private void assertRealmUsageStats(Realm realm, Boolean hasTruststore, Boolean hasAuthorizationRealms, + Boolean hasDefaultUsernamePattern, Boolean isAuthenticationDelegated) throws Exception { + final PlainActionFuture> future = new PlainActionFuture<>(); + realm.usageStats(future); + Map usage = future.get(); + assertThat(usage.get("has_truststore"), is(hasTruststore)); + assertThat(usage.get("has_authorization_realms"), is(hasAuthorizationRealms)); + assertThat(usage.get("has_default_username_pattern"), is(hasDefaultUsernamePattern)); + assertThat(usage.get("is_authentication_delegated"), is(isAuthenticationDelegated)); + } + + public void testX509AuthenticationTokenCaching() throws Exception { + X509Certificate[] mockCertChain = new X509Certificate[2]; + mockCertChain[0] = mock(X509Certificate.class); + when(mockCertChain[0].getSubjectX500Principal()).thenReturn(new X500Principal("CN=Test, OU=elasticsearch, O=org")); + when(mockCertChain[0].getIssuerX500Principal()).thenReturn(new X500Principal("CN=Test CA, OU=elasticsearch, O=org")); + when(mockCertChain[0].getEncoded()).thenReturn(randomByteArrayOfLength(2)); + mockCertChain[1] = mock(X509Certificate.class); + when(mockCertChain[1].getSubjectX500Principal()).thenReturn(new X500Principal("CN=Test CA, OU=elasticsearch, O=org")); + when(mockCertChain[1].getEncoded()).thenReturn(randomByteArrayOfLength(3)); + BytesKey cacheKey = PkiRealm.computeTokenFingerprint(new X509AuthenticationToken(mockCertChain)); + + BytesKey sameCacheKey = PkiRealm + .computeTokenFingerprint(new X509AuthenticationToken(new X509Certificate[] { mockCertChain[0], mockCertChain[1] })); + assertThat(cacheKey, is(sameCacheKey)); + + BytesKey cacheKeyClient = PkiRealm.computeTokenFingerprint(new X509AuthenticationToken(new X509Certificate[] { mockCertChain[0] })); + assertThat(cacheKey, is(not(cacheKeyClient))); + + BytesKey cacheKeyRoot = PkiRealm.computeTokenFingerprint(new X509AuthenticationToken(new X509Certificate[] { mockCertChain[1] })); + assertThat(cacheKey, is(not(cacheKeyRoot))); + assertThat(cacheKeyClient, is(not(cacheKeyRoot))); + } + static X509Certificate readCert(Path path) throws Exception { try (InputStream in = Files.newInputStream(path)) { CertificateFactory factory = CertificateFactory.getInstance("X.509"); diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/README.asciidoc b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/README.asciidoc new file mode 100644 index 00000000000..3230bdde7e2 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/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/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/bogus.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/bogus.crt new file mode 100644 index 00000000000..4b1bc66be0f --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/bogus.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/openssl_config.cnf b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/openssl_config.cnf new file mode 100644 index 00000000000..64ff556f352 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/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/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testClient.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testClient.crt new file mode 100644 index 00000000000..45efce91ef3 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/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/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testClient.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testClient.key new file mode 100644 index 00000000000..186e6f86745 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/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/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testIntermediateCA.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testIntermediateCA.crt new file mode 100644 index 00000000000..7d8781b8889 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/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/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testIntermediateCA.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testIntermediateCA.key new file mode 100644 index 00000000000..5147725f448 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/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/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt new file mode 100644 index 00000000000..50ba7a21727 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/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/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.key new file mode 100644 index 00000000000..148bbd52bd7 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/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/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml index 2e23a85b7e7..df1978f443f 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml @@ -15,5 +15,5 @@ setup: # This is fragile - it needs to be updated every time we add a new cluster/index privilege # I would much prefer we could just check that specific entries are in the array, but we don't have # an assertion for that - - length: { "cluster" : 28 } + - length: { "cluster" : 29 } - length: { "index" : 16 }