From cb785770e821d4e8e42afaea13f56f2187adbff0 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 18 Jul 2019 14:17:47 +0300 Subject: [PATCH 01/14] Add delegation feature to PKI realm (#44106) Introduces the `allow_delegation` PKI Realm setting and the `X509AuthenticationToken.isDelegated` property and puts them both at work inside `TransportDelegatePkiAction` to implement the exchange of a certificate chain for an ES access token, aka PKI delegation. If `allow_delegation` is `true` (default `false`) on a PKI realm then it will authenticate `X509AuthenticationTokens` created by the `TransportDelegatePkiAction` which have the delegated property set. --- .../settings/security-settings.asciidoc | 8 ++ .../DelegatePkiAuthenticationRequest.java | 96 +++++++++++++++++++ .../DelegatePkiAuthenticationResponse.java | 67 +++++++++++++ .../security/authc/pki/PkiRealmSettings.java | 5 + .../xpack/core/ssl/CertParsingUtils.java | 17 ++++ ...DelegatePkiAuthenticationRequestTests.java | 92 ++++++++++++++++++ ...elegatePkiAuthenticationResponseTests.java | 32 +++++++ .../xpack/security/Security.java | 2 + ...nsportDelegatePkiAuthenticationAction.java | 74 ++++++++++++++ .../xpack/security/authc/pki/PkiRealm.java | 4 + .../authc/pki/X509AuthenticationToken.java | 14 +++ .../security/authc/RealmSettingsTests.java | 3 + .../pki/PkiAuthDelegationIntegTests.java | 84 ++++++++++++++++ .../security/authc/pki/PkiRealmTests.java | 57 ++++++++++- 14 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationResponse.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationResponseTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index ee48257565ff5..486dbfc125832 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -861,6 +861,14 @@ 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`. + [[ref-saml-settings]] [float] ===== SAML realm settings 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 0000000000000..6a02da76b5ca9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationRequest.java @@ -0,0 +1,96 @@ +/* + * 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.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +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.Arrays; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public final class DelegatePkiAuthenticationRequest extends ActionRequest { + + private X509Certificate[] certificates; + + public DelegatePkiAuthenticationRequest(X509Certificate[] certificates) { + this.certificates = certificates; + } + + public DelegatePkiAuthenticationRequest(StreamInput in) throws IOException { + this.readFrom(in); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (certificates == null) { + validationException = addValidationError("certificates chain array must not be null", validationException); + } else if (certificates.length == 0) { + validationException = addValidationError("certificates chain array must not be empty", validationException); + } else if (false == CertParsingUtils.isOrderedCertificateChain(certificates)) { + validationException = addValidationError("certificates chain array is not ordered", validationException); + } + return validationException; + } + + public X509Certificate[] getCertificates() { + return certificates; + } + + @Override + public void readFrom(StreamInput input) throws IOException { + super.readFrom(input); + try { + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + certificates = input.readArray(in -> { + try (ByteArrayInputStream bis = new ByteArrayInputStream(in.readByteArray())) { + return (X509Certificate) certificateFactory.generateCertificate(bis); + } catch (CertificateException e) { + throw new IOException(e); + } + }, X509Certificate[]::new); + } catch (CertificateException e) { + throw new IOException(e); + } + } + + @Override + public void writeTo(StreamOutput output) throws IOException { + super.writeTo(output); + output.writeArray((out, cert) -> { + try { + out.writeByteArray(cert.getEncoded()); + } catch (CertificateEncodingException e) { + throw new IOException(e); + } + }, certificates); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DelegatePkiAuthenticationRequest that = (DelegatePkiAuthenticationRequest) o; + return Arrays.equals(certificates, that.certificates); + } + + @Override + public int hashCode() { + return Arrays.hashCode(certificates); + } + +} 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 0000000000000..2a3d0f006a7ea --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationResponse.java @@ -0,0 +1,67 @@ +/* + * 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.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.unit.TimeValue; + +import java.io.IOException; +import java.util.Objects; + +public final class DelegatePkiAuthenticationResponse extends ActionResponse { + + private String tokenString; + private TimeValue expiresIn; + + DelegatePkiAuthenticationResponse() { } + + public DelegatePkiAuthenticationResponse(String tokenString, TimeValue expiresIn) { + this.tokenString = Objects.requireNonNull(tokenString); + this.expiresIn = Objects.requireNonNull(expiresIn); + } + + public DelegatePkiAuthenticationResponse(StreamInput input) throws IOException { + this.readFrom(input); + } + + public String getTokenString() { + return tokenString; + } + + public TimeValue getExpiresIn() { + return expiresIn; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(tokenString); + out.writeTimeValue(expiresIn); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + tokenString = in.readString(); + expiresIn = in.readTimeValue(); + } + + @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(tokenString, that.tokenString) && + Objects.equals(expiresIn, that.expiresIn); + } + + @Override + public int hashCode() { + return Objects.hash(tokenString, expiresIn); + } +} 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 cd153c9009ed6..e9d203a3897ec 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 static Set> getSettings() { 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/ssl/CertParsingUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertParsingUtils.java index 6526bd394c94c..e2348345a1fc1 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 @@ -17,6 +17,7 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509ExtendedTrustManager; + import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -275,4 +276,20 @@ public static X509ExtendedTrustManager trustManager(KeyStore keyStore, String al } 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(X509Certificate[] chain) { + for (int i = 1; i < chain.length; i++) { + X509Certificate cert = chain[i - 1]; + X509Certificate issuer = chain[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 0000000000000..e02887f9dd624 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationRequestTests.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.core.action; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationRequest; + +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 javax.security.auth.x500.X500Principal; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.arrayContaining; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DelegatePkiAuthenticationRequestTests extends ESTestCase { + + public void testRequestValidation() { + DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(((X509Certificate[]) null)); + ActionRequestValidationException ve = request.validate(); + assertNotNull(ve); + assertEquals(1, ve.validationErrors().size()); + assertThat(ve.validationErrors().get(0), is("certificates chain array must not be null")); + + request = new DelegatePkiAuthenticationRequest(new X509Certificate[0]); + ve = request.validate(); + assertNotNull(ve); + assertEquals(1, ve.validationErrors().size()); + assertThat(ve.validationErrors().get(0), is("certificates chain array must not be empty")); + + 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")); + request = new DelegatePkiAuthenticationRequest(mockCertChain); + ve = request.validate(); + assertNotNull(ve); + assertEquals(1, ve.validationErrors().size()); + assertThat(ve.validationErrors().get(0), is("certificates chain array is not ordered")); + + request = new DelegatePkiAuthenticationRequest(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 { + X509Certificate[] certificates = 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); + } + }); + 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.getCertificates(), arrayContaining(certificates)); + assertThat(request, is(serialized)); + assertThat(request.hashCode(), is(serialized.hashCode())); + } + } + } + + static X509Certificate readCert(Path path) throws Exception { + try (InputStream in = Files.newInputStream(path)) { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(in); + } + } +} 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 0000000000000..b86e46fe0e12b --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/DelegatePkiAuthenticationResponseTests.java @@ -0,0 +1,32 @@ +/* + * 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.test.ESTestCase; +import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse; + +import static org.hamcrest.Matchers.is; + +public class DelegatePkiAuthenticationResponseTests extends ESTestCase { + + public void testSerialization() throws Exception { + DelegatePkiAuthenticationResponse response = new DelegatePkiAuthenticationResponse(randomAlphaOfLengthBetween(0, 10), + TimeValue.parseTimeValue(randomTimeValue(), getClass().getSimpleName() + ".expiresIn")); + try (BytesStreamOutput output = new BytesStreamOutput()) { + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + DelegatePkiAuthenticationResponse serialized = new DelegatePkiAuthenticationResponse(input); + assertThat(response.getTokenString(), is(serialized.getTokenString())); + assertThat(response.getExpiresIn(), is(serialized.getExpiresIn())); + assertThat(response, is(serialized)); + } + } + } +} 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 fc443d14e01d7..61c1c26c0fdb1 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 @@ -136,6 +136,7 @@ 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; @@ -729,6 +730,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(CreateApiKeyAction.INSTANCE, TransportCreateApiKeyAction.class), new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class), new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class), + new ActionHandler<>(TransportDelegatePkiAuthenticationAction.TYPE, TransportDelegatePkiAuthenticationAction.class), usageAction, infoAction); } 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 0000000000000..0aa7b7c9348d2 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAuthenticationAction.java @@ -0,0 +1,74 @@ +/* + * 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.ActionType; +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.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.util.Map; + +public class TransportDelegatePkiAuthenticationAction + extends HandledTransportAction { + + private static final String ACTION_NAME = "cluster:admin/xpack/security/delegate_pki"; + public static final ActionType TYPE = new ActionType<>(ACTION_NAME, + DelegatePkiAuthenticationResponse::new); + 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(ACTION_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); + final X509AuthenticationToken x509DelegatedToken = new X509AuthenticationToken(request.getCertificates(), true); + logger.trace("Attempting to authenticate delegated x509Token [{}]", x509DelegatedToken); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + authenticationService.authenticate(ACTION_NAME, request, x509DelegatedToken, ActionListener.wrap(authentication -> { + assert authentication != null : "authentication should never be null at this point"; + tokenService.createOAuth2Tokens(authentication, delegateeAuthentication, Map.of(), 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 40d44503aef22..98839a4841c97 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 @@ -76,6 +76,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)); @@ -93,6 +94,7 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ .setMaximumWeight(config.getSetting(PkiRealmSettings.CACHE_MAX_USERS_SETTING)) .build(); this.delegatedRealms = null; + this.delegationEnabled = config.getSetting(PkiRealmSettings.DELEGATION_ENABLED_SETTING); } @Override @@ -151,6 +153,8 @@ public void authenticate(AuthenticationToken authToken, ActionListener future = new PlainActionFuture<>(); + client().execute(TransportDelegatePkiAuthenticationAction.TYPE, delegatePkiRequest, future); + String token = future.get().getTokenString(); + assertThat(token, is(notNullValue())); + RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", "Bearer " + token); + RequestOptions options = optionsBuilder.build(); + try(RestHighLevelClient restClient = new TestRestHighLevelClient()) { + AuthenticateResponse resp = restClient.security().authenticate(options); + 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")); + } + } + + static X509Certificate readCert(Path path) throws Exception { + 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 e5eb265979a87..e4d30796b1b55 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 @@ -79,7 +79,7 @@ public void testTokenSupport() { 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)); + assertThat(realm.supports(new X509AuthenticationToken(new X509Certificate[0], randomBoolean())), is(true)); } public void testExtractToken() throws Exception { @@ -92,6 +92,7 @@ public void testExtractToken() throws Exception { 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 { @@ -249,6 +250,50 @@ public void testVerificationUsingATruststore() throws Exception { assertThat(user.roles().length, is(0)); } + public void testAuthenticationDelegationSuccess() throws Exception { + X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); + X509AuthenticationToken delegatedToken = new X509AuthenticationToken(new X509Certificate[] { certificate }, true); + + 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) + .setSecureSettings(secureSettings) + .build(); + PkiRealm realmWithDelegation = buildRealm(roleMapper, settings); + 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)); + } + + public void testAuthenticationDelegationFailure() throws Exception { + X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); + X509AuthenticationToken delegatedToken = new X509AuthenticationToken(new X509Certificate[] { certificate }, true); + + 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); + + 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(); @@ -388,6 +433,16 @@ public void testDelegatedAuthorization() throws Exception { assertThat(result.getUser(), sameInstance(lookupUser2)); } + public void testX509AuthenticationToken() 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")); + } + static X509Certificate readCert(Path path) throws Exception { try (InputStream in = Files.newInputStream(path)) { CertificateFactory factory = CertificateFactory.getInstance("X.509"); From 7538b1af7b63ac4b9a2ac6fd953bd03fd5a3f1fd Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 23 Jul 2019 22:52:05 +0300 Subject: [PATCH 02/14] Streamable adjustments --- .../DelegatePkiAuthenticationRequest.java | 33 ++++++++----------- .../DelegatePkiAuthenticationResponse.java | 11 ++----- 2 files changed, 17 insertions(+), 27 deletions(-) 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 index 6a02da76b5ca9..1bdaf546a99b0 100644 --- 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 @@ -30,8 +30,20 @@ public DelegatePkiAuthenticationRequest(X509Certificate[] certificates) { this.certificates = certificates; } - public DelegatePkiAuthenticationRequest(StreamInput in) throws IOException { - this.readFrom(in); + public DelegatePkiAuthenticationRequest(StreamInput input) throws IOException { + super(input); + try { + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + certificates = input.readArray(in -> { + try (ByteArrayInputStream bis = new ByteArrayInputStream(in.readByteArray())) { + return (X509Certificate) certificateFactory.generateCertificate(bis); + } catch (CertificateException e) { + throw new IOException(e); + } + }, X509Certificate[]::new); + } catch (CertificateException e) { + throw new IOException(e); + } } @Override @@ -51,23 +63,6 @@ public X509Certificate[] getCertificates() { return certificates; } - @Override - public void readFrom(StreamInput input) throws IOException { - super.readFrom(input); - try { - final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - certificates = input.readArray(in -> { - try (ByteArrayInputStream bis = new ByteArrayInputStream(in.readByteArray())) { - return (X509Certificate) certificateFactory.generateCertificate(bis); - } catch (CertificateException e) { - throw new IOException(e); - } - }, X509Certificate[]::new); - } catch (CertificateException e) { - throw new IOException(e); - } - } - @Override public void writeTo(StreamOutput output) throws IOException { super.writeTo(output); 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 index 2a3d0f006a7ea..91d9e415e2bb0 100644 --- 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 @@ -27,7 +27,9 @@ public DelegatePkiAuthenticationResponse(String tokenString, TimeValue expiresIn } public DelegatePkiAuthenticationResponse(StreamInput input) throws IOException { - this.readFrom(input); + super(input); + tokenString = input.readString(); + expiresIn = input.readTimeValue(); } public String getTokenString() { @@ -44,13 +46,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeTimeValue(expiresIn); } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - tokenString = in.readString(); - expiresIn = in.readTimeValue(); - } - @Override public boolean equals(Object o) { if (this == o) return true; From a4bbba3e27d40da030712046ace7f2a7c5ce20c9 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Wed, 24 Jul 2019 17:07:29 +0300 Subject: [PATCH 03/14] Adjust PKI bootstrap check for delegation (#44767) There is currently a bootstrap check that validates TLS client authentication is enabled (required or optional) in at least one context (transport or http) when there is at least one PKI realm enabled. That is because clients cannot be authenticated by the PKI realm if there's no channel that supports mutually authn TLS. However, with the new proxied PKI authn scheme, this is no longer true; it is possible to authn via the proxied PKI scheme even if there is no mutually authn TLS channel configured. This commit relaxes the bootstrap check to only fail if the PKI realm is enabled and does not support delegation. --- .../xpack/security/PkiRealmBootstrapCheck.java | 8 ++++---- .../xpack/security/PkiRealmBootstrapCheckTests.java | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) 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 8f5012e1ecaf1..eb12d9ef68855 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/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/PkiRealmBootstrapCheckTests.java index ec3e0a5132dfd..10f9c7891f0c7 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 void testBootstrapCheckWithDisabledRealm() throws Exception { 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(); From a17ea2d5fd7eaacf082136713b8666bc8be7bd3b Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Fri, 26 Jul 2019 12:04:25 +0300 Subject: [PATCH 04/14] PKIRealm with delegation enabled requires token service (#44821) Enforces that the token service be enabled when a PKI realm `allows.delegation`, by throwing an `IllegalStateException` in the constructor. --- .../xpack/security/authc/pki/PkiRealm.java | 8 +++++++- .../xpack/security/authc/pki/PkiRealmTests.java | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) 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 98839a4841c97..74fa858567b5a 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.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.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; @@ -85,6 +87,11 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ // pkg private for testing PkiRealm(RealmConfig config, UserRoleMapper roleMapper) { super(config); + this.delegationEnabled = config.getSetting(PkiRealmSettings.DELEGATION_ENABLED_SETTING); + if (delegationEnabled && (false == TokenService.isTokenServiceEnabled(config.settings()))) { + throw new IllegalStateException("PKI realms with delegation enabled require that the token service be enabled as well (" + + XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey() + ")"); + } this.trustManager = trustManagers(config); this.principalPattern = config.getSetting(PkiRealmSettings.USERNAME_PATTERN_SETTING); this.roleMapper = roleMapper; @@ -94,7 +101,6 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ .setMaximumWeight(config.getSetting(PkiRealmSettings.CACHE_MAX_USERS_SETTING)) .build(); this.delegatedRealms = null; - this.delegationEnabled = config.getSetting(PkiRealmSettings.DELEGATION_ENABLED_SETTING); } @Override 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 e4d30796b1b55..1a980534a3570 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 @@ -250,6 +250,19 @@ public void testVerificationUsingATruststore() throws Exception { assertThat(user.roles().length, is(0)); } + public void testAuthenticationDelegationFailsWithoutTokenService() 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 that" + + " the token service be enabled as well (xpack.security.authc.token.enabled)")); + } + public void testAuthenticationDelegationSuccess() throws Exception { X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); X509AuthenticationToken delegatedToken = new X509AuthenticationToken(new X509Certificate[] { certificate }, true); @@ -262,6 +275,7 @@ public void testAuthenticationDelegationSuccess() throws Exception { .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); From 16496d62e4eeb59df9412428e1208e132a229638 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 1 Aug 2019 08:40:30 +0300 Subject: [PATCH 05/14] PKIRealm delegation unsupported without a truststore (#45011) In the usual scenario, where the PKIRealm works without delegation, a trust configuration is not required at the realm settings scope. In this case the client's certificate chain has been validated by the TLS channel (HTTP), and the realm does not enforce any other extra chain validations. In this case the client's certificate is considered authenticated by the realm. This no-op validation, without a trust configuration at the realm level, is incompatible with the delegation use-case. This commit adds a constructor check to the PKIRealm that will forbid toggling delegation.enabled without also setting a truststore. Otherwise, the delegation feature would not work at run-time (all API calls will be un-authorized). --- .../xpack/security/authc/pki/PkiRealm.java | 44 ++++++++++++++----- .../security/authc/pki/PkiRealmTests.java | 25 +++++++++-- 2 files changed, 55 insertions(+), 14 deletions(-) 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 74fa858567b5a..04deb3af2f8e0 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 @@ -44,6 +44,7 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -88,10 +89,6 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ PkiRealm(RealmConfig config, UserRoleMapper roleMapper) { super(config); this.delegationEnabled = config.getSetting(PkiRealmSettings.DELEGATION_ENABLED_SETTING); - if (delegationEnabled && (false == TokenService.isTokenServiceEnabled(config.settings()))) { - throw new IllegalStateException("PKI realms with delegation enabled require that the token service be enabled as well (" - + XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey() + ")"); - } this.trustManager = trustManagers(config); this.principalPattern = config.getSetting(PkiRealmSettings.USERNAME_PATTERN_SETTING); this.roleMapper = roleMapper; @@ -101,6 +98,7 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ .setMaximumWeight(config.getSetting(PkiRealmSettings.CACHE_MAX_USERS_SETTING)) .build(); this.delegatedRealms = null; + validateAuthenticationDelegationConfiguration(config); } @Override @@ -157,10 +155,10 @@ public void authenticate(AuthenticationToken authToken, ActionListener 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); + } + } + } + private static BytesKey computeFingerprint(X509Certificate certificate) throws CertificateEncodingException { MessageDigest digest = MessageDigests.sha256(); digest.update(certificate.getEncoded()); 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 1a980534a3570..c5e32cab75f20 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 @@ -250,7 +250,7 @@ public void testVerificationUsingATruststore() throws Exception { assertThat(user.roles().length, is(0)); } - public void testAuthenticationDelegationFailsWithoutTokenService() throws Exception { + public void testAuthenticationDelegationFailsWithoutTokenServiceAndTruststore() throws Exception { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); Settings settings = Settings.builder() .put(globalSettings) @@ -259,8 +259,27 @@ public void testAuthenticationDelegationFailsWithoutTokenService() throws Except 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 that" - + " the token service be enabled as well (xpack.security.authc.token.enabled)")); + 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 { From 6cf863f2fea51773e519921641b3654a3ecf3edf Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 1 Aug 2019 11:46:07 +0300 Subject: [PATCH 06/14] X509AuthnToken cache key change for proxied-PKI (#44810) The `X509AuthenticationToken` contains the certificate chain that the PKI realm validates and parses out the principal from. This validation is guarded by a cache for which the key computation, given an X509AuthenticationToken, does a sha256 of the encoding of the first certificate in the chain. Given that the same end entity certificate cannot be validated by different CAs with different certification paths (at least in practice) the cache key computation saves cycles by not evaluating the complete chain. This assumption does not hold anymore, in the case that the certificate chain is passed by the delegate-pki-authn API, because in that case the certificate chain given must be a complete chain, it must not be a prefix of another chain. The current implementation exposes inconsistencies in cases where only the client certificate (not the full path) are passed in the API request after (and not before) the complete path for that same certificate has been cached. --- .../xpack/security/authc/pki/PkiRealm.java | 10 ++++--- .../security/authc/pki/PkiRealmTests.java | 27 ++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) 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 04deb3af2f8e0..be2859cdc4538 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 @@ -147,9 +147,11 @@ public void authenticate(AuthenticationToken authToken, ActionListener) () -> new ParameterizedMessage("Using cached authentication for DN [{}], as principal [{}]", + token.dn(), user.principal())); if (delegatedRealms.hasDelegation()) { delegatedRealms.resolve(user.principal(), listener); } else { @@ -345,9 +347,11 @@ private void validateAuthenticationDelegationConfiguration(RealmConfig config) { } } - private static BytesKey computeFingerprint(X509Certificate certificate) throws CertificateEncodingException { + 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/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 c5e32cab75f20..62637b6408f03 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 @@ -25,6 +25,7 @@ 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; @@ -47,6 +48,7 @@ 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; @@ -466,7 +468,7 @@ public void testDelegatedAuthorization() throws Exception { assertThat(result.getUser(), sameInstance(lookupUser2)); } - public void testX509AuthenticationToken() throws Exception { + 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")); @@ -476,6 +478,29 @@ public void testX509AuthenticationToken() throws Exception { assertThat(e.getMessage(), is("certificates chain array is not ordered")); } + 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"); From f71cd90be568ee99ea750c6e3573eaeeceafb503 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 1 Aug 2019 13:34:10 +0300 Subject: [PATCH 07/14] Add REST handler for PKI delegation (#44561) Adds a REST action for the TransportDelegatePkiAuthenticationAction, the corresponding HL Rest client methods (with tests) and a certificate chain for tests. --- client/rest-high-level/build.gradle | 9 + .../elasticsearch/client/SecurityClient.java | 36 ++++ .../client/SecurityRequestConverters.java | 7 + .../DelegatePkiAuthenticationRequest.java | 107 ++++++++++ .../DelegatePkiAuthenticationResponse.java | 88 +++++++++ .../SecurityRequestConvertersTests.java | 16 ++ .../SecurityDocumentationIT.java | 88 +++++++++ ...DelegatePkiAuthenticationRequestTests.java | 107 ++++++++++ ...elegatePkiAuthenticationResponseTests.java | 53 +++++ .../security/delegate_pki/README.asciidoc | 35 ++++ .../security/delegate_pki/openssl_config.cnf | 185 ++++++++++++++++++ .../security/delegate_pki/testClient.crt | 21 ++ .../security/delegate_pki/testClient.key | 27 +++ .../delegate_pki/testIntermediateCA.crt | 24 +++ .../delegate_pki/testIntermediateCA.key | 27 +++ .../security/delegate_pki/testRootCA.crt | 24 +++ .../security/delegate_pki/testRootCA.key | 27 +++ .../delegate-pki-authentication.asciidoc | 62 ++++++ .../DelegatePkiAuthenticationRequest.java | 85 ++++++-- .../DelegatePkiAuthenticationResponse.java | 60 ++++-- .../xpack/core/ssl/CertParsingUtils.java | 8 +- ...DelegatePkiAuthenticationRequestTests.java | 80 +++++--- ...elegatePkiAuthenticationResponseTests.java | 28 ++- .../xpack/security/Security.java | 4 +- ...nsportDelegatePkiAuthenticationAction.java | 19 +- .../authc/pki/X509AuthenticationToken.java | 3 +- .../RestDelegatePkiAuthenticationAction.java | 80 ++++++++ .../pki/PkiAuthDelegationIntegTests.java | 98 ++++++++-- .../action/pki_delegation/README.asciidoc | 35 ++++ .../security/action/pki_delegation/bogus.crt | 23 +++ .../action/pki_delegation/openssl_config.cnf | 185 ++++++++++++++++++ .../action/pki_delegation/testClient.crt | 21 ++ .../action/pki_delegation/testClient.key | 27 +++ .../pki_delegation/testIntermediateCA.crt | 24 +++ .../pki_delegation/testIntermediateCA.key | 27 +++ .../action/pki_delegation/testRootCA.crt | 24 +++ .../action/pki_delegation/testRootCA.key | 27 +++ 37 files changed, 1709 insertions(+), 92 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponse.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/DelegatePkiAuthenticationResponseTests.java create mode 100644 client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/README.asciidoc create mode 100644 client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/openssl_config.cnf create mode 100644 client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testClient.crt create mode 100644 client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testClient.key create mode 100644 client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testIntermediateCA.crt create mode 100644 client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testIntermediateCA.key create mode 100644 client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.crt create mode 100644 client/rest-high-level/src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.key create mode 100644 docs/java-rest/high-level/security/delegate-pki-authentication.asciidoc create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/README.asciidoc create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/bogus.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/openssl_config.cnf create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testClient.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testClient.key create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testIntermediateCA.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testIntermediateCA.key create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/action/pki_delegation/testRootCA.key diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle index 9a485e96f9cc2..d4a3d4c0383da 100644 --- a/client/rest-high-level/build.gradle +++ b/client/rest-high-level/build.gradle @@ -83,6 +83,7 @@ processTestResources { from({ zipTree(configurations.restSpec.singleFile) }) { include 'rest-api-spec/api/**' } + from(project(':client:rest-high-level').file('src/test/resources')) } dependencyLicenses { @@ -115,6 +116,7 @@ if (isEclipse) { 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') @@ -132,6 +134,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'), @@ -139,4 +147,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 8c29cfaae54e1..a807b798f57b5 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.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 void invalidateApiKeyAsync(final InvalidateApiKeyRequest request, final R 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 18ecc2cea281a..4634ef23dfe83 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.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 @@ static Request createToken(CreateTokenRequest createTokenRequest) throws IOExcep 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 0000000000000..c67e692c14d44 --- /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 0000000000000..064a5a9a4e293 --- /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 4c99cb323969e..527d92518a1f4 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.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; @@ -58,6 +59,7 @@ 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; @@ -67,6 +69,8 @@ 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 { @@ -304,6 +308,18 @@ public void testCreateTokenWithClientCredentialsGrant() throws Exception { 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 9bbc3b2ea9072..e46d48f4ca05a 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.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.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 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; @@ -2196,4 +2204,84 @@ public void onFailure(Exception e) { 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 0000000000000..08c7055e8f4bb --- /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 0000000000000..2348eef4bd16d --- /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 0000000000000..3230bdde7e2ce --- /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 0000000000000..64ff556f35219 --- /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 0000000000000..45efce91ef33a --- /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 0000000000000..186e6f86745f1 --- /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 0000000000000..7d8781b888901 --- /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 0000000000000..5147725f4486a --- /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 0000000000000..50ba7a21727a6 --- /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 0000000000000..148bbd52bd76f --- /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/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 0000000000000..9cb667c24dc6e --- /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/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 index 1bdaf546a99b0..3e662f735fc20 100644 --- 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 @@ -8,8 +8,14 @@ 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; @@ -18,29 +24,59 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; -public final class DelegatePkiAuthenticationRequest extends ActionRequest { +/** + * 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); + } - private X509Certificate[] certificates; + public static DelegatePkiAuthenticationRequest fromXContent(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + + private List certificateChain; - public DelegatePkiAuthenticationRequest(X509Certificate[] certificates) { - this.certificates = certificates; + public DelegatePkiAuthenticationRequest(List certificateChain) { + this.certificateChain = List.copyOf(certificateChain); } public DelegatePkiAuthenticationRequest(StreamInput input) throws IOException { super(input); try { final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - certificates = input.readArray(in -> { + certificateChain = List.copyOf(input.readList(in -> { try (ByteArrayInputStream bis = new ByteArrayInputStream(in.readByteArray())) { return (X509Certificate) certificateFactory.generateCertificate(bis); } catch (CertificateException e) { throw new IOException(e); } - }, X509Certificate[]::new); + })); } catch (CertificateException e) { throw new IOException(e); } @@ -49,30 +85,28 @@ public DelegatePkiAuthenticationRequest(StreamInput input) throws IOException { @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; - if (certificates == null) { - validationException = addValidationError("certificates chain array must not be null", validationException); - } else if (certificates.length == 0) { - validationException = addValidationError("certificates chain array must not be empty", validationException); - } else if (false == CertParsingUtils.isOrderedCertificateChain(certificates)) { - validationException = addValidationError("certificates chain array is not ordered", validationException); + 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 X509Certificate[] getCertificates() { - return certificates; + public List getCertificateChain() { + return certificateChain; } @Override public void writeTo(StreamOutput output) throws IOException { super.writeTo(output); - output.writeArray((out, cert) -> { + output.writeCollection(certificateChain, (out, cert) -> { try { out.writeByteArray(cert.getEncoded()); } catch (CertificateEncodingException e) { throw new IOException(e); } - }, certificates); + }); } @Override @@ -80,12 +114,25 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DelegatePkiAuthenticationRequest that = (DelegatePkiAuthenticationRequest) o; - return Arrays.equals(certificates, that.certificates); + return Objects.equals(certificateChain, that.certificateChain); } @Override public int hashCode() { - return Arrays.hashCode(certificates); + 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 index 91d9e415e2bb0..4335d8f1cc667 100644 --- 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 @@ -7,33 +7,62 @@ 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; -public final class DelegatePkiAuthenticationResponse extends ActionResponse { +/** + * 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 tokenString; + private String accessToken; private TimeValue expiresIn; DelegatePkiAuthenticationResponse() { } - public DelegatePkiAuthenticationResponse(String tokenString, TimeValue expiresIn) { - this.tokenString = Objects.requireNonNull(tokenString); - this.expiresIn = Objects.requireNonNull(expiresIn); + 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); - tokenString = input.readString(); + accessToken = input.readString(); expiresIn = input.readTimeValue(); } - public String getTokenString() { - return tokenString; + public String getAccessToken() { + return accessToken; } public TimeValue getExpiresIn() { @@ -42,7 +71,7 @@ public TimeValue getExpiresIn() { @Override public void writeTo(StreamOutput out) throws IOException { - out.writeString(tokenString); + out.writeString(accessToken); out.writeTimeValue(expiresIn); } @@ -51,12 +80,21 @@ 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(tokenString, that.tokenString) && + return Objects.equals(accessToken, that.accessToken) && Objects.equals(expiresIn, that.expiresIn); } @Override public int hashCode() { - return Objects.hash(tokenString, expiresIn); + 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/ssl/CertParsingUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertParsingUtils.java index 01372f4596d31..39111ceb610b4 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 @@ -286,10 +286,10 @@ public static X509ExtendedTrustManager trustManager(KeyStore keyStore, String al * 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(X509Certificate[] chain) { - for (int i = 1; i < chain.length; i++) { - X509Certificate cert = chain[i - 1]; - X509Certificate issuer = chain[i]; + 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; } 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 index e02887f9dd624..a89f5d3705ca4 100644 --- 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 @@ -9,84 +9,104 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.test.ESTestCase; +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.hamcrest.Matchers.arrayContaining; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class DelegatePkiAuthenticationRequestTests extends ESTestCase { +public class DelegatePkiAuthenticationRequestTests extends AbstractXContentTestCase { public void testRequestValidation() { - DelegatePkiAuthenticationRequest request = new DelegatePkiAuthenticationRequest(((X509Certificate[]) null)); - ActionRequestValidationException ve = request.validate(); - assertNotNull(ve); - assertEquals(1, ve.validationErrors().size()); - assertThat(ve.validationErrors().get(0), is("certificates chain array must not be null")); + expectThrows(NullPointerException.class, () -> new DelegatePkiAuthenticationRequest((List) null)); - request = new DelegatePkiAuthenticationRequest(new X509Certificate[0]); - ve = request.validate(); + 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 array must not be empty")); + assertThat(ve.validationErrors().get(0), is("certificates chain must not be empty")); - 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")); + 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 array is not ordered")); + assertThat(ve.validationErrors().get(0), is("certificates chain must be an ordered chain")); - request = new DelegatePkiAuthenticationRequest(randomArray(1, 3, X509Certificate[]::new, () -> { + 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 { - X509Certificate[] certificates = 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); - } - }); + 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.getCertificates(), arrayContaining(certificates)); + assertThat(request.getCertificateChain(), is(certificates)); assertThat(request, is(serialized)); assertThat(request.hashCode(), is(serialized.hashCode())); } } } - static X509Certificate readCert(Path path) throws Exception { + 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 index b86e46fe0e12b..362068053b71c 100644 --- 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 @@ -9,24 +9,42 @@ import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.test.ESTestCase; +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 ESTestCase { +public class DelegatePkiAuthenticationResponseTests extends AbstractXContentTestCase { public void testSerialization() throws Exception { - DelegatePkiAuthenticationResponse response = new DelegatePkiAuthenticationResponse(randomAlphaOfLengthBetween(0, 10), - TimeValue.parseTimeValue(randomTimeValue(), getClass().getSimpleName() + ".expiresIn")); + DelegatePkiAuthenticationResponse response = createTestInstance(); try (BytesStreamOutput output = new BytesStreamOutput()) { response.writeTo(output); try (StreamInput input = output.bytes().streamInput()) { DelegatePkiAuthenticationResponse serialized = new DelegatePkiAuthenticationResponse(input); - assertThat(response.getTokenString(), is(serialized.getTokenString())); + 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/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 61c1c26c0fdb1..e5182acf33c54 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 @@ -198,6 +198,7 @@ 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; @@ -784,7 +785,8 @@ public List getRestHandlers(Settings settings, RestController restC 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 index 0aa7b7c9348d2..f0d8c20ab27f9 100644 --- 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 @@ -27,9 +27,23 @@ import org.elasticsearch.xpack.security.authc.TokenService; import org.elasticsearch.xpack.security.authc.pki.X509AuthenticationToken; +import java.security.cert.X509Certificate; import java.util.Map; -public class TransportDelegatePkiAuthenticationAction +/** + * 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 String ACTION_NAME = "cluster:admin/xpack/security/delegate_pki"; @@ -55,7 +69,8 @@ protected void doExecute(Task task, DelegatePkiAuthenticationRequest request, ActionListener listener) { final ThreadContext threadContext = threadPool.getThreadContext(); Authentication delegateeAuthentication = Authentication.getAuthentication(threadContext); - final X509AuthenticationToken x509DelegatedToken = new X509AuthenticationToken(request.getCertificates(), true); + final X509AuthenticationToken x509DelegatedToken = new X509AuthenticationToken( + request.getCertificateChain().toArray(new X509Certificate[0]), true); logger.trace("Attempting to authenticate delegated x509Token [{}]", x509DelegatedToken); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { authenticationService.authenticate(ACTION_NAME, request, x509DelegatedToken, ActionListener.wrap(authentication -> { 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 42797e4aec7e5..0d5021031bcba 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 @@ -9,6 +9,7 @@ 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 { @@ -24,7 +25,7 @@ public X509AuthenticationToken(X509Certificate[] certificates) { public X509AuthenticationToken(X509Certificate[] certificates, boolean isDelegated) { this.credentials = Objects.requireNonNull(certificates); - if (false == CertParsingUtils.isOrderedCertificateChain(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(); 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 0000000000000..6a58be94f4c32 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java @@ -0,0 +1,80 @@ +/* + * 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.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(TransportDelegatePkiAuthenticationAction.TYPE, 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/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthDelegationIntegTests.java index f3722b4dcac5a..4b5d815bcfa27 100644 --- 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 @@ -6,27 +6,33 @@ package org.elasticsearch.xpack.security.authc.pki; -import org.elasticsearch.action.support.PlainActionFuture; +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.AuthenticateResponse.RealmInfo; +import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest; +import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse; 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.SecuritySettingsSource; +import org.elasticsearch.test.SecuritySettingsSourceField; import org.elasticsearch.xpack.core.XPackSettings; -import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationRequest; -import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationResponse; -import org.elasticsearch.xpack.security.action.TransportDelegatePkiAuthenticationAction; 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.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.startsWith; public class PkiAuthDelegationIntegTests extends SecurityIntegTestCase { @@ -35,11 +41,24 @@ 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/transport/ssl/certs/simple/testclient.crt").toString()) + 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")) - .put("xpack.security.authc.realms.pki.pki1.delegation.enabled", true) + // 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(); } @@ -54,31 +73,72 @@ protected boolean addMockHttpTransport() { } public void testDelegatePki() throws Exception { - X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testclient.crt")); - DelegatePkiAuthenticationRequest delegatePkiRequest = new DelegatePkiAuthenticationRequest(new X509Certificate[] { certificate }); - PlainActionFuture future = new PlainActionFuture<>(); - client().execute(TransportDelegatePkiAuthenticationAction.TYPE, delegatePkiRequest, future); - String token = future.get().getTokenString(); - assertThat(token, is(notNullValue())); - RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder(); - optionsBuilder.addHeader("Authorization", "Bearer " + token); - RequestOptions options = optionsBuilder.build(); - try(RestHighLevelClient restClient = new TestRestHighLevelClient()) { - AuthenticateResponse resp = restClient.security().authenticate(options); + X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); + X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); + X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt"); + RequestOptions.Builder optionsBuilder; + try (RestHighLevelClient restClient = new TestRestHighLevelClient()) { + DelegatePkiAuthenticationRequest delegatePkiRequest; + // trust root is optional + if (randomBoolean()) { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA)); + } else { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA)); + } + optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME, + new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()))); + DelegatePkiAuthenticationResponse delegatePkiResponse = restClient.security().delegatePkiAuthentication(delegatePkiRequest, + optionsBuilder.build()); + optionsBuilder = RequestOptions.DEFAULT.toBuilder(); + optionsBuilder.addHeader("Authorization", "Bearer " + delegatePkiResponse.getAccessToken()); + 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("pki1")); + assertThat(authnRealm.getName(), is("pki3")); assertThat(authnRealm.getType(), is("pki")); } } - static X509Certificate readCert(Path path) throws Exception { + public void testDelegatePkiFailure() 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/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 0000000000000..3230bdde7e2ce --- /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 0000000000000..4b1bc66be0f74 --- /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 0000000000000..64ff556f35219 --- /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 0000000000000..45efce91ef33a --- /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 0000000000000..186e6f86745f1 --- /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 0000000000000..7d8781b888901 --- /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 0000000000000..5147725f4486a --- /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 0000000000000..50ba7a21727a6 --- /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 0000000000000..148bbd52bd76f --- /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----- From e5995606dd9b7290b5fa0f4c46941cd2ec5f2588 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 5 Aug 2019 16:15:52 +0300 Subject: [PATCH 08/14] Add pki_delegated_by_* to User metadata (#44873) This adds the `pki_delegated_by_user` and `pki_delegated_by_realm` fields to the `User#metadata` map. These fields contains the authentication of the proxy client (eg. `kibana_system`). The intention is for the `es-admin` to be able to write role mapping rules that distinguish between users authenticated directly or by delegation. --- ...nsportDelegatePkiAuthenticationAction.java | 8 ++- .../xpack/security/authc/pki/PkiRealm.java | 9 ++- .../authc/pki/X509AuthenticationToken.java | 20 ++++-- .../pki/PkiAuthDelegationIntegTests.java | 70 ++++++++++++++++++- .../security/authc/pki/PkiRealmTests.java | 22 +++++- 5 files changed, 116 insertions(+), 13 deletions(-) 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 index f0d8c20ab27f9..a97c6283f681c 100644 --- 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 @@ -69,8 +69,12 @@ protected void doExecute(Task task, DelegatePkiAuthenticationRequest request, ActionListener listener) { final ThreadContext threadContext = threadPool.getThreadContext(); Authentication delegateeAuthentication = Authentication.getAuthentication(threadContext); - final X509AuthenticationToken x509DelegatedToken = new X509AuthenticationToken( - request.getCertificateChain().toArray(new X509Certificate[0]), true); + 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(ACTION_NAME, request, x509DelegatedToken, ActionListener.wrap(authentication -> { 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 be2859cdc4538..ce5b2860090b4 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 @@ -198,7 +198,14 @@ public void authenticate(AuthenticationToken authToken, ActionListener listener) { - final Map metadata = Map.of("pki_dn", token.dn()); + final Map metadata; + if (token.isDelegated()) { + metadata = Map.of("pki_dn", token.dn(), + "pki_delegated_by_user", token.getDelegateeAuthentication().getUser().principal(), + "pki_delegated_by_realm", token.getDelegateeAuthentication().getAuthenticatedBy().getName()); + } else { + metadata = Map.of("pki_dn", token.dn()); + } final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(principal, token.dn(), Set.of(), metadata, config); roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { final User computedUser = new User(principal, roles.toArray(new String[roles.size()]), null, null, metadata, true); 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 0d5021031bcba..57e05c4314229 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,6 +5,7 @@ */ 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; @@ -16,21 +17,26 @@ public class X509AuthenticationToken implements AuthenticationToken { private final String dn; private final X509Certificate[] credentials; - private final boolean isDelegated; + private final Authentication delegateeAuthentication; private String principal; public X509AuthenticationToken(X509Certificate[] certificates) { - this(certificates, false); + this(certificates, null); } - public X509AuthenticationToken(X509Certificate[] certificates, boolean isDelegated) { + 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.isDelegated = isDelegated; + this.delegateeAuthentication = delegateeAuthentication; + } + + public static X509AuthenticationToken delegated(X509Certificate[] certificates, Authentication delegateeAuthentication) { + Objects.requireNonNull(delegateeAuthentication); + return new X509AuthenticationToken(certificates, delegateeAuthentication); } @Override @@ -57,6 +63,10 @@ public void clearCredentials() { } public boolean isDelegated() { - return isDelegated; + return delegateeAuthentication != null; + } + + public Authentication getDelegateeAuthentication() { + return delegateeAuthentication; } } 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 index 4b5d815bcfa27..97bc9991e45a8 100644 --- 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 @@ -11,7 +11,11 @@ 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.user.User; @@ -27,11 +31,14 @@ 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 { @@ -96,6 +103,14 @@ public void testDelegatePki() throws Exception { 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")); + // 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")); @@ -103,6 +118,59 @@ public void testDelegatePki() throws Exception { } } + public void testDelegatePkiWithRoleMapping() throws Exception { + X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); + X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); + X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt"); + DelegatePkiAuthenticationRequest delegatePkiRequest; + if (randomBoolean()) { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA)); + } else { + delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA)); + } + final RequestOptions testUserOptions = RequestOptions.DEFAULT.toBuilder() + .addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME, + new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()))) + .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 testDelegatePkiFailure() throws Exception { X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); @@ -117,14 +185,12 @@ public void testDelegatePkiFailure() throws Exception { () -> 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, 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 62637b6408f03..43a4c4c51c47e 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.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; @@ -81,7 +83,10 @@ public void testTokenSupport() { 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], randomBoolean())), 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 { @@ -286,7 +291,15 @@ public void testAuthenticationDelegationFailsWithoutTruststore() throws Exceptio public void testAuthenticationDelegationSuccess() throws Exception { X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); - X509AuthenticationToken delegatedToken = new X509AuthenticationToken(new X509Certificate[] { certificate }, true); + 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(); @@ -306,11 +319,14 @@ public void testAuthenticationDelegationSuccess() throws Exception { 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 = new X509AuthenticationToken(new X509Certificate[] { certificate }, true); + X509AuthenticationToken delegatedToken = X509AuthenticationToken.delegated(new X509Certificate[] { certificate }, + mock(Authentication.class)); UserRoleMapper roleMapper = buildRoleMapper(); MockSecureSettings secureSettings = new MockSecureSettings(); From 9277980c609f798015042bf3e629e11393b0dab7 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 5 Aug 2019 16:18:08 +0300 Subject: [PATCH 09/14] PKI realm usage stats includes delegation (#44871) This adds the following fields to the PKI realm's usage stats: `has_truststore`, `has_authorization_realms`, `has_default_username_pattern` and `is_authentication_delegated`. --- .../xpack/security/authc/pki/PkiRealm.java | 11 +++++++++ .../security/authc/pki/PkiRealmTests.java | 24 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) 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 ce5b2860090b4..ca6e4e09c2d28 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 @@ -332,6 +332,17 @@ public void expireAll() { } } + @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); 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 43a4c4c51c47e..132c22846cb6f 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 @@ -43,6 +43,7 @@ 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; @@ -76,11 +77,12 @@ public void setup() throws Exception { 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)); X509AuthenticationToken token = randomBoolean() @@ -122,7 +124,6 @@ private void assertSuccessfulAuthentication(Set roles) throws Exception 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())); @@ -207,6 +208,7 @@ public void testCustomUsernamePatternMatches() throws Exception { 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); @@ -226,6 +228,7 @@ public void testCustomUsernamePatternMismatchesAndNullToken() throws Exception { 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); @@ -246,6 +249,7 @@ public void testVerificationUsingATruststore() throws Exception { .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 }); @@ -313,6 +317,8 @@ public void testAuthenticationDelegationSuccess() throws Exception { .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())); @@ -338,6 +344,7 @@ public void testAuthenticationDelegationFailure() throws Exception { .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)); @@ -358,6 +365,7 @@ public void testVerificationFailsUsingADifferentTruststore() throws Exception { .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 }); @@ -470,6 +478,7 @@ public void testDelegatedAuthorization() throws Exception { .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)); @@ -494,6 +503,17 @@ public void testX509AuthenticationTokenOrdered() throws Exception { 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); From 1d6e31b8304411f4110cfc60b4a1939c9ffef880 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Wed, 7 Aug 2019 11:18:34 +0300 Subject: [PATCH 10/14] Cluster privilege for proxied PKI (#44862) This adds the delegate_pki cluster privilege that grants permissions to run the delegate-pki and invalidate-token APIs. The delegate_pki is not part of the manage, manage_security or any other cluster privilege apart from all. It also adds the privilege to the kibana_system role. --- .../security/get-builtin-privileges.asciidoc | 1 + .../DelegatePkiAuthenticationAction.java | 22 +++ .../authz/privilege/ClusterPrivilege.java | 14 +- .../authz/store/ReservedRolesStore.java | 2 +- .../authz/store/ReservedRolesStoreTests.java | 25 +++ .../xpack/security/Security.java | 3 +- ...nsportDelegatePkiAuthenticationAction.java | 31 ++-- .../RestDelegatePkiAuthenticationAction.java | 3 +- .../pki/PkiAuthDelegationIntegTests.java | 159 +++++++++++++++--- .../test/privileges/11_builtin.yml | 2 +- 10 files changed, 220 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiAuthenticationAction.java 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 b6afc70715a55..6ee26fa778d59 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/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 0000000000000..e8b6c26ff6749 --- /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/authz/privilege/ClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java index c3852d5ac63be..4d400d28c0439 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java @@ -16,12 +16,14 @@ 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; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.slm.action.GetSnapshotLifecycleAction; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Locale; @@ -32,12 +34,15 @@ import static java.util.Map.entry; import static org.elasticsearch.xpack.core.security.support.Automatons.minusAndMinimize; +import static org.elasticsearch.xpack.core.security.support.Automatons.unionAndMinimize; import static org.elasticsearch.xpack.core.security.support.Automatons.patterns; public final class ClusterPrivilege extends Privilege { // shared automatons - private static final Automaton MANAGE_SECURITY_AUTOMATON = patterns("cluster:admin/xpack/security/*"); + private static final Automaton DELEGATE_PKI_AUTOMATON = patterns(DelegatePkiAuthenticationAction.NAME); + private static final Automaton ALL_SECURITY_AUTOMATON = patterns("cluster:admin/xpack/security/*"); + private static final Automaton MANAGE_SECURITY_AUTOMATON = minusAndMinimize(ALL_SECURITY_AUTOMATON, DELEGATE_PKI_AUTOMATON); private static final Automaton MANAGE_SAML_AUTOMATON = patterns("cluster:admin/xpack/security/saml/*", InvalidateTokenAction.NAME, RefreshTokenAction.NAME); private static final Automaton MANAGE_OIDC_AUTOMATON = patterns("cluster:admin/xpack/security/oidc/*"); @@ -49,7 +54,7 @@ public final class ClusterPrivilege extends Privilege { private static final Automaton MONITOR_WATCHER_AUTOMATON = patterns("cluster:monitor/xpack/watcher/*"); private static final Automaton MONITOR_ROLLUP_AUTOMATON = patterns("cluster:monitor/xpack/rollup/*"); private static final Automaton ALL_CLUSTER_AUTOMATON = patterns("cluster:*", "indices:admin/template/*"); - private static final Automaton MANAGE_AUTOMATON = minusAndMinimize(ALL_CLUSTER_AUTOMATON, MANAGE_SECURITY_AUTOMATON); + private static final Automaton MANAGE_AUTOMATON = minusAndMinimize(ALL_CLUSTER_AUTOMATON, ALL_SECURITY_AUTOMATON); private static final Automaton MANAGE_ML_AUTOMATON = patterns("cluster:admin/xpack/ml/*", "cluster:monitor/xpack/ml/*"); private static final Automaton MANAGE_DATA_FRAME_AUTOMATON = patterns("cluster:admin/data_frame/*", "cluster:monitor/data_frame/*"); private static final Automaton MANAGE_WATCHER_AUTOMATON = patterns("cluster:admin/xpack/watcher/*", "cluster:monitor/xpack/watcher/*"); @@ -100,6 +105,8 @@ public final class ClusterPrivilege extends Privilege { public static final ClusterPrivilege READ_ILM = new ClusterPrivilege("read_ilm", READ_ILM_AUTOMATON); public static final ClusterPrivilege MANAGE_SLM = new ClusterPrivilege("manage_slm", MANAGE_SLM_AUTOMATON); public static final ClusterPrivilege READ_SLM = new ClusterPrivilege("read_slm", READ_SLM_AUTOMATON); + public static final ClusterPrivilege DELEGATE_PKI = new ClusterPrivilege("delegate_pki", + unionAndMinimize(Arrays.asList(DELEGATE_PKI_AUTOMATON, patterns(InvalidateTokenAction.NAME)))); public static final Predicate ACTION_MATCHER = ClusterPrivilege.ALL.predicate(); @@ -131,7 +138,8 @@ public final class ClusterPrivilege extends Privilege { entry("manage_ilm", MANAGE_ILM), entry("read_ilm", READ_ILM), entry("manage_slm", MANAGE_SLM), - entry("read_slm", READ_SLM)); + entry("read_slm", READ_SLM), + entry("delegate_pki", DELEGATE_PKI)); private static final ConcurrentHashMap, ClusterPrivilege> CACHE = new ConcurrentHashMap<>(); 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 494f92e1a4137..b1bfc69156148 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 @@ private static Map initializeReservedRoles() { .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/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 4433b9d3750e7..783500d8123fc 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.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 void testSnapshotUserRole() { 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 void testIngestAdminRole() { 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 void testKibanaSystemRole() { // 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 void testKibanaUserRole() { 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 void testMonitoringUserRole() { 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 void testRemoteMonitoringAgentRole() { 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 void testRemoteMonitoringCollectorRole() { 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 void testReportingUserRole() { 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 void testKibanaDashboardOnlyUserRole() { 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 void testSuperuserRole() { 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 void testLogstashSystemRole() { 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 void testBeatsAdminRole() { 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 void testBeatsSystemRole() { 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 void testAPMSystemRole() { 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 void testAPMUserRole() { 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 void testMachineLearningAdminRole() { 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 void testMachineLearningUserRole() { 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 void testDataFrameTransformsAdminRole() { 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 void testDataFrameTransformsUserRole() { 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 void testWatcherAdminRole() { 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 void testWatcherUserRole() { 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 void testLogstashAdminRole() { 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 void testCodeAdminRole() { 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 void testCodeUserRole() { 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/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index e5182acf33c54..e423cb61c359b 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.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; @@ -731,7 +732,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(CreateApiKeyAction.INSTANCE, TransportCreateApiKeyAction.class), new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class), new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class), - new ActionHandler<>(TransportDelegatePkiAuthenticationAction.TYPE, TransportDelegatePkiAuthenticationAction.class), + new ActionHandler<>(DelegatePkiAuthenticationAction.INSTANCE, TransportDelegatePkiAuthenticationAction.class), usageAction, infoAction); } 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 index a97c6283f681c..abfacc6cc2aa2 100644 --- 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 @@ -11,7 +11,6 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionType; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.inject.Inject; @@ -20,6 +19,7 @@ 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; @@ -46,9 +46,6 @@ public final class TransportDelegatePkiAuthenticationAction extends HandledTransportAction { - private static final String ACTION_NAME = "cluster:admin/xpack/security/delegate_pki"; - public static final ActionType TYPE = new ActionType<>(ACTION_NAME, - DelegatePkiAuthenticationResponse::new); private static final Logger logger = LogManager.getLogger(TransportDelegatePkiAuthenticationAction.class); private final ThreadPool threadPool; @@ -58,7 +55,7 @@ public final class TransportDelegatePkiAuthenticationAction @Inject public TransportDelegatePkiAuthenticationAction(ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters, AuthenticationService authenticationService, TokenService tokenService) { - super(ACTION_NAME, transportService, actionFilters, DelegatePkiAuthenticationRequest::new); + super(DelegatePkiAuthenticationAction.NAME, transportService, actionFilters, DelegatePkiAuthenticationRequest::new); this.threadPool = threadPool; this.authenticationService = authenticationService; this.tokenService = tokenService; @@ -77,17 +74,19 @@ protected void doExecute(Task task, DelegatePkiAuthenticationRequest request, .delegated(request.getCertificateChain().toArray(new X509Certificate[0]), delegateeAuthentication); logger.trace("Attempting to authenticate delegated x509Token [{}]", x509DelegatedToken); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { - authenticationService.authenticate(ACTION_NAME, request, x509DelegatedToken, ActionListener.wrap(authentication -> { - assert authentication != null : "authentication should never be null at this point"; - tokenService.createOAuth2Tokens(authentication, delegateeAuthentication, Map.of(), 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); - })); + authenticationService.authenticate(DelegatePkiAuthenticationAction.NAME, request, x509DelegatedToken, + ActionListener.wrap(authentication -> { + assert authentication != null : "authentication should never be null at this point"; + tokenService.createOAuth2Tokens(authentication, delegateeAuthentication, Map.of(), 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/rest/action/RestDelegatePkiAuthenticationAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAuthenticationAction.java index 6a58be94f4c32..c63d965a3b7c6 100644 --- 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 @@ -19,6 +19,7 @@ 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; @@ -61,7 +62,7 @@ protected Exception checkFeatureAvailable(RestRequest request) { protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { try (XContentParser parser = request.contentParser()) { final DelegatePkiAuthenticationRequest delegatePkiRequest = DelegatePkiAuthenticationRequest.fromXContent(parser); - return channel -> client.execute(TransportDelegatePkiAuthenticationAction.TYPE, delegatePkiRequest, + return channel -> client.execute(DelegatePkiAuthenticationAction.INSTANCE, delegatePkiRequest, new RestBuilderListener(channel) { @Override public RestResponse buildResponse(DelegatePkiAuthenticationResponse delegatePkiResponse, XContentBuilder 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 index 97bc9991e45a8..28beeed8cbcfa 100644 --- 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 @@ -18,13 +18,16 @@ 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.SecuritySettingsSource; import org.elasticsearch.test.SecuritySettingsSourceField; import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.test.SecuritySettingsSource; import java.io.InputStream; import java.nio.file.Files; @@ -53,7 +56,7 @@ public Settings nodeSettings(int nodeOrdinal) { .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 + // 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()) @@ -69,6 +72,44 @@ public Settings nodeSettings(int nodeOrdinal) { .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; @@ -79,26 +120,68 @@ protected boolean addMockHttpTransport() { return false; // enable http } - public void testDelegatePki() throws Exception { - X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); - X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); - X509Certificate rootCA = readCertForPkiDelegation("testRootCA.crt"); - RequestOptions.Builder optionsBuilder; + 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()) { - DelegatePkiAuthenticationRequest delegatePkiRequest; - // trust root is optional - if (randomBoolean()) { - delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA)); - } else { - delegatePkiRequest = new DelegatePkiAuthenticationRequest(Arrays.asList(clientCertificate, intermediateCA, rootCA)); + 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")); } - optionsBuilder = RequestOptions.DEFAULT.toBuilder(); - optionsBuilder.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME, - new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()))); + } + } + + 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 " + delegatePkiResponse.getAccessToken()); + optionsBuilder.addHeader("Authorization", "Bearer " + token); AuthenticateResponse resp = restClient.security().authenticate(optionsBuilder.build()); User user = resp.getUser(); assertThat(user, is(notNullValue())); @@ -106,7 +189,7 @@ public void testDelegatePki() throws Exception { 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_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 @@ -115,6 +198,43 @@ public void testDelegatePki() throws Exception { 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")); + } } } @@ -123,6 +243,7 @@ public void testDelegatePkiWithRoleMapping() throws Exception { 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 { @@ -171,7 +292,7 @@ public void testDelegatePkiWithRoleMapping() throws Exception { } } - public void testDelegatePkiFailure() throws Exception { + public void testIncorrectCertChain() throws Exception { X509Certificate clientCertificate = readCertForPkiDelegation("testClient.crt"); X509Certificate intermediateCA = readCertForPkiDelegation("testIntermediateCA.crt"); X509Certificate bogusCertificate = readCertForPkiDelegation("bogus.crt"); 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 2e23a85b7e737..df1978f443fc1 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 } From 0017a77d35e9f75975bae93b0b278d4b6a305b4c Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 15 Aug 2019 16:31:11 +0300 Subject: [PATCH 11/14] ClusterPrivilegeResolver merge conflict --- .../authz/privilege/ClusterPrivilegeResolver.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 755d76e76aa03..058af8b25ee47 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 @@ -18,6 +18,7 @@ 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 @@ */ public class ClusterPrivilegeResolver { // shared automatons - private static final Set MANAGE_SECURITY_PATTERN = Set.of("cluster:admin/xpack/security/*"); + private static final Set ALL_SECURITY_PATTERN = Set.of("cluster:admin/xpack/security/*"); private static final Set MANAGE_SAML_PATTERN = Set.of("cluster:admin/xpack/security/saml/*", InvalidateTokenAction.NAME, RefreshTokenAction.NAME); private static final Set MANAGE_OIDC_PATTERN = Set.of("cluster:admin/xpack/security/oidc/*"); @@ -75,8 +76,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); @@ -89,7 +89,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, + Set.of(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); @@ -102,6 +103,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", + Set.of(DelegatePkiAuthenticationAction.NAME, InvalidateTokenAction.NAME)); private static final Map VALUES = Stream.of( NONE, @@ -131,7 +134,8 @@ public class ClusterPrivilegeResolver { MANAGE_ILM, READ_ILM, MANAGE_SLM, - READ_SLM).collect(Collectors.toUnmodifiableMap(NamedClusterPrivilege::name, Function.identity())); + READ_SLM, + DELEGATE_PKI).collect(Collectors.toUnmodifiableMap(NamedClusterPrivilege::name, Function.identity())); /** * Resolves a {@link NamedClusterPrivilege} from a given name if it exists. From 57d435d31ccfbbd11fbc9c94585f68602112aa79 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Fri, 23 Aug 2019 15:06:40 +0300 Subject: [PATCH 12/14] Reference DOCS for proxied PKI (#45259) This commit contains the reference docs (xpack/docs) for the proxied PKI. Specifically, this contains PKI realm configuration instructions for delegation in x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc and docs for the newly introduced API in x-pack/docs/en/rest-api/security/delegate-pki-authentication.asciidoc Stack overview docs are in another repo so there will be a follow-up PR. But, I am thinking to add them after Kibana also has a sketch of their docs, so I can link to them from Stack overview. --- .../settings/security-settings.asciidoc | 7 +- x-pack/docs/build.gradle | 4 + x-pack/docs/en/rest-api/security.asciidoc | 2 + .../delegate-pki-authentication.asciidoc | 96 +++++++++ .../configuring-pki-realm.asciidoc | 188 ++++++++++++++---- .../authorization/mapping-roles.asciidoc | 5 + .../pki/PkiAuthDelegationIntegTests.java | 7 + 7 files changed, 268 insertions(+), 41 deletions(-) create mode 100644 x-pack/docs/en/rest-api/security/delegate-pki-authentication.asciidoc diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 58ec623282c72..bcde849402086 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -858,10 +858,13 @@ 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 +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`. +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] diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index fe07450bbc10e..df4a318e1ffd2 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -34,6 +34,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' @@ -53,6 +54,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 059dbc1e74716..d385ef29c4672 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 0000000000000..92d82f1c273e9 --- /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/security/authentication/configuring-pki-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc index 58144d0b23c1a..a3fc1a6c0b0a1 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 a99e385bd8c25..c4950d18f55c4 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 {ref}/anonymous-access.html[anonymous access] is enabled the roles +of the anonymous user are mapped 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/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 index 28beeed8cbcfa..fd84e71b91e0a 100644 --- 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 @@ -26,7 +26,9 @@ 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; @@ -120,6 +122,11 @@ 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"); From 2519a6421c6082fbd61acc9e904f5e518cd906e0 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 26 Aug 2019 16:01:36 +0300 Subject: [PATCH 13/14] Docs reference fix --- x-pack/docs/en/security/authorization/mapping-roles.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc index c4950d18f55c4..cf8911238a02c 100644 --- a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc @@ -28,8 +28,8 @@ 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 {ref}/anonymous-access.html[anonymous access] is enabled the roles -of the anonymous user are mapped to all the other users as well. +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. From 47a93b2f3d16c984236eb20b2d7ff267c84774e6 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Mon, 26 Aug 2019 17:15:34 +0300 Subject: [PATCH 14/14] Async returns cancellable --- .../main/java/org/elasticsearch/client/SecurityClient.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 a25396af648dd..be84d790f229e 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 @@ -1035,10 +1035,11 @@ public DelegatePkiAuthenticationResponse delegatePkiAuthentication(DelegatePkiAu * @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 + * @return cancellable that may be used to cancel the request */ - public void delegatePkiAuthenticationAsync(DelegatePkiAuthenticationRequest request, RequestOptions options, + public Cancellable delegatePkiAuthenticationAsync(DelegatePkiAuthenticationRequest request, RequestOptions options, ActionListener listener) { - restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::delegatePkiAuthentication, options, + return restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::delegatePkiAuthentication, options, DelegatePkiAuthenticationResponse::fromXContent, listener, emptySet()); } }