From ad1c98a10bce6389b6a0cfadbc42408a897b3bf6 Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Tue, 4 Aug 2026 00:20:49 +0530 Subject: [PATCH] feat(spring): support Key Vault certificate alias filters Signed-off-by: Arnab Nandy --- sdk/spring/CHANGELOG.md | 4 ++ .../CHANGELOG.md | 3 +- .../jca/AzureKeyVaultSslBundleRegistrar.java | 5 ++ .../AzureKeyVaultSslBundleProperties.java | 12 +++++ ...zureKeyVaultJcaAutoConfigurationTests.java | 10 +++- .../AzureKeyVaultSslBundleRegistrarTests.java | 47 ++++++++++++++++++- 6 files changed, 78 insertions(+), 3 deletions(-) diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 527f8962a513..2f8e1e126860 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -3,6 +3,10 @@ ### Spring Cloud Azure Autoconfigure +#### Features Added + +- Added `spring.ssl.bundle.keyvault..keystore.certificate-alias-filter-patterns` and `spring.ssl.bundle.keyvault..truststore.certificate-alias-filter-patterns` configuration. The patterns are passed to the Key Vault JCA provider to limit which certificate aliases are loaded. ([#50013](https://github.com/Azure/azure-sdk-for-java/issues/50013)) + #### Bugs Fixed - Fixed Service Bus JMS listener containers using `JmsPoolConnectionFactory` when both `spring.jms.servicebus.pool.enabled=true` and `spring.jms.cache.enabled=false`. The sender continues to use `JmsPoolConnectionFactory`, while listener containers now use a dedicated `ServiceBusJmsConnectionFactory`, enabling topic subscriptions on the Standard tier. ([#49308](https://github.com/Azure/azure-sdk-for-java/issues/49308)) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md b/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md index 5114a28e73ae..bcd438b35163 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added `spring.ssl.bundle.keyvault..keystore.certificate-alias-filter-patterns` and `spring.ssl.bundle.keyvault..truststore.certificate-alias-filter-patterns` configuration. The patterns are passed to the Key Vault JCA provider to limit which certificate aliases are loaded. ([#50013](https://github.com/Azure/azure-sdk-for-java/issues/50013)) + ### Breaking Changes ### Bugs Fixed @@ -253,4 +255,3 @@ Please refer to [spring/CHANGELOG.md](https://github.com/Azure/azure-sdk-for-jav ## 4.0.0-beta.2 (2021-11-22) Please refer to [spring/CHANGELOG.md](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/CHANGELOG.md#400-beta2-2021-11-22) for more details. - diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrar.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrar.java index 10521f66f533..ddfe8b456f5f 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrar.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrar.java @@ -51,6 +51,7 @@ public class AzureKeyVaultSslBundleRegistrar implements SslBundleRegistrar, Reso "azure.keyvault.client-secret", "azure.keyvault.managed-identity", "azure.keyvault.jca.certificates-refresh-interval", + "azure.keyvault.jca.certificate-alias-filter-patterns", "azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate", "azure.cert-path.well-known", "azure.cert-path.custom" @@ -209,6 +210,10 @@ private static void configureJcaKeyStoreSystemProperties(AzureKeyVaultJcaPropert pm.from(keyStoreProperties.getCertificatesRefreshInterval()) .when(Objects::nonNull) .to(v -> System.setProperty("azure.keyvault.jca.certificates-refresh-interval", String.valueOf(v.toMillis()))); + pm.from(keyStoreProperties.getCertificateAliasFilterPatterns()) + .when(patterns -> !patterns.isEmpty()) + .to(patterns -> System.setProperty("azure.keyvault.jca.certificate-alias-filter-patterns", + String.join(",", patterns))); pm.from(keyStoreProperties.isRefreshCertificatesWhenHaveUntrustedCertificate()) .to(v -> System.setProperty("azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate", Boolean.toString(v))); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/properties/AzureKeyVaultSslBundleProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/properties/AzureKeyVaultSslBundleProperties.java index bf32fcd226b0..e61c3a2e9cd5 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/properties/AzureKeyVaultSslBundleProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/properties/AzureKeyVaultSslBundleProperties.java @@ -8,7 +8,9 @@ import org.springframework.boot.context.properties.NestedConfigurationProperty; import java.time.Duration; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -76,6 +78,12 @@ public static class KeyStoreProperties { * Time interval to refresh all Key Vault certificate. */ private Duration certificatesRefreshInterval; + /** + * Key Vault certificate alias filter patterns. Include patterns are configured as regular expressions and + * exclude patterns are prefixed with {@code !}. If no patterns are configured, all certificate aliases are + * loaded. + */ + private final List certificateAliasFilterPatterns = new ArrayList<>(); @NestedConfigurationProperty private final CertificatePathsProperties certificatePaths = new CertificatePathsProperties(); @@ -104,6 +112,10 @@ public void setCertificatesRefreshInterval(Duration certificatesRefreshInterval) this.certificatesRefreshInterval = certificatesRefreshInterval; } + public List getCertificateAliasFilterPatterns() { + return certificateAliasFilterPatterns; + } + public CertificatePathsProperties getCertificatePaths() { return certificatePaths; } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultJcaAutoConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultJcaAutoConfigurationTests.java index e40ebe8b6a32..bb26939cac1c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultJcaAutoConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultJcaAutoConfigurationTests.java @@ -52,8 +52,12 @@ void keyVaultJca() { "spring.cloud.azure.keyvault.jca.vaults.kv2.endpoint=" + String.format(ENDPOINT, "test2"), "spring.ssl.bundle.keyvault.testBundle1.truststore.certificate-paths.custom=classpath:keyvault/certificate-paths/custom", "spring.ssl.bundle.keyvault.testBundle2.truststore.keyvault-ref=kv2", + "spring.ssl.bundle.keyvault.testBundle2.truststore.certificate-alias-filter-patterns[0]=^prod-.*", + "spring.ssl.bundle.keyvault.testBundle2.truststore.certificate-alias-filter-patterns[1]=!^prod-deprecated$", "spring.ssl.bundle.keyvault.testBundle3.truststore.keyvault-ref=kv1", - "spring.ssl.bundle.keyvault.testBundle3.keystore.keyvault-ref=kv2" + "spring.ssl.bundle.keyvault.testBundle3.keystore.keyvault-ref=kv2", + "spring.ssl.bundle.keyvault.testBundle3.keystore.certificate-alias-filter-patterns[0]=client-cert", + "spring.ssl.bundle.keyvault.testBundle3.keystore.certificate-alias-filter-patterns[1]=!old-client-cert" ) .run(context -> { assertThat(context).hasSingleBean(AzureKeyVaultJcaAutoConfiguration.class); @@ -71,8 +75,12 @@ void keyVaultJca() { assertThat(sslBundlesProperties.getKeyvault()).hasSize(3); assertThat(sslBundlesProperties.getKeyvault().get("testBundle1").getTruststore().getCertificatePaths().getCustom()).isEqualTo("classpath:keyvault/certificate-paths/custom"); assertThat(sslBundlesProperties.getKeyvault().get("testBundle2").getTruststore().getKeyvaultRef()).isEqualTo("kv2"); + assertThat(sslBundlesProperties.getKeyvault().get("testBundle2").getTruststore() + .getCertificateAliasFilterPatterns()).containsExactly("^prod-.*", "!^prod-deprecated$"); assertThat(sslBundlesProperties.getKeyvault().get("testBundle3").getTruststore().getKeyvaultRef()).isEqualTo("kv1"); assertThat(sslBundlesProperties.getKeyvault().get("testBundle3").getKeystore().getKeyvaultRef()).isEqualTo("kv2"); + assertThat(sslBundlesProperties.getKeyvault().get("testBundle3").getKeystore() + .getCertificateAliasFilterPatterns()).containsExactly("client-cert", "!old-client-cert"); }); } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java index 2d1a99583046..b5eb21c3f9b6 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/keyvault/jca/AzureKeyVaultSslBundleRegistrarTests.java @@ -22,8 +22,11 @@ import java.security.KeyStore; import java.security.Security; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -95,7 +98,8 @@ void notConfigureEndpointOrSslBundleProperties(CapturedOutput capturedOutput) { void registerKeyVaultSslBundle(CapturedOutput capturedOutput) { AzureKeyVaultJcaProperties jcaProperties = new AzureKeyVaultJcaProperties(); AzureKeyVaultSslBundleProperties sslBundleProperties = new AzureKeyVaultSslBundleProperties(); - AzureKeyVaultSslBundleRegistrar registrar = new AzureKeyVaultSslBundleRegistrar(jcaProperties, sslBundleProperties); + AzureKeyVaultSslBundleRegistrar registrar + = new AzureKeyVaultSslBundleRegistrar(jcaProperties, sslBundleProperties); registrar.setResourceLoader(new DefaultResourceLoader()); SslBundleRegistry registry = Mockito.mock(SslBundleRegistry.class); @@ -209,6 +213,47 @@ void registerMultipleSslBundles(CapturedOutput capturedOutput) { } } + @Test + void configureCertificateAliasFilterPatterns() { + AzureKeyVaultJcaProperties jcaProperties = new AzureKeyVaultJcaProperties(); + AzureKeyVaultSslBundleProperties sslBundleProperties = new AzureKeyVaultSslBundleProperties(); + AzureKeyVaultSslBundleRegistrar registrar = new AzureKeyVaultSslBundleRegistrar(jcaProperties, sslBundleProperties); + registrar.setResourceLoader(new DefaultResourceLoader()); + + try (MockedStatic keyStoreMockedStatic = mockStatic(KeyStore.class)) { + KeyStore keyStore = Mockito.mock(KeyStore.class); + List configuredFilterPatterns = new ArrayList<>(); + keyStoreMockedStatic.when(() -> KeyStore.getInstance(KeyVaultJcaProvider.PROVIDER_NAME)) + .thenAnswer(invocation -> { + configuredFilterPatterns.add( + System.getProperty("azure.keyvault.jca.certificate-alias-filter-patterns")); + return keyStore; + }); + + AzureKeyVaultJcaProperties.JcaVaultProperties vaultProperties + = new AzureKeyVaultJcaProperties.JcaVaultProperties(); + vaultProperties.setEndpoint("https://test.vault.azure.net/"); + jcaProperties.getVaults().put("keyvault1", vaultProperties); + + AzureKeyVaultSslBundleProperties.KeyVaultSslBundleProperties bundleProperties + = new AzureKeyVaultSslBundleProperties.KeyVaultSslBundleProperties(); + bundleProperties.getKeystore().setKeyvaultRef("keyvault1"); + bundleProperties.getKeystore().getCertificateAliasFilterPatterns() + .addAll(Arrays.asList("^prod-.*", "!^prod-deprecated$")); + bundleProperties.getTruststore().setKeyvaultRef("keyvault1"); + bundleProperties.getTruststore().getCertificateAliasFilterPatterns() + .addAll(Arrays.asList("^partner-.*", "!^partner-deprecated$")); + sslBundleProperties.getKeyvault().put("testBundle", bundleProperties); + + registrar.registerBundles(Mockito.mock(SslBundleRegistry.class)); + + assertThat(configuredFilterPatterns) + .containsExactly("^prod-.*,!^prod-deprecated$", "^partner-.*,!^partner-deprecated$"); + } finally { + System.clearProperty("azure.keyvault.jca.certificate-alias-filter-patterns"); + } + } + @Test void keyVaultProviderNotInsertedAtHighestPriority() { AzureKeyVaultJcaProperties jcaProperties = new AzureKeyVaultJcaProperties();