Skip to content

Commit dd652a8

Browse files
moarychansaragluna
andauthored
Support Spring SSL bundles based on Key Vault JCA (Azure#44259)
* Support Spring SSL bundles based on Key Vault JCA * One KeyStore per Key Vault Store * allow client auth choose alias --------- Co-authored-by: Xiaolu Dai <[email protected]>
1 parent 9e02ed8 commit dd652a8

File tree

25 files changed

+1444
-3
lines changed

25 files changed

+1444
-3
lines changed

eng/versioning/version_client.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ com.azure.spring:spring-cloud-azure-starter-jdbc-mysql;5.20.1;5.21.0-beta.1
242242
com.azure.spring:spring-cloud-azure-starter-jdbc-postgresql;5.20.1;5.21.0-beta.1
243243
com.azure.spring:spring-cloud-azure-starter-keyvault;5.20.1;5.21.0-beta.1
244244
com.azure.spring:spring-cloud-azure-starter-keyvault-certificates;5.20.1;5.21.0-beta.1
245+
com.azure.spring:spring-cloud-azure-starter-keyvault-jca;5.21.0-beta.1;5.21.0-beta.1
245246
com.azure.spring:spring-cloud-azure-starter-keyvault-secrets;5.20.1;5.21.0-beta.1
246247
com.azure.spring:spring-cloud-azure-starter-monitor;1.0.0-beta.6;1.0.0-beta.7
247248
com.azure.spring:spring-cloud-azure-starter-servicebus-jms;5.20.1;5.21.0-beta.1

sdk/boms/spring-cloud-azure-dependencies/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@
194194
<artifactId>spring-cloud-azure-starter-keyvault</artifactId>
195195
<version>${project.version}</version>
196196
</dependency>
197+
<dependency>
198+
<groupId>com.azure.spring</groupId>
199+
<artifactId>spring-cloud-azure-starter-keyvault-jca</artifactId>
200+
<version>${project.version}</version>
201+
</dependency>
197202
<dependency>
198203
<groupId>com.azure.spring</groupId>
199204
<artifactId>spring-cloud-azure-starter-keyvault-secrets</artifactId>

sdk/spring/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
# Release History
22

33
## 5.21.0-beta.1 (Unreleased)
4+
5+
#### Features Added
6+
- Add the `spring-cloud-azure-starter-keyvault-jca`. This starter supports SSL Bundle with Azure Key Vault certificates. [#35782](https://github.com/Azure/azure-sdk-for-java/issues/35782).
7+
8+
### Spring Cloud Azure Dependencies (BOM)
9+
10+
#### Dependency Updates
11+
- Add dependency `com.azure.spring:spring-cloud-azure-starter-keyvault-jca`.
12+
413
### Spring Cloud Azure Autoconfigure
514
This section includes changes in `spring-cloud-azure-autoconfigure` module.
615

716
#### Bugs Fixed
817
- Custom `ObjectMapper` bean does not work for received messages. [#37796](https://github.com/Azure/azure-sdk-for-java/issues/37796).
918

19+
### Spring Cloud Azure Starter Key Vault
20+
This section includes changes in `spring-cloud-azure-starter-keyvault` module.
21+
22+
#### Features Added
23+
- Support SSL Bundle with Azure Key Vault certificates [#44259](https://github.com/Azure/azure-sdk-for-java/pull/44259).
24+
1025
## 5.20.1 (2025-03-03)
1126
- This release is compatible with Spring Boot 3.4.0-3.4.2, 3.3.0-3.3.6, 3.2.0-3.2.12, 3.1.0-3.1.12, 3.0.0-3.0.13. (Note: 3.4.x (x>2), 3.3.y (y>6) and 3.2.z (z>12) should be supported, but they aren't tested with this release.)
1227
- This release is compatible with Spring Cloud 2024.0.0, 2023.0.0-2023.0.4, 2022.0.0-2022.0.5. (Note: 2024.0.x(x>0) and 2023.0.y (y>4) should be supported, but they aren't tested with this release.)

sdk/spring/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ parameters:
146146
displayName: 'spring-cloud-azure-starter-keyvault-certificates'
147147
type: boolean
148148
default: true
149+
- name: release_springcloudazurestarterkeyvaultjca
150+
displayName: 'spring-cloud-azure-starter-keyvault-jca'
151+
type: boolean
152+
default: true
149153
- name: release_springcloudazurestarterkeyvaultsecrets
150154
displayName: 'spring-cloud-azure-starter-keyvault-secrets'
151155
type: boolean
@@ -427,6 +431,13 @@ extends:
427431
skipPublishDocMs: true
428432
skipVerifyChangelog: true
429433
releaseInBatch: ${{ parameters.release_springcloudazurestarterkeyvaultcertificates }}
434+
- name: spring-cloud-azure-starter-keyvault-jca
435+
groupId: com.azure.spring
436+
safeName: springcloudazurestarterkeyvaultjca
437+
skipPublishDocGithubIo: true
438+
skipPublishDocMs: true
439+
skipVerifyChangelog: true
440+
releaseInBatch: ${{ parameters.release_springcloudazurestarterkeyvaultjca }}
430441
- name: spring-cloud-azure-starter-keyvault-secrets
431442
groupId: com.azure.spring
432443
safeName: springcloudazurestarterkeyvaultsecrets

sdk/spring/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<module>spring-cloud-azure-starter-eventgrid</module>
4141
<module>spring-cloud-azure-starter-keyvault</module>
4242
<module>spring-cloud-azure-starter-keyvault-certificates</module>
43+
<module>spring-cloud-azure-starter-keyvault-jca</module>
4344
<module>spring-cloud-azure-starter-keyvault-secrets</module>
4445
<module>spring-cloud-azure-starter-servicebus-jms</module>
4546
<module>spring-cloud-azure-starter-servicebus</module>
@@ -104,6 +105,7 @@
104105
<module>spring-cloud-azure-starter-eventgrid</module>
105106
<module>spring-cloud-azure-starter-keyvault</module>
106107
<module>spring-cloud-azure-starter-keyvault-certificates</module>
108+
<module>spring-cloud-azure-starter-keyvault-jca</module>
107109
<module>spring-cloud-azure-starter-keyvault-secrets</module>
108110
<module>spring-cloud-azure-starter-servicebus-jms</module>
109111
<module>spring-cloud-azure-starter-servicebus</module>

sdk/spring/spring-cloud-azure-autoconfigure/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@
247247
<version>4.7.3</version> <!-- {x-version-update;com.azure:azure-security-keyvault-certificates;dependency} -->
248248
<optional>true</optional>
249249
</dependency>
250+
<dependency>
251+
<groupId>com.azure</groupId>
252+
<artifactId>azure-security-keyvault-jca</artifactId>
253+
<version>2.10.0</version> <!-- {x-version-update;com.azure:azure-security-keyvault-jca;dependency} -->
254+
<optional>true</optional>
255+
</dependency>
250256
<dependency>
251257
<groupId>com.azure</groupId>
252258
<artifactId>azure-security-keyvault-secrets</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.spring.cloud.autoconfigure.implementation.keyvault.jca;
5+
6+
import com.azure.security.keyvault.jca.KeyVaultJcaProvider;
7+
import com.azure.spring.cloud.autoconfigure.implementation.keyvault.jca.properties.AzureKeyVaultJcaProperties;
8+
import com.azure.spring.cloud.autoconfigure.implementation.keyvault.jca.properties.AzureKeyVaultSslBundleProperties;
9+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
12+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
13+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
14+
import org.springframework.boot.ssl.SslBundle;
15+
import org.springframework.context.annotation.Bean;
16+
import org.springframework.context.annotation.Configuration;
17+
18+
/**
19+
* {@link EnableAutoConfiguration Auto-configuration} for Azure Key Vault JCA support.
20+
* @since 5.21.0
21+
*/
22+
@Configuration(proxyBeanMethods = false)
23+
@ConditionalOnClass({ KeyVaultJcaProvider.class, SslBundle.class })
24+
@EnableConfigurationProperties({AzureKeyVaultJcaProperties.class, AzureKeyVaultSslBundleProperties.class})
25+
@ConditionalOnProperty(value = "spring.cloud.azure.keyvault.jca.enabled", havingValue = "true", matchIfMissing = true)
26+
public class AzureKeyVaultJcaAutoConfiguration {
27+
28+
@Bean
29+
@ConditionalOnMissingBean
30+
AzureKeyVaultSslBundleRegistrar azureKeyVaultSslBundleRegistrar(AzureKeyVaultJcaProperties jcaProperties,
31+
AzureKeyVaultSslBundleProperties sslBundlesProperties) {
32+
return new AzureKeyVaultSslBundleRegistrar(jcaProperties, sslBundlesProperties);
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.spring.cloud.autoconfigure.implementation.keyvault.jca;
5+
6+
import com.azure.security.keyvault.jca.KeyVaultJcaProvider;
7+
import com.azure.spring.cloud.autoconfigure.implementation.keyvault.jca.properties.AzureKeyVaultJcaProperties;
8+
import com.azure.spring.cloud.autoconfigure.implementation.keyvault.jca.properties.AzureKeyVaultSslBundleProperties;
9+
import com.azure.spring.cloud.core.implementation.properties.PropertyMapper;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.boot.autoconfigure.ssl.SslBundleRegistrar;
13+
import org.springframework.boot.ssl.SslBundle;
14+
import org.springframework.boot.ssl.SslBundleKey;
15+
import org.springframework.boot.ssl.SslBundleRegistry;
16+
import org.springframework.boot.ssl.SslOptions;
17+
import org.springframework.boot.ssl.SslStoreBundle;
18+
import org.springframework.context.ResourceLoaderAware;
19+
import org.springframework.core.io.ResourceLoader;
20+
import org.springframework.util.ClassUtils;
21+
import org.springframework.util.StringUtils;
22+
23+
import java.io.IOException;
24+
import java.security.KeyStore;
25+
import java.security.KeyStoreException;
26+
import java.security.NoSuchAlgorithmException;
27+
import java.security.NoSuchProviderException;
28+
import java.security.Security;
29+
import java.security.cert.CertificateException;
30+
import java.util.Arrays;
31+
import java.util.Map;
32+
import java.util.Objects;
33+
import java.util.Optional;
34+
import java.util.concurrent.atomic.AtomicBoolean;
35+
36+
/**
37+
* Azure Key Vault SslBundleRegistrar that registers SSL bundles based Key Vault JCA properties and Key Vault SSL bundle properties.
38+
*
39+
* @since 5.21.0
40+
*/
41+
public class AzureKeyVaultSslBundleRegistrar implements SslBundleRegistrar, ResourceLoaderAware {
42+
43+
private static final Logger LOGGER = LoggerFactory.getLogger(AzureKeyVaultSslBundleRegistrar.class);
44+
private ResourceLoader resourceLoader;
45+
private final Map<String, AzureKeyVaultJcaProperties.JcaVaultProperties> jcaVaults;
46+
private final Map<String, AzureKeyVaultSslBundleProperties.KeyVaultSslBundleProperties> sslBundles;
47+
private static final String[] JCA_SYSTEM_PROPERTY_KEYS = new String[]{
48+
"azure.keyvault.uri",
49+
"azure.keyvault.tenant-id",
50+
"azure.keyvault.client-id",
51+
"azure.keyvault.client-secret",
52+
"azure.keyvault.managed-identity",
53+
"azure.keyvault.jca.certificates-refresh-interval",
54+
"azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate",
55+
"azure.cert-path.well-known",
56+
"azure.cert-path.custom"
57+
};
58+
59+
public AzureKeyVaultSslBundleRegistrar(AzureKeyVaultJcaProperties azureKeyVaultJcaProperties,
60+
AzureKeyVaultSslBundleProperties azureKeyVaultSslBundleProperties) {
61+
this.jcaVaults = azureKeyVaultJcaProperties.getVaults();
62+
this.sslBundles = azureKeyVaultSslBundleProperties.getKeyvault();
63+
}
64+
65+
@Override
66+
public void registerBundles(SslBundleRegistry registry) {
67+
if (!hasKeyVaultJcaOnClasspath()) {
68+
LOGGER.debug("Skip configuring Key Vault SSL bundles because {}", "'com.azure:azure-security-keyvault-jca' "
69+
+ "doesn't exist in classpath.");
70+
return;
71+
}
72+
73+
if (sslBundles.isEmpty()) {
74+
LOGGER.debug("Skip configuring Key Vault SSL bundles because {}", "'spring.ssl.bundle.azure-keyvault' "
75+
+ "is empty.");
76+
return;
77+
}
78+
79+
final AtomicBoolean providerConfigured = new AtomicBoolean(false);
80+
sslBundles.forEach((bundleName, bundle) -> {
81+
boolean hasAnyCertConfiguredForTruststore = hasAnyCertConfigured(jcaVaults, bundle.getTruststore());
82+
boolean hasAnyCertConfiguredForKeyStore = hasAnyCertConfigured(jcaVaults, bundle.getKeystore());
83+
boolean anyCertConfigured = hasAnyCertConfiguredForTruststore || hasAnyCertConfiguredForKeyStore;
84+
if (!anyCertConfigured) {
85+
LOGGER.debug("Skip configuring Key Vault SSL bundle '{}'. Consider configuring 'keyvault-ref', "
86+
+ "'certificate-paths.custom' or 'certificate-paths.well-known' properties of the keystore or "
87+
+ "truststore.", bundleName);
88+
return;
89+
}
90+
91+
KeyStore keyVaultKeyStore = initilizeKeyVaultKeyStore("keystore",
92+
bundleName,
93+
hasAnyCertConfiguredForKeyStore,
94+
providerConfigured,
95+
jcaVaults.get(bundle.getKeystore().getKeyvaultRef()),
96+
bundle.getKeystore());
97+
98+
KeyStore keyVaultTruststore = initilizeKeyVaultKeyStore("truststore",
99+
bundleName,
100+
hasAnyCertConfiguredForTruststore,
101+
providerConfigured,
102+
jcaVaults.get(bundle.getTruststore().getKeyvaultRef()),
103+
bundle.getTruststore());
104+
105+
SslStoreBundle sslStoreBundle = SslStoreBundle.of(keyVaultKeyStore, null, keyVaultTruststore);
106+
107+
SslBundleKey sslBundleKey = Optional.ofNullable(bundle.getKey())
108+
.map(k -> SslBundleKey.of(k.getPassword(), k.getAlias()))
109+
.orElse(SslBundleKey.NONE);
110+
111+
SslOptions sslOptions = Optional.ofNullable(bundle.getOptions())
112+
.map(o -> SslOptions.of(o.getCiphers(), o.getEnabledProtocols()))
113+
.orElse(SslOptions.NONE);
114+
115+
SslBundle sslBundle = SslBundle.of(sslStoreBundle, sslBundleKey,
116+
sslOptions,
117+
bundle.getProtocol(),
118+
new KeyVaultSslManagerBundle(sslStoreBundle, sslBundleKey, bundle.isForClientAuth()));
119+
120+
registry.registerBundle(bundleName, sslBundle);
121+
122+
LOGGER.debug("Registered Azure Key Vault SSL bundle '{}'.", bundleName);
123+
});
124+
}
125+
126+
private KeyStore initilizeKeyVaultKeyStore(String storeName,
127+
String bundleName,
128+
boolean anyCertConfigured,
129+
AtomicBoolean providerConfigured,
130+
AzureKeyVaultJcaProperties.JcaVaultProperties jcaVaultProperties,
131+
AzureKeyVaultSslBundleProperties.KeyStoreProperties keyStoreProperties) {
132+
if (!anyCertConfigured) {
133+
LOGGER.debug("The {} parameter of Key Vault SSL bundle '{}' is null.", storeName, bundleName);
134+
return null;
135+
}
136+
137+
configureJcaKeyStoreSystemProperties(jcaVaultProperties, keyStoreProperties, resourceLoader);
138+
if (providerConfigured.compareAndSet(false, true)) {
139+
Security.removeProvider(KeyVaultJcaProvider.PROVIDER_NAME);
140+
Security.insertProviderAt(new KeyVaultJcaProvider(), 1);
141+
}
142+
KeyStore azureKeyVaultKeyStore;
143+
try {
144+
if (hasEmbeddedTomcat()) {
145+
// DKS (Domain Key Store) type key store can act as a single logical key store and support key stores of various
146+
// types (JKS - Java Key Store, pkcs12) within a single domain. When configuring the Tomcat SSL context, if the
147+
// KeyStore is not of type DKS, the final key store will be reinitialized and loaded, see source code from
148+
// https://github.com/apache/tomcat/blob/cab38e5b9c4f498336f716afd1bf4161adedd71d/java/org/apache/tomcat/util/net/SSLUtilBase.java#L393~L403,
149+
// which will result in the KeyManager being used not being wrapped by the JSSEKeyManager provided by Tomcat, see source code from
150+
// https://github.com/apache/tomcat/blob/cab38e5b9c4f498336f716afd1bf4161adedd71d/java/org/apache/tomcat/util/net/SSLUtilBase.java#L424,
151+
// so JSSEKeyManager#chooseEngineServerAlias does not delegate KeyVaultKeyManager.chooseEngineServerAlias, resulting in a null return value.
152+
azureKeyVaultKeyStore = KeyStore.getInstance("DKS", KeyVaultJcaProvider.PROVIDER_NAME);
153+
} else {
154+
azureKeyVaultKeyStore = KeyStore.getInstance(KeyVaultJcaProvider.PROVIDER_NAME);
155+
}
156+
azureKeyVaultKeyStore.load(null);
157+
} catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException | NoSuchProviderException e) {
158+
throw new RuntimeException("Failed to load Key Vault " + storeName + " for SSL bundle '" + bundleName + "'", e);
159+
}
160+
return azureKeyVaultKeyStore;
161+
}
162+
163+
private static boolean hasKeyVaultJcaOnClasspath() {
164+
return ClassUtils.isPresent("com.azure.security.keyvault.jca.KeyVaultJcaProvider",
165+
AzureKeyVaultSslBundleRegistrar.class.getClassLoader());
166+
}
167+
168+
private static boolean hasAnyCertConfigured(Map<String, AzureKeyVaultJcaProperties.JcaVaultProperties> jcaVaults,
169+
AzureKeyVaultSslBundleProperties.KeyStoreProperties keyStoreProperties) {
170+
AzureKeyVaultSslBundleProperties.CertificatePathsProperties certificatePaths = keyStoreProperties.getCertificatePaths();
171+
String keyvaultRef = keyStoreProperties.getKeyvaultRef();
172+
boolean localCertConfigured = StringUtils.hasText(certificatePaths.getWellKnown()) || StringUtils.hasText(certificatePaths.getCustom());
173+
boolean keyVaultRefConfigured = StringUtils.hasText(keyvaultRef) && jcaVaults.get(keyvaultRef) != null;
174+
return localCertConfigured || keyVaultRefConfigured;
175+
}
176+
177+
private static boolean hasEmbeddedTomcat() {
178+
try {
179+
Class.forName("org.apache.tomcat.InstanceManager");
180+
return true;
181+
} catch (ClassNotFoundException ex) {
182+
return false;
183+
}
184+
}
185+
186+
private static void configureJcaKeyStoreSystemProperties(AzureKeyVaultJcaProperties.JcaVaultProperties jcaVaultProperties,
187+
AzureKeyVaultSslBundleProperties.KeyStoreProperties keyStoreProperties,
188+
ResourceLoader resourceLoader) {
189+
PropertyMapper pm = new PropertyMapper();
190+
clearJcaSystemProperties();
191+
if (jcaVaultProperties != null) {
192+
pm.from(jcaVaultProperties.getEndpoint())
193+
.when(StringUtils::hasText)
194+
.to(v -> System.setProperty("azure.keyvault.uri", v));
195+
pm.from(jcaVaultProperties.getProfile().getTenantId())
196+
.when(StringUtils::hasText)
197+
.to(v -> System.setProperty("azure.keyvault.tenant-id", v));
198+
pm.from(jcaVaultProperties.getCredential().getClientId())
199+
.when(StringUtils::hasText)
200+
.to(v -> System.setProperty("azure.keyvault.client-id", v));
201+
pm.from(jcaVaultProperties.getCredential().getClientSecret())
202+
.when(StringUtils::hasText)
203+
.to(v -> System.setProperty("azure.keyvault.client-secret", v));
204+
pm.from(jcaVaultProperties.getCredential().isManagedIdentityEnabled())
205+
.whenTrue()
206+
// should put the client id to the managed-identity property
207+
.to(v -> System.setProperty("azure.keyvault.managed-identity", jcaVaultProperties.getCredential().getClientId()));
208+
}
209+
210+
pm.from(keyStoreProperties.getCertificatesRefreshInterval())
211+
.when(Objects::nonNull)
212+
.to(v -> System.setProperty("azure.keyvault.jca.certificates-refresh-interval", String.valueOf(v.toMillis())));
213+
pm.from(keyStoreProperties.isRefreshCertificatesWhenHaveUntrustedCertificate())
214+
.to(v -> System.setProperty("azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate", Boolean.toString(v)));
215+
216+
pm.from(keyStoreProperties.getCertificatePaths().getWellKnown())
217+
.to(v -> resolvePath(resourceLoader, v).ifPresent(path -> System.setProperty("azure.cert-path.well-known", path)));
218+
pm.from(keyStoreProperties.getCertificatePaths().getCustom())
219+
.to(v -> resolvePath(resourceLoader, v).ifPresent(path -> System.setProperty("azure.cert-path.custom", path)));
220+
}
221+
222+
private static void clearJcaSystemProperties() {
223+
Arrays.stream(JCA_SYSTEM_PROPERTY_KEYS).forEach(System::clearProperty);
224+
}
225+
226+
private static Optional<String> resolvePath(ResourceLoader resourceLoader, String path) {
227+
return Optional.ofNullable(path)
228+
.filter(p -> p.startsWith("classpath:") || p.startsWith("file:"))
229+
.map(resourceLoader::getResource)
230+
.map(res -> {
231+
try {
232+
return res.getFile().getAbsolutePath();
233+
} catch (IOException e) {
234+
throw new RuntimeException("Failed to load the certificate path '" + path + "'", e);
235+
}
236+
});
237+
}
238+
239+
@Override
240+
public void setResourceLoader(ResourceLoader resourceLoader) {
241+
this.resourceLoader = resourceLoader;
242+
}
243+
}

0 commit comments

Comments
 (0)