I would like to be able to run a spring boot webserver that connects to other servers using the SSL protocol that uses self-signed certificates.
To do this I now have to specify the javax.net.ssl.trustStore
and javax.net.ssl.trustStorePassword
system properties when starting the application.
I would like to be able to set this up using my application.properties
, so that all configuration is in one place, and I can use classpath to locate the trust store.
I can specify the server.ssl.trust-store
and server.ssl.trust-store-password
but this is not picked up without also specifying server.ssl.key-store
and related properties.
The main problem then becomes that the spring boot application will start with a https connector (and no http connector), while actually I have no interest to run in https mode.
The spring boot server just needs to connect to other servers with https.
My feature request is that you are able to set up a trust store without having to specify properties related to running the server in https mode.
The server.ssl.trust-store
and server.ssl.trust-store-password
are specifically to setup entries for the embedded server. If I understand you correctly you want something similar for client connections that you make yourself.
How are you making these connections? Do you use RestTemplate
?
Yes, indeed. I want something similar for client connections. I'm not making the client connections directly. In this case they are created by a keycloak adapter. I see references to RestTemplate in this library.
@robert-gv Do you have any sample code that shows what you're currently doing? That might be a good start in us creating auto-configuration.
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
I don't think the sample code will add much to the information already provided, but here you go.
You can see in the Application.java what I would like to be able to set in the application.properties
For what it's worth, this is how we do it. We put the self-signed server certificate in src/main/resources
, and add a custom property app.ssl.trusted-certificate-location = classpath:server.cert.pem
. We use a certificate instead of a keystore because it's easier to export from the server.
In the code, we parse the certificate and add it to a custom X509TrustManager that trusts both the default truststore and the included certificate (because we use valid certificates for production, and self-signed for staging). Then we call SSLContext.setDefault(sslContext)
and HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory())
. Only the former should be needed, but it seems to be ignored by IBM WebSphere, so we call the latter as well.
SslConfig.java
@Configuration
public class SslConfig {
private static final Logger logger = LoggerFactory.getLogger(SslConfig.class);
@Bean
public Configured configure(SslProperties sslProperties, ResourceLoader resourceLoader)
throws GeneralSecurityException, IOException {
String certLocation = sslProperties.getTrustedCertificateLocation();
if (!StringUtils.hasText(certLocation)) {
return new Configured();
}
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<Certificate> certificates = new ArrayList<Certificate>();
Resource certificateResource = resourceLoader.getResource(certLocation);
InputStream inputStream = null;
try {
inputStream = certificateResource.getInputStream();
certificates.addAll(certificateFactory.generateCertificates(inputStream));
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ex) {
logger.warn("Could not close resource InputStream for {}", certLocation, ex);
}
}
}
ExtraCertsTrustManager extraCertsTrustManager = new ExtraCertsTrustManager(certificates);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { extraCertsTrustManager }, null);
SSLContext.setDefault(sslContext);
// Required for WebSphere
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
return new Configured();
}
/**
* Dummy bean to notify that SSL is configured
*/
public static class Configured {
private Configured() {
}
}
}
SslProperties.java
@Component
@ConfigurationProperties(prefix = "app.ssl")
public class SslProperties {
/**
* Location of an X.509 certificate file. Can use classpath: prefix to use
* certificate file from resources.
*/
private String trustedCertificateLocation;
public String getTrustedCertificateLocation() {
return trustedCertificateLocation;
}
public void setTrustedCertificateLocation(String trustedCertificateLocation) {
this.trustedCertificateLocation = trustedCertificateLocation;
}
}
ExtraCertsTrustManager.java
public class ExtraCertsTrustManager implements X509TrustManager {
private final X509TrustManager defaultX509TrustManager;
private final X509TrustManager extraX509TrustManager;
public ExtraCertsTrustManager(Collection<Certificate> certificates) throws GeneralSecurityException {
defaultX509TrustManager = createX509TrustManager(null);
KeyStore extraKeyStore = createKeyStore(certificates);
extraX509TrustManager = createX509TrustManager(extraKeyStore);
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
CertificateException ex1 = null;
if (defaultX509TrustManager != null) {
try {
defaultX509TrustManager.checkClientTrusted(chain, authType);
// Success
return;
} catch (CertificateException ex) {
ex1 = ex;
}
}
CertificateException ex2 = null;
if (extraX509TrustManager != null) {
try {
extraX509TrustManager.checkClientTrusted(chain, authType);
// Success
return;
} catch (CertificateException ex) {
ex2 = ex;
}
}
if (ex1 != null) {
throw ex1;
}
if (ex2 != null) {
throw ex2;
}
throw new CertificateException("No trust managers");
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
CertificateException ex1 = null;
if (defaultX509TrustManager != null) {
try {
defaultX509TrustManager.checkServerTrusted(chain, authType);
// Success
return;
} catch (CertificateException ex) {
ex1 = ex;
}
}
CertificateException ex2 = null;
if (extraX509TrustManager != null) {
try {
extraX509TrustManager.checkServerTrusted(chain, authType);
// Success
return;
} catch (CertificateException ex) {
ex2 = ex;
}
}
if (ex1 != null) {
throw ex1;
}
if (ex2 != null) {
throw ex2;
}
throw new CertificateException("No trust managers");
}
@Override
public X509Certificate[] getAcceptedIssuers() {
Set<X509Certificate> acceptedIssuers = new HashSet<X509Certificate>();
if (defaultX509TrustManager != null) {
Collections.addAll(acceptedIssuers, defaultX509TrustManager.getAcceptedIssuers());
}
if (extraX509TrustManager != null) {
Collections.addAll(acceptedIssuers, extraX509TrustManager.getAcceptedIssuers());
}
return acceptedIssuers.toArray(new X509Certificate[acceptedIssuers.size()]);
}
private KeyStore createKeyStore(Collection<Certificate> certificates) throws KeyStoreException {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
try {
keystore.load(null, null);
} catch (IOException ex) {
// Should never happen
throw new RuntimeException(ex);
} catch (GeneralSecurityException ex) {
// Should never happen
throw new RuntimeException(ex);
}
for (Certificate certificate : certificates) {
String alias = certificate.toString();
keystore.setCertificateEntry(alias, certificate);
}
return keystore;
}
private X509TrustManager createX509TrustManager(KeyStore keystore) throws GeneralSecurityException {
TrustManagerFactory trustManagerFactory = //
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keystore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length == 0) {
return null;
}
if (trustManagers.length > 1) {
throw new GeneralSecurityException(String.format( //
"Expected 1 TrustManager from TrustManagerFactory(%s), got %s", //
trustManagerFactory, trustManagers.length));
}
TrustManager trustManager = trustManagers[0];
if (!(trustManager instanceof X509TrustManager)) {
throw new GeneralSecurityException(String.format( //
"Expected %s from TrustManagerFactory(%s), got %s", //
X509TrustManager.class.getCanonicalName(), trustManagerFactory,
trustManager.getClass().getCanonicalName()));
}
return (X509TrustManager) trustManager;
}
}
IMO, setting the default SSLContext
is too broad and isn't something that we should do via auto-configuration.
A quick search in Eclipse shows me that the default context is used by Cassandra's driver, RabbitMQ's client, Tomcat, Jetty, etc. While I'm sure it works very nicely in the context of a specific application, I think we might break things in ways that are difficult to debug if we applied this approach more generally. Furthermore, you may want each different sort of client that's using SSL to have different certificates that it trusts. The concerns described above also largely apply to configuring the javax.net.ssl.trustStore
and javax.net.ssl.trustStorePassword
system properties as well.
I think we're left with making sure it's easy to configure a truststore on clients that may be using SSL. Rather than trying to tackle all of them on a case-by-case basic, I'd prefer to consider each type of client individually and see what requirements people have so I'm going to close this issue.
Anyone looking for easy truststore configuration for a particular type of client, please open a new issue stating the client that you're using and providing as much detail as possible about what you'd like to configure.
Most helpful comment
For what it's worth, this is how we do it. We put the self-signed server certificate in
src/main/resources
, and add a custom propertyapp.ssl.trusted-certificate-location = classpath:server.cert.pem
. We use a certificate instead of a keystore because it's easier to export from the server.In the code, we parse the certificate and add it to a custom X509TrustManager that trusts both the default truststore and the included certificate (because we use valid certificates for production, and self-signed for staging). Then we call
SSLContext.setDefault(sslContext)
andHttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory())
. Only the former should be needed, but it seems to be ignored by IBM WebSphere, so we call the latter as well.SslConfig.java
SslProperties.java
ExtraCertsTrustManager.java