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");