Browse Source

Improve errors when TLS files cannot be read (#44787)

This change improves the exception messages that are thrown when the
system cannot read TLS resources such as keystores, truststores,
certificates, keys or certificate-chains (CAs).

This change specifically handles:

- Files that do not exist
- Files that cannot be read due to file-system permissions
- Files that cannot be read due to the ES security-manager

Relates: #43079
Tim Vernum 6 years ago
parent
commit
724a0e40a3
23 changed files with 754 additions and 54 deletions
  1. 7 3
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertParsingUtils.java
  2. 29 0
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/KeyConfig.java
  3. 31 6
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PEMKeyConfig.java
  4. 16 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PEMTrustConfig.java
  5. 4 3
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java
  6. 20 13
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java
  7. 27 10
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfig.java
  8. 15 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreTrustConfig.java
  9. 51 5
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/TrustConfig.java
  10. 36 0
      x-pack/plugin/core/src/test/java/org/elasticsearch/test/TestMatchers.java
  11. 2 1
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/CertParsingUtilsTests.java
  12. 4 3
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java
  13. 11 6
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java
  14. 2 1
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfigTests.java
  15. 322 0
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageTests.java
  16. 106 0
      x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/README.txt
  17. 23 0
      x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/ca1.crt
  18. BIN
      x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/ca1.jks
  19. BIN
      x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/ca1.p12
  20. 21 0
      x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/cert1a.crt
  21. BIN
      x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/cert1a.jks
  22. 27 0
      x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/cert1a.key
  23. BIN
      x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/cert1a.p12

+ 7 - 3
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertParsingUtils.java

@@ -53,6 +53,10 @@ public class CertParsingUtils {
         return environment.configFile().resolve(path);
     }
 
+    static List<Path> resolvePaths(List<String> certPaths, Environment environment) {
+        return certPaths.stream().map(p -> environment.configFile().resolve(p)).collect(Collectors.toList());
+    }
+
     public static KeyStore readKeyStore(Path path, String type, char[] password)
             throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
         try (InputStream in = Files.newInputStream(path)) {
@@ -67,12 +71,12 @@ public class CertParsingUtils {
      * Reads the provided paths and parses them into {@link Certificate} objects
      *
      * @param certPaths   the paths to the PEM encoded certificates
-     * @param environment the environment to resolve files against. May be {@code null}
+     * @param environment the environment to resolve files against. May be not be {@code null}
      * @return an array of {@link Certificate} objects
      */
-    public static Certificate[] readCertificates(List<String> certPaths, @Nullable Environment environment)
+    public static Certificate[] readCertificates(List<String> certPaths, Environment environment)
             throws CertificateException, IOException {
-        final List<Path> resolvedPaths = certPaths.stream().map(p -> environment.configFile().resolve(p)).collect(Collectors.toList());
+        final List<Path> resolvedPaths = resolvePaths(certPaths, environment);
         return readCertificates(resolvedPaths);
     }
 

+ 29 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/KeyConfig.java

@@ -5,6 +5,7 @@
  */
 package org.elasticsearch.xpack.core.ssl;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.xpack.core.ssl.cert.CertificateInfo;
@@ -12,7 +13,10 @@ import org.elasticsearch.xpack.core.ssl.cert.CertificateInfo;
 import javax.net.ssl.X509ExtendedKeyManager;
 import javax.net.ssl.X509ExtendedTrustManager;
 
+import java.io.IOException;
+import java.nio.file.AccessDeniedException;
 import java.nio.file.Path;
+import java.security.AccessControlException;
 import java.security.PrivateKey;
 import java.util.Collection;
 import java.util.Collections;
@@ -64,6 +68,31 @@ abstract class KeyConfig extends TrustConfig {
 
     abstract X509ExtendedKeyManager createKeyManager(@Nullable Environment environment);
 
+    /**
+     * generate a new exception caused by a missing file, that is required for this key config
+     */
+    static ElasticsearchException missingKeyConfigFile(IOException cause, String fileType, Path path) {
+        return new ElasticsearchException(
+            "failed to initialize SSL KeyManager - " + fileType + " file [{}] does not exist", cause, path.toAbsolutePath());
+    }
+
+    /**
+     * generate a new exception caused by an unreadable file (i.e. file-system access denied), that is required for this key config
+     */
+    static ElasticsearchException unreadableKeyConfigFile(AccessDeniedException cause, String fileType, Path path) {
+        return new ElasticsearchException(
+            "failed to initialize SSL KeyManager - not permitted to read " + fileType + " file [{}]", cause, path.toAbsolutePath());
+    }
+
+    /**
+     * generate a new exception caused by a blocked file (i.e. security-manager access denied), that is required for this key config
+     */
+    static ElasticsearchException blockedKeyConfigFile(AccessControlException cause, Environment environment, String fileType, Path path) {
+        return new ElasticsearchException(
+            "failed to initialize SSL KeyManager - access to read {} file [{}] is blocked;" +
+                " SSL resources should be placed in the [{}] directory", cause, fileType, path, environment.configFile());
+    }
+
     abstract List<PrivateKey> privateKeys(@Nullable Environment environment);
 
 }

+ 31 - 6
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PEMKeyConfig.java

@@ -14,9 +14,13 @@ import org.elasticsearch.xpack.core.ssl.cert.CertificateInfo;
 import javax.net.ssl.X509ExtendedKeyManager;
 import javax.net.ssl.X509ExtendedTrustManager;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.security.AccessControlException;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
@@ -35,6 +39,9 @@ import java.util.Objects;
  */
 class PEMKeyConfig extends KeyConfig {
 
+    private static final String CERTIFICATE_FILE = "certificate";
+    private static final String KEY_FILE = "key";
+
     private final String keyPath;
     private final SecureString keyPassword;
     private final String certPath;
@@ -55,7 +62,7 @@ class PEMKeyConfig extends KeyConfig {
     @Override
     X509ExtendedKeyManager createKeyManager(@Nullable Environment environment) {
         try {
-            PrivateKey privateKey = readPrivateKey(CertParsingUtils.resolvePath(keyPath, environment), keyPassword);
+            PrivateKey privateKey = readPrivateKey(keyPath, keyPassword, environment);
             if (privateKey == null) {
                 throw new IllegalArgumentException("private key [" + keyPath + "] could not be loaded");
             }
@@ -63,12 +70,21 @@ class PEMKeyConfig extends KeyConfig {
 
             return CertParsingUtils.keyManager(certificateChain, privateKey, keyPassword.getChars());
         } catch (IOException | UnrecoverableKeyException | NoSuchAlgorithmException | CertificateException | KeyStoreException e) {
-            throw new ElasticsearchException("failed to initialize a KeyManagerFactory", e);
+            throw new ElasticsearchException("failed to initialize SSL KeyManagerFactory", e);
         }
     }
 
     private Certificate[] getCertificateChain(@Nullable Environment environment) throws CertificateException, IOException {
-        return CertParsingUtils.readCertificates(Collections.singletonList(certPath), environment);
+        final Path certificate = CertParsingUtils.resolvePath(certPath, environment);
+        try {
+            return CertParsingUtils.readCertificates(Collections.singletonList(certificate));
+        } catch (FileNotFoundException | NoSuchFileException fileException) {
+            throw missingKeyConfigFile(fileException, CERTIFICATE_FILE, certificate);
+        } catch (AccessDeniedException accessException) {
+            throw unreadableKeyConfigFile(accessException, CERTIFICATE_FILE, certificate);
+        } catch (AccessControlException securityException) {
+            throw blockedKeyConfigFile(securityException, environment, CERTIFICATE_FILE, certificate);
+        }
     }
 
     @Override
@@ -87,14 +103,23 @@ class PEMKeyConfig extends KeyConfig {
     @Override
     List<PrivateKey> privateKeys(@Nullable Environment environment) {
         try {
-            return Collections.singletonList(readPrivateKey(CertParsingUtils.resolvePath(keyPath, environment), keyPassword));
+            return Collections.singletonList(readPrivateKey(keyPath, keyPassword, environment));
         } catch (IOException e) {
             throw new UncheckedIOException("failed to read key", e);
         }
     }
 
-    private static PrivateKey readPrivateKey(Path keyPath, SecureString keyPassword) throws IOException {
-        return PemUtils.readPrivateKey(keyPath, keyPassword::getChars);
+    private static PrivateKey readPrivateKey(String keyPath, SecureString keyPassword, Environment environment) throws IOException {
+        final Path key = CertParsingUtils.resolvePath(keyPath, environment);
+        try {
+            return PemUtils.readPrivateKey(key, keyPassword::getChars);
+        } catch (FileNotFoundException | NoSuchFileException fileException) {
+            throw missingKeyConfigFile(fileException, KEY_FILE, key);
+        } catch (AccessDeniedException accessException) {
+            throw unreadableKeyConfigFile(accessException, KEY_FILE, key);
+        } catch (AccessControlException securityException) {
+            throw blockedKeyConfigFile(securityException, environment, KEY_FILE, key);
+        }
     }
 
     @Override

+ 16 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PEMTrustConfig.java

@@ -14,7 +14,10 @@ import org.elasticsearch.xpack.core.ssl.cert.CertificateInfo;
 import javax.net.ssl.X509ExtendedTrustManager;
 
 import java.io.IOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.security.AccessControlException;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
@@ -29,10 +32,13 @@ import java.util.Objects;
  */
 class PEMTrustConfig extends TrustConfig {
 
+    private static final String CA_FILE = "certificate_authorities";
+    
     private final List<String> caPaths;
 
     /**
      * Create a new trust configuration that is built from the certificate files
+     *
      * @param caPaths the paths to the certificate files to trust
      */
     PEMTrustConfig(List<String> caPaths) {
@@ -44,8 +50,17 @@ class PEMTrustConfig extends TrustConfig {
         try {
             Certificate[] certificates = CertParsingUtils.readCertificates(caPaths, environment);
             return CertParsingUtils.trustManager(certificates);
+        } catch (NoSuchFileException noSuchFileException) {
+            final Path missingPath = CertParsingUtils.resolvePath(noSuchFileException.getFile(), environment);
+            throw missingTrustConfigFile(noSuchFileException, CA_FILE, missingPath);
+        } catch (AccessDeniedException accessDeniedException) {
+            final Path missingPath = CertParsingUtils.resolvePath(accessDeniedException.getFile(), environment);
+            throw unreadableTrustConfigFile(accessDeniedException, CA_FILE, missingPath);
+        } catch (AccessControlException accessControlException) {
+            final List<Path> paths = CertParsingUtils.resolvePaths(caPaths, environment);
+            throw blockedTrustConfigFile(accessControlException, environment, CA_FILE, paths);
         } catch (Exception e) {
-            throw new ElasticsearchException("failed to initialize a TrustManagerFactory", e);
+            throw new ElasticsearchException("failed to initialize SSL TrustManager", e);
         }
     }
 

+ 4 - 3
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java

@@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.security.GeneralSecurityException;
+import java.security.KeyException;
 import java.security.KeyFactory;
 import java.security.KeyPairGenerator;
 import java.security.MessageDigest;
@@ -72,7 +73,7 @@ public class PemUtils {
      * @param passwordSupplier A password supplier for the potentially encrypted (password protected) key
      * @return a private key from the contents of the file
      */
-    public static PrivateKey readPrivateKey(Path keyPath, Supplier<char[]> passwordSupplier) {
+    public static PrivateKey readPrivateKey(Path keyPath, Supplier<char[]> passwordSupplier) throws IOException {
         try (BufferedReader bReader = Files.newBufferedReader(keyPath, StandardCharsets.UTF_8)) {
             String line = bReader.readLine();
             while (null != line && line.startsWith(HEADER) == false){
@@ -103,7 +104,7 @@ public class PemUtils {
                 throw new IllegalStateException("Error parsing Private Key from: " + keyPath.toString() + ". File did not contain a " +
                         "supported key format");
             }
-        } catch (IOException | GeneralSecurityException e) {
+        } catch (GeneralSecurityException e) {
             throw new IllegalStateException("Error parsing Private Key from: " + keyPath.toString(), e);
         }
     }
@@ -176,7 +177,7 @@ public class PemUtils {
             line = bReader.readLine();
         }
         if (null == line || PKCS8_FOOTER.equals(line.trim()) == false) {
-            throw new IOException("Malformed PEM file, PEM footer is invalid or missing");
+            throw new KeyException("Malformed PEM file, PEM footer is invalid or missing");
         }
         byte[] keyBytes = Base64.getDecoder().decode(sb.toString());
         String keyAlgo = getKeyAlgorithmIdentifier(keyBytes);

+ 20 - 13
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java

@@ -11,6 +11,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.ElasticsearchSecurityException;
 import org.elasticsearch.common.CheckedSupplier;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
@@ -416,27 +417,33 @@ public class SSLService {
         sslSettingsMap.putAll(getRealmsSSLSettings(settings));
         sslSettingsMap.putAll(getMonitoringExporterSettings(settings));
 
-        sslSettingsMap.forEach((key, sslSettings) -> {
-            final SSLConfiguration configuration = new SSLConfiguration(sslSettings);
-            storeSslConfiguration(key, configuration);
-            sslContextHolders.computeIfAbsent(configuration, this::createSslContext);
-        });
+        sslSettingsMap.forEach((key, sslSettings) -> loadConfiguration(key, sslSettings, sslContextHolders));
 
         final Settings transportSSLSettings = settings.getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX);
-        final SSLConfiguration transportSSLConfiguration = new SSLConfiguration(transportSSLSettings);
+        final SSLConfiguration transportSSLConfiguration =
+            loadConfiguration(XPackSettings.TRANSPORT_SSL_PREFIX, transportSSLSettings, sslContextHolders);
         this.transportSSLConfiguration.set(transportSSLConfiguration);
-        storeSslConfiguration(XPackSettings.TRANSPORT_SSL_PREFIX, transportSSLConfiguration);
         Map<String, Settings> profileSettings = getTransportProfileSSLSettings(settings);
-        sslContextHolders.computeIfAbsent(transportSSLConfiguration, this::createSslContext);
-        profileSettings.forEach((key, profileSetting) -> {
-            final SSLConfiguration configuration = new SSLConfiguration(profileSetting);
-            storeSslConfiguration(key, configuration);
-            sslContextHolders.computeIfAbsent(configuration, this::createSslContext);
-        });
+        profileSettings.forEach((key, profileSetting) -> loadConfiguration(key, profileSetting, sslContextHolders));
 
         return Collections.unmodifiableMap(sslContextHolders);
     }
 
+    private SSLConfiguration loadConfiguration(String key, Settings settings, Map<SSLConfiguration, SSLContextHolder> contextHolders) {
+        if (key.endsWith(".")) {
+            // Drop trailing '.' so that any exception messages are consistent
+            key = key.substring(0, key.length() - 1);
+        }
+        try {
+            final SSLConfiguration configuration = new SSLConfiguration(settings);
+            storeSslConfiguration(key, configuration);
+            contextHolders.computeIfAbsent(configuration, this::createSslContext);
+            return configuration;
+        } catch (Exception e) {
+            throw new ElasticsearchSecurityException("failed to load SSL configuration [{}]", e, key);
+        }
+    }
+
     private void storeSslConfiguration(String key, SSLConfiguration configuration) {
         if (key.endsWith(".")) {
             key = key.substring(0, key.length() - 1);

+ 27 - 10
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfig.java

@@ -14,17 +14,18 @@ import org.elasticsearch.xpack.core.ssl.cert.CertificateInfo;
 import javax.net.ssl.X509ExtendedKeyManager;
 import javax.net.ssl.X509ExtendedTrustManager;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.security.AccessControlException;
 import java.security.GeneralSecurityException;
 import java.security.Key;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
-import java.security.UnrecoverableKeyException;
 import java.security.cert.Certificate;
-import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -38,6 +39,8 @@ import java.util.Objects;
  */
 class StoreKeyConfig extends KeyConfig {
 
+    private static final String KEYSTORE_FILE = "keystore";
+
     final String keyStorePath;
     final String keyStoreType;
     final SecureString keyStorePassword;
@@ -68,28 +71,42 @@ class StoreKeyConfig extends KeyConfig {
 
     @Override
     X509ExtendedKeyManager createKeyManager(@Nullable Environment environment) {
+        Path ksPath = keyStorePath == null ? null : CertParsingUtils.resolvePath(keyStorePath, environment);
         try {
-            KeyStore ks = getStore(environment, keyStorePath, keyStoreType, keyStorePassword);
+            KeyStore ks = getStore(ksPath, keyStoreType, keyStorePassword);
             checkKeyStore(ks);
             return CertParsingUtils.keyManager(ks, keyPassword.getChars(), keyStoreAlgorithm);
-        } catch (IOException | CertificateException | NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException e) {
-            throw new ElasticsearchException("failed to initialize a KeyManagerFactory", e);
+        } catch (FileNotFoundException | NoSuchFileException e) {
+            throw missingKeyConfigFile(e, KEYSTORE_FILE, ksPath);
+        } catch (AccessDeniedException e) {
+            throw unreadableKeyConfigFile(e, KEYSTORE_FILE, ksPath);
+        } catch (AccessControlException e) {
+            throw blockedKeyConfigFile(e, environment, KEYSTORE_FILE, ksPath);
+        } catch (IOException | GeneralSecurityException e) {
+            throw new ElasticsearchException("failed to initialize SSL KeyManager", e);
         }
     }
 
     @Override
     X509ExtendedTrustManager createTrustManager(@Nullable Environment environment) {
+        final Path ksPath = CertParsingUtils.resolvePath(keyStorePath, environment);
         try {
-            KeyStore ks = getStore(environment, keyStorePath, keyStoreType, keyStorePassword);
+            KeyStore ks = getStore(ksPath, keyStoreType, keyStorePassword);
             return CertParsingUtils.trustManager(ks, trustStoreAlgorithm);
-        } catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException e) {
-            throw new ElasticsearchException("failed to initialize a TrustManagerFactory", e);
+        } catch (FileNotFoundException | NoSuchFileException e) {
+            throw missingTrustConfigFile(e, KEYSTORE_FILE, ksPath);
+        } catch (AccessDeniedException e) {
+            throw missingTrustConfigFile(e, KEYSTORE_FILE, ksPath);
+        } catch (AccessControlException e) {
+            throw blockedTrustConfigFile(e, environment, KEYSTORE_FILE, List.of(ksPath));
+        } catch (IOException | GeneralSecurityException e) {
+            throw new ElasticsearchException("failed to initialize SSL TrustManager", e);
         }
     }
 
     @Override
     Collection<CertificateInfo> certificates(Environment environment) throws GeneralSecurityException, IOException {
-        final KeyStore trustStore = getStore(environment, keyStorePath, keyStoreType, keyStorePassword);
+        final KeyStore trustStore = getStore(CertParsingUtils.resolvePath(keyStorePath, environment), keyStoreType, keyStorePassword);
         final List<CertificateInfo> certificates = new ArrayList<>();
         final Enumeration<String> aliases = trustStore.aliases();
         while (aliases.hasMoreElements()) {

+ 15 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreTrustConfig.java

@@ -13,8 +13,12 @@ import org.elasticsearch.xpack.core.ssl.cert.CertificateInfo;
 
 import javax.net.ssl.X509ExtendedTrustManager;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.security.AccessControlException;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
 import java.security.cert.Certificate;
@@ -31,6 +35,8 @@ import java.util.Objects;
  */
 class StoreTrustConfig extends TrustConfig {
 
+    private static final String TRUSTSTORE_FILE = "truststore";
+
     final String trustStorePath;
     final String trustStoreType;
     final SecureString trustStorePassword;
@@ -54,11 +60,18 @@ class StoreTrustConfig extends TrustConfig {
 
     @Override
     X509ExtendedTrustManager createTrustManager(@Nullable Environment environment) {
+        final Path storePath = CertParsingUtils.resolvePath(trustStorePath, environment);
         try {
-            KeyStore trustStore = getStore(environment, trustStorePath, trustStoreType, trustStorePassword);
+            KeyStore trustStore = getStore(storePath, trustStoreType, trustStorePassword);
             return CertParsingUtils.trustManager(trustStore, trustStoreAlgorithm);
+        } catch (FileNotFoundException | NoSuchFileException e) {
+            throw missingTrustConfigFile(e, TRUSTSTORE_FILE, storePath);
+        } catch (AccessDeniedException  e) {
+            throw unreadableTrustConfigFile(e, TRUSTSTORE_FILE, storePath);
+        } catch (AccessControlException e) {
+            throw blockedTrustConfigFile(e, environment, TRUSTSTORE_FILE, List.of(storePath));
         } catch (Exception e) {
-            throw new ElasticsearchException("failed to initialize a TrustManagerFactory", e);
+            throw new ElasticsearchException("failed to initialize SSL TrustManager", e);
         }
     }
 

+ 51 - 5
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/TrustConfig.java

@@ -12,11 +12,12 @@ import org.elasticsearch.env.Environment;
 import org.elasticsearch.xpack.core.ssl.cert.CertificateInfo;
 
 import javax.net.ssl.X509ExtendedTrustManager;
-
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.AccessDeniedException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.AccessControlException;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
@@ -65,12 +66,20 @@ abstract class TrustConfig {
      */
     public abstract int hashCode();
 
+    /**
+     * @deprecated Use {@link #getStore(Path, String, SecureString)} instead
+     */
+    @Deprecated
+    KeyStore getStore(Environment environment, @Nullable String storePath, String storeType, SecureString storePassword)
+        throws GeneralSecurityException, IOException {
+        return getStore(CertParsingUtils.resolvePath(storePath, environment), storeType, storePassword);
+    }
+
     /**
      * Loads and returns the appropriate {@link KeyStore} for the given configuration. The KeyStore can be backed by a file
      * in any format that the Security Provider might support, or a cryptographic software or hardware token in the case
      * of a PKCS#11 Provider.
      *
-     * @param environment   the environment to resolve files against
      * @param storePath     the path to the {@link KeyStore} to load, or null if a PKCS11 token is configured as the keystore/truststore
      *                      of the JVM
      * @param storeType     the type of the {@link KeyStore}
@@ -81,10 +90,9 @@ abstract class TrustConfig {
      * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
      * @throws IOException              if there is an I/O issue with the KeyStore data or the password is incorrect
      */
-    KeyStore getStore(Environment environment, @Nullable String storePath, String storeType, SecureString storePassword)
-        throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
+    KeyStore getStore(Path storePath, String storeType, SecureString storePassword) throws IOException, GeneralSecurityException {
         if (null != storePath) {
-            try (InputStream in = Files.newInputStream(CertParsingUtils.resolvePath(storePath, environment))) {
+            try (InputStream in = Files.newInputStream(storePath)) {
                 KeyStore ks = KeyStore.getInstance(storeType);
                 ks.load(in, storePassword.getChars());
                 return ks;
@@ -97,6 +105,44 @@ abstract class TrustConfig {
         throw new IllegalArgumentException("keystore.path or truststore.path can only be empty when using a PKCS#11 token");
     }
 
+    /**
+     * generate a new exception caused by a missing file, that is required for this trust config
+     */
+    protected ElasticsearchException missingTrustConfigFile(IOException cause, String fileType, Path path) {
+        return new ElasticsearchException(
+            "failed to initialize SSL TrustManager - " + fileType + " file [{}] does not exist", cause, path.toAbsolutePath());
+    }
+
+    /**
+     * generate a new exception caused by an unreadable file (i.e. file-system access denied), that is required for this trust config
+     */
+    protected ElasticsearchException unreadableTrustConfigFile(AccessDeniedException cause, String fileType, Path path) {
+        return new ElasticsearchException(
+            "failed to initialize SSL TrustManager - not permitted to read " + fileType + " file [{}]", cause, path.toAbsolutePath());
+    }
+
+    /**
+     * generate a new exception caused by a blocked file (i.e. security-manager access denied), that is required for this trust config
+     * @param paths A list of possible files. Depending on the context, it may not be possible to know exactly which file caused the
+     *              exception, so this method accepts multiple paths.
+     */
+    protected ElasticsearchException blockedTrustConfigFile(AccessControlException cause, Environment environment,
+                                                            String fileType, List<Path> paths) {
+        if (paths.size() == 1) {
+            return new ElasticsearchException(
+                "failed to initialize SSL TrustManager - access to read {} file [{}] is blocked;" +
+                    " SSL resources should be placed in the [{}] directory",
+                cause, fileType, paths.get(0).toAbsolutePath(), environment.configFile());
+        } else {
+            final String pathString = paths.stream().map(Path::toAbsolutePath).map(Path::toString).collect(Collectors.joining(", "));
+            return new ElasticsearchException(
+                "failed to initialize SSL TrustManager - access to read one or more of the {} files [{}] is blocked;" +
+                    " SSL resources should be placed in the [{}] directory",
+                cause, fileType, pathString, environment.configFile());
+        }
+    }
+
+
     /**
      * A trust configuration that is a combination of a trust configuration with the default JDK trust configuration. This trust
      * configuration returns a trust manager verifies certificates against both the default JDK trusted configurations and the specific

+ 36 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/test/TestMatchers.java

@@ -5,7 +5,10 @@
  */
 package org.elasticsearch.test;
 
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.CoreMatchers;
 import org.hamcrest.CustomMatcher;
+import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.Matchers;
 
@@ -26,6 +29,39 @@ public class TestMatchers extends Matchers {
         };
     }
 
+    public static Matcher<Throwable> throwableWithMessage(String message) {
+        return throwableWithMessage(CoreMatchers.equalTo(message));
+    }
+
+    public static Matcher<Throwable> throwableWithMessage(Matcher<String> messageMatcher) {
+        return new BaseMatcher<>() {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("a throwable with message of ").appendDescriptionOf(messageMatcher);
+            }
+
+            @Override
+            public boolean matches(Object actual) {
+                if (actual instanceof Throwable) {
+                    final Throwable throwable = (Throwable) actual;
+                    return messageMatcher.matches(throwable.getMessage());
+                } else {
+                    return false;
+                }
+            }
+
+            @Override
+            public void describeMismatch(Object item, Description description) {
+                super.describeMismatch(item, description);
+                if (item instanceof Throwable) {
+                    Throwable e = (Throwable) item;
+                    final StackTraceElement at = e.getStackTrace()[0];
+                    description.appendText(" at ").appendText(at.toString());
+                }
+            }
+        };
+    }
+
     public static <T> Matcher<Predicate<T>> predicateMatches(T value) {
         return new CustomMatcher<Predicate<T>>("Matches " + value) {
             @Override

+ 2 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/CertParsingUtilsTests.java

@@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.ssl;
 
 import org.elasticsearch.test.ESTestCase;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -86,7 +87,7 @@ public class CertParsingUtilsTests extends ESTestCase {
         assertEquals("EC", cert.getPublicKey().getAlgorithm());
     }
 
-    private void verifyPrime256v1ECKey(Path keyPath) {
+    private void verifyPrime256v1ECKey(Path keyPath) throws IOException {
         PrivateKey privateKey = PemUtils.readPrivateKey(keyPath, () -> null);
         assertEquals("EC", privateKey.getAlgorithm());
         assertThat(privateKey, instanceOf(ECPrivateKey.class));

+ 4 - 3
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java

@@ -69,6 +69,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 
+import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.sameInstance;
 
@@ -352,7 +353,7 @@ public class SSLConfigurationReloaderTests extends ESTestCase {
 
         latch.await();
         assertNotNull(exceptionRef.get());
-        assertThat(exceptionRef.get().getMessage(), containsString("failed to initialize a KeyManagerFactory"));
+        assertThat(exceptionRef.get(), throwableWithMessage(containsString("failed to initialize SSL KeyManager")));
         assertThat(sslService.sslContextHolder(config).sslContext(), sameInstance(context));
     }
 
@@ -450,7 +451,7 @@ public class SSLConfigurationReloaderTests extends ESTestCase {
 
         latch.await();
         assertNotNull(exceptionRef.get());
-        assertThat(exceptionRef.get().getMessage(), containsString("failed to initialize a TrustManagerFactory"));
+        assertThat(exceptionRef.get(), throwableWithMessage(containsString("failed to initialize SSL TrustManager")));
         assertThat(sslService.sslContextHolder(config).sslContext(), sameInstance(context));
     }
 
@@ -496,7 +497,7 @@ public class SSLConfigurationReloaderTests extends ESTestCase {
 
         latch.await();
         assertNotNull(exceptionRef.get());
-        assertThat(exceptionRef.get().getMessage(), containsString("failed to initialize a TrustManagerFactory"));
+        assertThat(exceptionRef.get(), throwableWithMessage(containsString("failed to initialize SSL TrustManager")));
         assertThat(sslService.sslContextHolder(config).sslContext(), sameInstance(context));
     }
 

+ 11 - 6
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java

@@ -60,6 +60,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
 import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsString;
@@ -194,7 +195,8 @@ public class SSLServiceTests extends ESTestCase {
             sslService.createSSLEngine(configuration, null, -1);
             fail("expected an exception");
         } catch (ElasticsearchException e) {
-            assertThat(e.getMessage(), containsString("failed to initialize a KeyManagerFactory"));
+            assertThat(e, throwableWithMessage("failed to load SSL configuration [xpack.security.transport.ssl]"));
+            assertThat(e.getCause(), throwableWithMessage(containsString("failed to initialize SSL KeyManager")));
         }
     }
 
@@ -326,7 +328,8 @@ public class SSLServiceTests extends ESTestCase {
             .build();
         ElasticsearchException e =
                 expectThrows(ElasticsearchException.class, () -> new SSLService(settings, env));
-        assertThat(e.getMessage(), is("failed to initialize a TrustManagerFactory"));
+        assertThat(e, throwableWithMessage("failed to load SSL configuration [xpack.security.transport.ssl]"));
+        assertThat(e.getCause(), throwableWithMessage(containsString("failed to initialize SSL TrustManager")));
     }
 
     public void testThatKeystorePasswordIsRequired() throws Exception {
@@ -336,7 +339,8 @@ public class SSLServiceTests extends ESTestCase {
             .build();
         ElasticsearchException e =
                 expectThrows(ElasticsearchException.class, () -> new SSLService(settings, env));
-        assertThat(e.getMessage(), is("failed to create trust manager"));
+        assertThat(e, throwableWithMessage("failed to load SSL configuration [xpack.security.transport.ssl]"));
+        assertThat(e.getCause(), throwableWithMessage("failed to create trust manager"));
     }
 
     public void testCiphersAndInvalidCiphersWork() throws Exception {
@@ -369,9 +373,10 @@ public class SSLServiceTests extends ESTestCase {
             .setSecureSettings(secureSettings)
             .putList("xpack.security.transport.ssl.cipher_suites", new String[] { "foo", "bar" })
             .build();
-        IllegalArgumentException e =
-                expectThrows(IllegalArgumentException.class, () -> new SSLService(settings, env));
-        assertThat(e.getMessage(), is("none of the ciphers [foo, bar] are supported by this JVM"));
+        ElasticsearchException e =
+                expectThrows(ElasticsearchException.class, () -> new SSLService(settings, env));
+        assertThat(e, throwableWithMessage("failed to load SSL configuration [xpack.security.transport.ssl]"));
+        assertThat(e.getCause(), throwableWithMessage("none of the ciphers [foo, bar] are supported by this JVM"));
     }
 
     public void testThatSSLEngineHasCipherSuitesOrderSet() throws Exception {

+ 2 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfigTests.java

@@ -17,6 +17,7 @@ import javax.net.ssl.X509ExtendedKeyManager;
 
 import java.security.PrivateKey;
 
+import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.notNullValue;
@@ -46,7 +47,7 @@ public class StoreKeyConfigTests extends ESTestCase {
             KeyManagerFactory.getDefaultAlgorithm(), TrustManagerFactory.getDefaultAlgorithm());
         ElasticsearchException ee = expectThrows(ElasticsearchException.class, () ->
             keyConfigPkcs11.createKeyManager(TestEnvironment.newEnvironment(settings)));
-        assertThat(ee.getMessage(), containsString("failed to initialize a KeyManagerFactory"));
+        assertThat(ee, throwableWithMessage(containsString("failed to initialize SSL KeyManager")));
         assertThat(ee.getCause().getMessage(), containsString("PKCS11 not found"));
     }
 

+ 322 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/ssl/SSLErrorMessageTests.java

@@ -0,0 +1,322 @@
+/*
+ * 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.ssl;
+
+import org.apache.lucene.util.Constants;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.env.Environment;
+import org.elasticsearch.env.TestEnvironment;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.core.ssl.SSLService;
+import org.junit.Before;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.channels.Channels;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.CopyOption;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.security.AccessControlException;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+import static org.elasticsearch.test.SecuritySettingsSource.addSecureSettings;
+import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.instanceOf;
+
+/**
+ * This is a suite of tests to ensure that meaningful error messages are generated for defined SSL configuration problems.
+ */
+public class SSLErrorMessageTests extends ESTestCase {
+
+    private Environment env;
+    private Map<String, Path> paths;
+
+    @Before
+    public void setup() throws Exception {
+        env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build());
+        paths = new HashMap<>();
+
+        requirePath("ca1.p12");
+        requirePath("ca1.jks");
+        requirePath("ca1.crt");
+
+        requirePath("cert1a.p12");
+        requirePath("cert1a.jks");
+        requirePath("cert1a.crt");
+        requirePath("cert1a.key");
+    }
+
+    public void testMessageForMissingKeystore() {
+        checkMissingKeyManagerResource("keystore", "keystore.path", null);
+    }
+
+    public void testMessageForMissingPemCertificate() {
+        checkMissingKeyManagerResource("certificate", "certificate", withKey("cert1a.key"));
+    }
+
+    public void testMessageForMissingPemKey() {
+        checkMissingKeyManagerResource("key", "key", withCertificate("cert1a.crt"));
+    }
+
+    public void testMessageForMissingTruststore() {
+        checkMissingTrustManagerResource("truststore", "truststore.path");
+    }
+
+    public void testMessageForMissingCertificateAuthorities() {
+        checkMissingTrustManagerResource("certificate_authorities", "certificate_authorities");
+    }
+
+    public void testMessageForKeystoreWithoutReadAccess() throws Exception {
+        checkUnreadableKeyManagerResource("cert1a.p12", "keystore", "keystore.path", null);
+    }
+
+    public void testMessageForPemCertificateWithoutReadAccess() throws Exception {
+        checkUnreadableKeyManagerResource("cert1a.crt", "certificate", "certificate", withKey("cert1a.key"));
+    }
+
+    public void testMessageForPemKeyWithoutReadAccess() throws Exception {
+        checkUnreadableKeyManagerResource("cert1a.key", "key", "key", withCertificate("cert1a.crt"));
+    }
+
+    public void testMessageForTruststoreWithoutReadAccess() throws Exception {
+        checkUnreadableTrustManagerResource("cert1a.p12", "truststore", "truststore.path");
+    }
+
+    public void testMessageForCertificateAuthoritiesWithoutReadAccess() throws Exception {
+        checkUnreadableTrustManagerResource("ca1.crt", "certificate_authorities", "certificate_authorities");
+    }
+
+    public void testMessageForKeyStoreOutsideConfigDir() throws Exception {
+        checkBlockedKeyManagerResource("keystore", "keystore.path", null);
+    }
+
+    public void testMessageForPemCertificateOutsideConfigDir() throws Exception {
+        checkBlockedKeyManagerResource("certificate", "certificate", withKey("cert1a.key"));
+    }
+
+    public void testMessageForPemKeyOutsideConfigDir() throws Exception {
+        checkBlockedKeyManagerResource("key", "key", withCertificate("cert1a.crt"));
+    }
+
+    public void testMessageForTrustStoreOutsideConfigDir() throws Exception {
+        checkBlockedTrustManagerResource("truststore", "truststore.path");
+    }
+
+    public void testMessageForCertificateAuthoritiesOutsideConfigDir() throws Exception {
+        checkBlockedTrustManagerResource("certificate_authorities", "certificate_authorities");
+    }
+
+    private void checkMissingKeyManagerResource(String fileType, String configKey, @Nullable Settings.Builder additionalSettings) {
+        checkMissingResource("KeyManager", fileType, configKey,
+            (prefix, builder) -> buildKeyConfigSettings(additionalSettings, prefix, builder));
+    }
+
+    private void buildKeyConfigSettings(@Nullable Settings.Builder additionalSettings, String prefix, Settings.Builder builder) {
+        configureWorkingTruststore(prefix, builder);
+        if (additionalSettings != null) {
+            builder.put(additionalSettings.normalizePrefix(prefix + ".").build());
+        }
+    }
+
+    private void checkMissingTrustManagerResource(String fileType, String configKey) {
+        checkMissingResource("TrustManager", fileType, configKey, this::configureWorkingKeystore);
+    }
+
+    private void checkUnreadableKeyManagerResource(String fromResource, String fileType, String configKey,
+                                                   @Nullable Settings.Builder additionalSettings) throws Exception {
+        checkUnreadableResource("KeyManager", fromResource, fileType, configKey,
+            (prefix, builder) -> buildKeyConfigSettings(additionalSettings, prefix, builder));
+    }
+
+    private void checkUnreadableTrustManagerResource(String fromResource, String fileType, String configKey) throws Exception {
+        checkUnreadableResource("TrustManager", fromResource, fileType, configKey, this::configureWorkingKeystore);
+    }
+
+    private void checkBlockedKeyManagerResource(String fileType, String configKey, Settings.Builder additionalSettings) throws Exception {
+        checkBlockedResource("KeyManager", fileType, configKey,
+            (prefix, builder) -> buildKeyConfigSettings(additionalSettings, prefix, builder));
+    }
+
+    private void checkBlockedTrustManagerResource(String fileType, String configKey) throws Exception {
+        checkBlockedResource("TrustManager", fileType, configKey, this::configureWorkingKeystore);
+    }
+
+    private void checkMissingResource(String sslManagerType, String fileType, String configKey,
+                                      BiConsumer<String, Settings.Builder> configure) {
+        final String prefix = randomSslPrefix();
+        final Settings.Builder settings = Settings.builder();
+        configure.accept(prefix, settings);
+
+        final String fileName = missingFile();
+        final String key = prefix + "." + configKey;
+        settings.put(key, fileName);
+
+        Throwable exception = expectFailure(settings);
+        assertThat(exception, throwableWithMessage("failed to load SSL configuration [" + prefix + "]"));
+        assertThat(exception, instanceOf(ElasticsearchSecurityException.class));
+
+        exception = exception.getCause();
+        assertThat(exception, throwableWithMessage(
+            "failed to initialize SSL " + sslManagerType + " - " + fileType + " file [" + fileName + "] does not exist"));
+        assertThat(exception, instanceOf(ElasticsearchException.class));
+
+        exception = exception.getCause();
+        assertThat(exception, instanceOf(NoSuchFileException.class));
+        assertThat(exception, throwableWithMessage(fileName));
+    }
+
+    private void checkUnreadableResource(String sslManagerType, String fromResource, String fileType, String configKey,
+                                         BiConsumer<String, Settings.Builder> configure) throws Exception {
+        final String prefix = randomSslPrefix();
+        final Settings.Builder settings = Settings.builder();
+        configure.accept(prefix, settings);
+
+        final String fileName = unreadableFile(fromResource);
+        final String key = prefix + "." + configKey;
+        settings.put(key, fileName);
+
+        Throwable exception = expectFailure(settings);
+        assertThat(exception, throwableWithMessage("failed to load SSL configuration [" + prefix + "]"));
+        assertThat(exception, instanceOf(ElasticsearchSecurityException.class));
+
+        exception = exception.getCause();
+        assertThat(exception, throwableWithMessage(
+            "failed to initialize SSL " + sslManagerType + " - not permitted to read " + fileType + " file [" + fileName + "]"));
+        assertThat(exception, instanceOf(ElasticsearchException.class));
+
+        exception = exception.getCause();
+        assertThat(exception, instanceOf(AccessDeniedException.class));
+        assertThat(exception, throwableWithMessage(fileName));
+    }
+
+    private void checkBlockedResource(String sslManagerType, String fileType, String configKey,
+                                      BiConsumer<String, Settings.Builder> configure) throws Exception {
+        final String prefix = randomSslPrefix();
+        final Settings.Builder settings = Settings.builder();
+        configure.accept(prefix, settings);
+
+        final String fileName = blockedFile();
+        final String key = prefix + "." + configKey;
+        settings.put(key, fileName);
+
+        Throwable exception = expectFailure(settings);
+        assertThat(exception, throwableWithMessage("failed to load SSL configuration [" + prefix + "]"));
+        assertThat(exception, instanceOf(ElasticsearchSecurityException.class));
+
+        exception = exception.getCause();
+        assertThat(exception, throwableWithMessage(
+            "failed to initialize SSL " + sslManagerType + " - access to read " + fileType + " file [" + fileName +
+                "] is blocked; SSL resources should be placed in the [" + env.configFile() + "] directory"));
+        assertThat(exception, instanceOf(ElasticsearchException.class));
+
+        exception = exception.getCause();
+        assertThat(exception, instanceOf(AccessControlException.class));
+        assertThat(exception, throwableWithMessage(containsString(fileName)));
+    }
+
+    private String missingFile() {
+        return resource("cert1a.p12").replace("cert1a.p12", "file.dne");
+    }
+
+    private String unreadableFile(String fromResource) throws IOException {
+        assumeFalse("This behaviour uses POSIX file permissions", Constants.WINDOWS);
+        final Path fromPath = this.paths.get(fromResource);
+        if (fromPath == null) {
+            throw new IllegalArgumentException("Test SSL resource " + fromResource + " has not been loaded");
+        }
+        return copy(fromPath, createTempFile(fromResource, "-no-read"), PosixFilePermissions.fromString("---------"));
+    }
+
+    private String blockedFile() throws IOException {
+        return "/this/path/is/outside/the/config/directory/file.error";
+    }
+
+    /**
+     * This more-or-less replicates the functionality of {@link Files#copy(Path, Path, CopyOption...)} but doing it this way allows us to
+     * set the file permissions when creating the file (which helps with security manager issues)
+     */
+    private String copy(Path fromPath, Path toPath, Set<PosixFilePermission> permissions) throws IOException {
+        Files.deleteIfExists(toPath);
+        final FileAttribute<Set<PosixFilePermission>> fileAttributes = PosixFilePermissions.asFileAttribute(permissions);
+        final EnumSet<StandardOpenOption> options = EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
+        try (SeekableByteChannel channel = Files.newByteChannel(toPath, options, fileAttributes);
+             OutputStream out = Channels.newOutputStream(channel)) {
+            Files.copy(fromPath, out);
+        }
+        return toPath.toString();
+    }
+
+    private Settings.Builder withKey(String fileName) {
+        assertThat(fileName, endsWith(".key"));
+        return Settings.builder().put("key", resource(fileName));
+    }
+
+    private Settings.Builder withCertificate(String fileName) {
+        assertThat(fileName, endsWith(".crt"));
+        return Settings.builder().put("certificate", resource(fileName));
+    }
+
+    private Settings.Builder configureWorkingTruststore(String prefix, Settings.Builder settings) {
+        settings.put(prefix + ".truststore.path", resource("cert1a.p12"));
+        addSecureSettings(settings, secure -> secure.setString(prefix + ".truststore.secure_password", "cert1a-p12-password"));
+        return settings;
+    }
+
+    private Settings.Builder configureWorkingKeystore(String prefix, Settings.Builder settings) {
+        settings.put(prefix + ".keystore.path", resource("cert1a.p12"));
+        addSecureSettings(settings, secure -> secure.setString(prefix + ".keystore.secure_password", "cert1a-p12-password"));
+        return settings;
+    }
+
+    private ElasticsearchException expectFailure(Settings.Builder settings) {
+        return expectThrows(ElasticsearchException.class, () -> new SSLService(settings.build(), env));
+    }
+
+    private String resource(String fileName) {
+        final Path path = this.paths.get(fileName);
+        if (path == null) {
+            throw new IllegalArgumentException("Test SSL resource " + fileName + " has not been loaded");
+        }
+        return path.toString();
+    }
+
+    private void requirePath(String fileName) throws FileNotFoundException {
+        final Path path = getDataPath("/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/" + fileName);
+        if (Files.exists(path)) {
+            paths.put(fileName, path);
+        } else {
+            throw new FileNotFoundException("File " + path + " does not exist");
+        }
+    }
+
+    private String randomSslPrefix() {
+        return randomFrom(
+            "xpack.security.transport.ssl",
+            "xpack.security.http.ssl",
+            "xpack.http.ssl",
+            "xpack.security.authc.realms.ldap.ldap1.ssl",
+            "xpack.security.authc.realms.saml.saml1.ssl",
+            "xpack.monitoring.exporters.http.ssl"
+        );
+    }
+}

+ 106 - 0
x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/README.txt

@@ -0,0 +1,106 @@
+#
+# NOTE: This readme is also an executable shell script.
+#       Run it with bash ./README.txt
+#
+
+#
+# Make sure we can call certutil
+#
+[ -n "$ES_HOME" ] || { printf '%s: $ES_HOME is not set\n' "$0" ; exit 1; }
+[ -d "$ES_HOME" ] || { printf '%s: $ES_HOME is not a directory\n' "$0" ; exit 1; }
+
+function certutil() { "$ES_HOME/bin/elasticsearch-certutil" "$@"; }
+
+#
+# Helper functions to generate files & convert file types
+#
+function new-p12-ca() {
+    local P12File="$1"
+    local P12Pass="$2"
+    local CaDn="$3"
+
+    certutil ca --ca-dn="$CaDn" --days=5000 --out ${PWD}/$P12File --pass="$P12Pass"
+}
+
+function new-p12-cert() {
+    local CertFile="$1"
+    local CertPass="$2"
+    local CertName="$3"
+    local CaFile="$4"
+    local CaPass="$5"
+    shift 5
+
+    certutil cert --ca="${PWD}/$CaFile" --ca-pass="$CaPass" --days=5000 --out ${PWD}/$CertFile --pass="$CertPass" --name="$CertName" "$@"
+}
+
+function p12-to-jks() {
+    local P12File="$1"
+    local P12Pass="$2"
+    local JksFile="$3"
+    local JksPass="$4"
+    
+    keytool -importkeystore -srckeystore "${PWD}/$P12File" -srcstorepass "$P12Pass" \
+        -destkeystore "${PWD}/$JksFile"  -deststoretype JKS -deststorepass "$JksPass"
+}
+
+function p12-export-cert() {
+    local P12File="$1"
+    local P12Pass="$2"
+    local P12Name="$3"
+    local PemFile="$4"
+    
+    keytool -exportcert -keystore "${PWD}/$P12File" -storepass "$P12Pass" -alias "$P12Name" \
+        -rfc -file "${PWD}/$PemFile" 
+}
+
+function p12-export-pair() {
+    local P12File="$1"
+    local P12Pass="$2"
+    local P12Name="$3"
+    local CrtFile="$4"
+    local KeyFile="$5"
+
+    local TmpFile="${PWD}/$(basename $P12File .p12).tmp.p12"
+    
+    # OpenSSL doesn't have a way to export a single entry
+    # Keytool doesn't have a way to export keys
+    # So we use keytool to export the whole entry to a temporary PKCS#12 and then use openssl to export that to PEM
+
+    keytool -importkeystore -srckeystore "${PWD}/$P12File" -srcstorepass "$P12Pass" -srcalias "$P12Name" \
+        -destkeystore "$TmpFile" -deststorepass "tmp_password" 
+
+    # This produces an unencrypted PKCS#1 key. Use other commands to convert it if needed
+    # The sed is to skip "BagAttributes" which we don't need
+    openssl pkcs12 -in "$TmpFile" -nodes   -nocerts -passin "pass:tmp_password" | sed -n -e'/^-----/,/^-----/p' > $KeyFile
+    openssl pkcs12 -in "$TmpFile" -clcerts -nokeys  -passin "pass:tmp_password" | sed -n -e'/^-----/,/^-----/p' > $CrtFile
+
+    rm $TmpFile
+}
+
+
+#
+# Create a CA in PKCS#12
+#
+new-p12-ca ca1.p12 "ca1-p12-password" 'CN=Certificate Authority 1,OU=ssl-error-message-test,DC=elastic,DC=co'
+
+# Make a JKS version of the CA
+p12-to-jks ca1.p12 "ca1-p12-password" ca1.jks "ca1-jks-password" 
+
+# Make a PEM version of the CA cert
+p12-export-cert ca1.p12 "ca1-p12-password" "ca" ca1.crt 
+
+#
+# Create a Cert/Key Pair in PKCS#12
+#  - "cert1a" is signed by "ca1"
+#  - "cert1a.p12" is password protected, and can act as a keystore or truststore
+#
+new-p12-cert cert1a.p12 "cert1a-p12-password" "cert1a" "ca1.p12" "ca1-p12-password"
+
+# Convert to JKS
+#  - "cert1a.jks" is password protected, and can act as a keystore or truststore
+p12-to-jks cert1a.p12 "cert1a-p12-password" cert1a.jks "cert1a-jks-password" 
+
+# Convert to PEM
+#  - "cert1a.key" is an (unprotected) PKCS#1 key
+p12-export-pair cert1a.p12 "cert1a-p12-password" "cert1a" cert1a.crt cert1a.key 
+

+ 23 - 0
x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/ca1.crt

@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDwTCCAqmgAwIBAgIUK67pJtem4zfhIP4SpjoukdLlwnIwDQYJKoZIhvcNAQEL
+BQAwcDESMBAGCgmSJomT8ixkARkWAmNvMRcwFQYKCZImiZPyLGQBGRYHZWxhc3Rp
+YzEfMB0GA1UECxMWc3NsLWVycm9yLW1lc3NhZ2UtdGVzdDEgMB4GA1UEAxMXQ2Vy
+dGlmaWNhdGUgQXV0aG9yaXR5IDEwHhcNMTkwNzE4MDc1NDMyWhcNMzMwMzI2MDc1
+NDMyWjBwMRIwEAYKCZImiZPyLGQBGRYCY28xFzAVBgoJkiaJk/IsZAEZFgdlbGFz
+dGljMR8wHQYDVQQLExZzc2wtZXJyb3ItbWVzc2FnZS10ZXN0MSAwHgYDVQQDExdD
+ZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBAK/yaI+3JTakaGHrUBlVhvAP7Jeejkw0XevcRr4H96pO6fEZuq8Kkf73
+/J/wZkzTqE6AGxVYGJV7brzZj8QwXPnkI4fCOLYLeBBX4333lb+X20RGmEjLeTKK
+XkOA4FlpbDaecQwbUArBPF3sc2AXW62ZWEosUTR67wr3tFn3uX1RnX5OM0+7qI0Z
+gcEm8ohqlig3NC7EmkuP/50CA2OnuuD8b57u0cdgM7uFXTzIASUCz5RU7SSYiI0q
+HYOPza+CfUgevReRDc9rzIIVcxMmbab6gABysKGbUdlSTsqGqZMbEprFTv7zt7u1
+lqHeVEjhL7F9769XGaqFeGfa9b1odokCAwEAAaNTMFEwHQYDVR0OBBYEFKHpV7oS
+AMlY64s6FbHLPWYtoClTMB8GA1UdIwQYMBaAFKHpV7oSAMlY64s6FbHLPWYtoClT
+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAg9Rq5QIiFpmmIN
+yMMeSVCeqZJ1l03WO8YPNcMVGzvjsm12UCHK2ppCahOerGOchnX6tdsjj2aKIL0n
+L8++LSDgxbsDA8kxkSldjcY8sJZhTMzvfUkabmWvr99UpVRDKT3EXkMmSaSIaA+U
+1xEJAz8dPPFTXyxCNwEePyvy32I3JR6e/8UnUTVNlev8sOdwGvidqHq/PJvR4/qD
+B5bn39gsQ6OgUaT6ye6Zp3iUx5uNipGwr2SzoK6ERJ2rwEC1/mQ321rDkSSdnHLT
+E+hVL/qrGkg5cl0otPDFxIkOL33/lyyOW7cHdHEUkFsl9Osi6O0HFw1z4DoR7zT3
+Ngckq2E=
+-----END CERTIFICATE-----

BIN
x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/ca1.jks


BIN
x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/ca1.p12


+ 21 - 0
x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/cert1a.crt

@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIVAOsqJ9r+RWgkCe7vyZXYmF+sfBoNMA0GCSqGSIb3DQEB
+CwUAMHAxEjAQBgoJkiaJk/IsZAEZFgJjbzEXMBUGCgmSJomT8ixkARkWB2VsYXN0
+aWMxHzAdBgNVBAsTFnNzbC1lcnJvci1tZXNzYWdlLXRlc3QxIDAeBgNVBAMTF0Nl
+cnRpZmljYXRlIEF1dGhvcml0eSAxMB4XDTE5MDcxODA3NTQzNVoXDTMzMDMyNjA3
+NTQzNVowETEPMA0GA1UEAxMGY2VydDFhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAxZy4TEULFQxk988qZ4itwYo5oRnTfzrL5/Rb516Ll5Q9RrXnIFyg
+u1CtF/eX0S1BGw9MCDSFK9fpwQMbDKo0G6E9arV9hTMo2MHMOhRXux6qnQlYzxW+
+7dfpzoLQPb0ObYlvC9LclJwz2Hqz4qABSMfbHd789Y9CjHsNPb+9OZDMj6oJBT/G
+nXiZxk0bJ/O8N1BN9nDUWnhqsDuuHVW30PuF0JVwHzqMqi/FVporabGcYnur+Pbd
+kevpidWFFx6kpoAeXuYWPEeqTLiUpPyMwxNn620xUbr4bq8hEOkhswsdhZ90ffbv
+lMZezS7vL5zBOGEtnGFXh0FaHfutZwX2nwIDAQABo00wSzAdBgNVHQ4EFgQUCcIu
+5sjyXxEoFfz7zz3CVdpPGTMwHwYDVR0jBBgwFoAUoelXuhIAyVjrizoVscs9Zi2g
+KVMwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAUtXdkttqA4BVyVhfjiOg
+LA43V26gl/NsjVp/8SpEYahp/FPl2wQe3D/6Py9a0eVIdJQ7vwx15RqyeNi679Lg
+hQxyh/dvLlnxhOY1LHAwc2v4zDr96h/JA8FeqemXNPmEIwdy930rZpgA/O/9dlh/
+pVSRxg5mrNL1ekyzty44IOnbJ3URLyQmNOns3d6AKxwIxgkM041lex9NXsyw7gaz
+GnxhORukdbuxW2zqweVKoTTo2AriUWic8FnZDNeaHgJ8LP1xvYECq1cb3S1qzzEO
+LCofq9Lvxzemc/Q435khAvFAA4oB2qV37eZn3B1HT2RiPzHvM3ypjqhIAbqKX++w
+AA==
+-----END CERTIFICATE-----

BIN
x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/cert1a.jks


+ 27 - 0
x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/cert1a.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAxZy4TEULFQxk988qZ4itwYo5oRnTfzrL5/Rb516Ll5Q9RrXn
+IFygu1CtF/eX0S1BGw9MCDSFK9fpwQMbDKo0G6E9arV9hTMo2MHMOhRXux6qnQlY
+zxW+7dfpzoLQPb0ObYlvC9LclJwz2Hqz4qABSMfbHd789Y9CjHsNPb+9OZDMj6oJ
+BT/GnXiZxk0bJ/O8N1BN9nDUWnhqsDuuHVW30PuF0JVwHzqMqi/FVporabGcYnur
++PbdkevpidWFFx6kpoAeXuYWPEeqTLiUpPyMwxNn620xUbr4bq8hEOkhswsdhZ90
+ffbvlMZezS7vL5zBOGEtnGFXh0FaHfutZwX2nwIDAQABAoIBAQCfB1lVn7akcL4M
+o36HpXnXqCpqmIMY/7M67u7LCs4h5R1O+3KOG50KQYmbIRjfMKEVasEQVVvahb7L
+InmxPoQCvEbVykrCWAKGNafqEZbssmgxSmVa+jAV7k1tcN6u4KdsxU5FYKM9QVuD
+2nNLbOK7tIKEzoAaCflPXnOwfs6ENLYz5vu96RAI5u8dn7GDw9gjuOF5Ij+IpY38
+HBqrAhmZs4P7VFO03XwVXvuxEmuJ4Ng9n/bd+D8dO9WVLhRCNPY6HIyUTtRyHyvY
+hW+ejkgBjrpdCh6jmWKUz3aD1m4QY1NSmuYvXCLJjeO9PLHj8Qib7BSET4/ZvLcn
+xGmFlpDZAoGBAOLcV2PTYHeRSkgd1l3HRYV022TZLdqPsKs4OQnG0oz3OoLt8nBW
+85VhDVYIgWF7Ro6SQBJUGUtCy/8pCySGcmm2RrS9VSunuiLr1mFTyZEg6Py5LixI
+fimr8R5ZZp3yQtyDR/rX62z/F76HT87lBtAkc9gz50ceeHIbYjUK9zotAoGBAN7+
+oB7JJnFtkKd7b17wEqz7xsGaeXKrZPt9LyTG0/3hTlqRKs51+TEo11R3sH6ny/so
+VcobEnvLWJ1slRMFxbJTI2gQYjkRdxbgRKlL3D7nkXP6ZkyPTqZWHwnmL25HvkfT
+Cg5t6uuUwu7zBx8pgvSVI7+ohNbIQihOuvSzwe97AoGAPz/vnYVxf+SiMTkga0UD
+vRG3mYZzolwthY9HV9J3IZZMWfS9g1S61QsaDMKST/tu0JE2C+YmpzUYayumT6is
+fYy0ae/fryw0WUgnnTsfF7d1PgDjPrV3d2bY8v05/w9sVM7FYsqQePMmS5iuR6DT
+JXUEV2MhOFLmgengl6lXBIECgYEApaMs/LEdRRPFZyJgz3wz4xoBwL0liO+Wytdp
+tT+bJ/G7abp6uXEX8FQN3kgpaWrqMVz8nGsPh7S71fNZqA+ZkaP/oMTKsOkwNGXR
+mo8mwfLwL93HLwcjvJor5AZ/JMNVq7QuvBkZbnPJeAQ9PgBvrY06SJq8UW3e9mHE
+rQ749xECgYA84wbJYvOBDzXq9QFBVgCI46i7rDH7L8fxSU5kXYqG8hQ0DMmjXPE1
+aBD5a0qbeMbaA3yVYdtgSK2T47Kq+k/DE5Q/EFshuLShX9iEmB2WeNtLvTl4AO0i
+9pwaI9uyzZCtK4bOUMq3Anf6AFnlrPRiXdhQYMrNQlc7cPULdawAtw==
+-----END RSA PRIVATE KEY-----

BIN
x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/ssl/SSLServiceErrorMessageTests/cert1a.p12