Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6c8ec90
Add configured certificate filtering and lazy loading in KeyVault JCA
rujche Jul 10, 2026
86621f2
Add PR link to keyvault-jca changelog entry
rujche Jul 10, 2026
f4371a4
Address PR review comments for alias refresh and thread safety
rujche Jul 10, 2026
5507d46
Address additional PR review feedback on thread safety and refresh be…
rujche Jul 10, 2026
cc494aa
Make alias lookup use snapshots for thread-safe iteration
rujche Jul 10, 2026
b3cd9d5
Ensure refreshed aliases list remains mutable
rujche Jul 10, 2026
7875700
Restore key vault precedence over classpath in keystore lookups
rujche Jul 10, 2026
a343059
Refine lazy key vault loads and remove brittle reflection test
rujche Jul 10, 2026
a6fbf69
Fix import order and remove unused import
rujche Jul 10, 2026
9d74fac
Strengthen test isolation and list API assertions
rujche Jul 10, 2026
162a828
Retry certificate load when Key Vault returns null
rujche Jul 10, 2026
d588c4d
Suppress GoodLoggingCheck and fix synchronization consistency
rujche Jul 10, 2026
b4aae28
Retry key and chain loads after transient failures
rujche Jul 10, 2026
c553796
Use lazy warning logs and remove extra suppression
rujche Jul 10, 2026
c73eb95
Reset key vault certificate cache when client changes
rujche Jul 10, 2026
72887a0
Make key vault client update atomic and reuse cache reset
rujche Jul 10, 2026
ce99b3e
Ignore null configured certificate aliases
rujche Jul 10, 2026
7ec6baf
Use allCertificates precedence in KeyVaultKeyStore lookups
rujche Jul 10, 2026
f34921f
Add GoodLoggingCheck suppression for KeyVaultCertificates
rujche Jul 10, 2026
9506d26
Guard null certificate alias lookup and clarify refresh docs
rujche Jul 10, 2026
35885e3
Reduce lock contention in lazy certificate loading
rujche Jul 13, 2026
e67607c
Add certificates-filter-patterns property for alias regex filtering
rujche Jul 13, 2026
8cddbf0
Simplify certificates filter pattern README wording
rujche Jul 13, 2026
f1ce384
Align changelog filter pattern wording with README
rujche Jul 13, 2026
470b184
Rename certificate alias filter property
rujche Jul 13, 2026
524655e
Document default alias filter behavior
rujche Jul 13, 2026
7662126
Reorder default filter behavior sentence in README
rujche Jul 13, 2026
60907c2
Fix stale-client publish races in lazy loaders
rujche Jul 13, 2026
d75b0e2
Regenerate linting suppressions for keyvault-jca
rujche Jul 13, 2026
b3120a8
Address review feedback on alias logging and filter parsing
rujche Jul 13, 2026
91a495c
Address review feedback on filter pattern normalization
rujche Jul 13, 2026
a407a55
Refine alias filter error message and state usage
rujche Jul 13, 2026
31c94e2
Harden certificate lookup null safety and key map exposure
rujche Jul 13, 2026
fb84762
Defensively copy exposed certificate collections
rujche Jul 13, 2026
e450841
Refine KeyVault certificate refresh locking
rujche Jul 13, 2026
7c23284
Avoid duplicate certificate chain cloning
rujche Jul 13, 2026
207c42d
Fix out-of-order alias writes on concurrent refresh
moarychan Aug 3, 2026
4479907
Configure each alias filter as its own property
moarychan Aug 3, 2026
15c6dcb
Merge remote-tracking branch 'origin/main' into pr-49774
moarychan Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## 2.13.0-beta.1 (Unreleased)

### Features Added
- Added lazy loading for Key Vault certificate details in the JCA keystore. Certificate details are now loaded by alias when requested, avoiding unnecessary reads for unconfigured certificates. ([#49774](https://github.com/Azure/azure-sdk-for-java/pull/49774))
- Added support for `azure.keyvault.jca.certificate-alias-filter-pattern` to filter Key Vault certificate aliases with include/exclude regex patterns. Include patterns are configured directly and exclude patterns are prefixed with `!`. Configure more than one filter by appending a suffix to the property name, such as `azure.keyvault.jca.certificate-alias-filter-pattern.1`, so that a pattern can contain any character. If no such property is configured, alias filtering is disabled and all discovered Key Vault aliases remain eligible for lazy loading. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Added support for `azure.keyvault.jca.certificate-alias-filter-pattern` to filter Key Vault certificate aliases with include/exclude regex patterns. Include patterns are configured directly and exclude patterns are prefixed with `!`. Configure more than one filter by appending a suffix to the property name, such as `azure.keyvault.jca.certificate-alias-filter-pattern.1`, so that a pattern can contain any character. If no such property is configured, alias filtering is disabled and all discovered Key Vault aliases remain eligible for lazy loading. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487))
- Added support for `azure.keyvault.jca.certificate-alias-filter-pattern` to filter Key Vault certificate aliases with inclusion/exclusion regex patterns. Inclusion patterns are configured directly and exclusion patterns are prefixed with `!`. Configure more than one filter by appending a suffix to the property name, such as `azure.keyvault.jca.certificate-alias-filter-pattern.1`. A pattern can contain any character as long as the regex is correct. If no filter property is configured, alias filtering is disabled and all discovered Key Vault aliases remain eligible for lazy loading. ([#39487](https://github.com/Azure/azure-sdk-for-java/issues/39487))


### Breaking Changes

Expand Down
25 changes: 25 additions & 0 deletions sdk/keyvault/azure-security-keyvault-jca/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ The JCA library supports configuring the following options:
* `azure.keyvault.jca.refresh-certificates-when-have-un-trust-certificate`: Indicates whether to refresh certificates when have untrusted certificate.
* `azure.keyvault.jca.certificates-refresh-interval`: The refresh interval time.
* `azure.keyvault.jca.certificates-refresh-interval-in-ms`: The refresh interval time.
Comment thread
rujche marked this conversation as resolved.
* `azure.keyvault.jca.certificate-alias-filter-pattern`: A regex that filters which Key Vault certificate aliases are eligible for lazy loading. Append a suffix to the property name to configure more than one filter, for example `azure.keyvault.jca.certificate-alias-filter-pattern.1` or `azure.keyvault.jca.certificate-alias-filter-pattern.prod`. If no such property is configured, all discovered Key Vault aliases are eligible for lazy loading. See "Filtering Key Vault certificate aliases" below.
* `azure.keyvault.disable-challenge-resource-verification`: Indicates whether to disable verification that the authentication challenge resource matches the Key Vault or Managed HSM domain.

You can configure these properties using:
Expand All @@ -152,6 +153,30 @@ or as a JVM argument:
-Dazure.keyvault.uri=<your-azure-keyvault-uri>
```

#### Filtering Key Vault certificate aliases

Each filter is configured as its own property, so no delimiter is required and a pattern may contain any character:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Each filter is configured as its own property, so no delimiter is required and a pattern may contain any character:
Each filter is configured as its own property, so no delimiter is required and a pattern may contain any character. Filters use Java-based regex:


```shell
-Dazure.keyvault.jca.certificate-alias-filter-pattern.1='^prod-.*'
-Dazure.keyvault.jca.certificate-alias-filter-pattern.2='^cert-\d{1,5}$'
-Dazure.keyvault.jca.certificate-alias-filter-pattern.exclude-old='!.*-old$'
Comment on lines +161 to +163

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small suggestion to make the following points that talk about case sensitivity easier to read:

Suggested change
-Dazure.keyvault.jca.certificate-alias-filter-pattern.1='^prod-.*'
-Dazure.keyvault.jca.certificate-alias-filter-pattern.2='^cert-\d{1,5}$'
-Dazure.keyvault.jca.certificate-alias-filter-pattern.exclude-old='!.*-old$'
-Dazure.keyvault.jca.certificate-alias-filter-pattern.1='^cert-.*'
-Dazure.keyvault.jca.certificate-alias-filter-pattern.prod='^prod-\d{1,5}$'
-Dazure.keyvault.jca.certificate-alias-filter-pattern.exclude-old='!.*-old$'

```

* Use an include pattern directly and an exclude pattern with a `!` prefix.
* A suffix can be a number or a string. It only keeps the property names unique and does not affect evaluation, so the filters are unordered. Property names are case-sensitive, which means `.prod` and `.PROD` are two different filters.
* Patterns use full-alias matching (`Pattern.matcher(alias).matches()`).
* An alias is loaded only if it matches at least one include pattern, or if no include pattern is configured, and matches no exclude pattern.
* An invalid pattern fails fast with an `IllegalArgumentException` that names the offending pattern.
Comment on lines +166 to +170

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Use an include pattern directly and an exclude pattern with a `!` prefix.
* A suffix can be a number or a string. It only keeps the property names unique and does not affect evaluation, so the filters are unordered. Property names are case-sensitive, which means `.prod` and `.PROD` are two different filters.
* Patterns use full-alias matching (`Pattern.matcher(alias).matches()`).
* An alias is loaded only if it matches at least one include pattern, or if no include pattern is configured, and matches no exclude pattern.
* An invalid pattern fails fast with an `IllegalArgumentException` that names the offending pattern.
* Inclusion patterns are used directly and exclusion patterns start with a `!` prefix.
* A suffix for the property name can be a number or a string; it is used to keep the property names unique and does not affect evaluation. The filters are unordered. Property names are case-sensitive, which means `.prod` and `.PROD` are two different filters.
* Patterns use full-alias matching (`Pattern.matcher(alias).matches()`).
* An alias is loaded only if it matches at least one inclusion pattern, or if no inclusion pattern is configured, and matches no exclusion pattern.
* An invalid pattern fails fast with an `IllegalArgumentException` that names the offending pattern.


Quote the value as required by your shell, otherwise characters such as `^` and `\` can be altered before the JVM receives them:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Quote the value as required by your shell, otherwise characters such as `^` and `\` can be altered before the JVM receives them:
Quote the filter value as required by your shell, otherwise characters such as `^` and `\` can be altered before the JVM receives them:


| Shell | Example |
| --- | --- |
| Bash, including Git Bash | `-Dazure.keyvault.jca.certificate-alias-filter-pattern.1='^prod-.*'` |
| PowerShell | `'-Dazure.keyvault.jca.certificate-alias-filter-pattern.1=^prod-.*'` |
| Windows `cmd.exe` | `"-Dazure.keyvault.jca.certificate-alias-filter-pattern.1=^^prod-.*"` |

### SSL/TLS
#### Server side SSL
If you are looking to integrate the JCA provider to create an SSLServerSocket see the example below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
<suppress files="com.azure.security.keyvault.jca.implementation.KeyVaultClient.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.certificates.ClasspathCertificates.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.certificates.JreCertificates.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.certificates.KeyVaultCertificates.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.certificates.SpecificPathCertificates.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
Comment thread
rujche marked this conversation as resolved.
Comment thread
rujche marked this conversation as resolved.
<suppress files="com.azure.security.keyvault.jca.implementation.utils.AccessTokenUtil.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.utils.HttpUtil.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.utils.JsonConverterUtil.java" checks="io.clientcore.linting.extensions.checkstyle.checks.GoodLoggingCheck" />
<suppress files="com.azure.security.keyvault.jca.KeyVaultTrustManager.java" checks="io.clientcore.linting.extensions.checkstyle.checks.ThrowFromClientLoggerCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.KeyVaultClient.java" checks="io.clientcore.linting.extensions.checkstyle.checks.ThrowFromClientLoggerCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.certificates.KeyVaultCertificates.java" checks="io.clientcore.linting.extensions.checkstyle.checks.ThrowFromClientLoggerCheck" />
Comment thread
rujche marked this conversation as resolved.
<suppress files="com.azure.security.keyvault.jca.implementation.signature.AbstractKeyVaultKeylessSignature.java" checks="io.clientcore.linting.extensions.checkstyle.checks.ThrowFromClientLoggerCheck" />
Comment thread
rujche marked this conversation as resolved.
<suppress files="com.azure.security.keyvault.jca.implementation.signature.KeyVaultKeylessRsaSsaPssSignature.java" checks="io.clientcore.linting.extensions.checkstyle.checks.ThrowFromClientLoggerCheck" />
<suppress files="com.azure.security.keyvault.jca.implementation.utils.HttpUtil.java" checks="io.clientcore.linting.extensions.checkstyle.checks.UseCaughtExceptionCauseCheck" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.logging.Level.FINE;
Expand All @@ -56,6 +59,9 @@ public final class KeyVaultKeyStore extends KeyStoreSpi {
*/
private static final Logger LOGGER = Logger.getLogger(KeyVaultKeyStore.class.getName());

static final String CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY
= "azure.keyvault.jca.certificate-alias-filter-pattern";

/**
* Stores the Jre key store certificates.
*/
Expand Down Expand Up @@ -146,9 +152,10 @@ public KeyVaultKeyStore() {
customCertificates = SpecificPathCertificates.getSpecificPathCertificates(customPath);
LOGGER.log(FINE, String.format("Loaded custom certificates: %s.", customCertificates.getAliases()));

keyVaultCertificates = new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret,
managedIdentity, accessToken, disableChallengeResourceVerification);
LOGGER.log(FINE, String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases()));
keyVaultCertificates
= new KeyVaultCertificates(refreshInterval, keyVaultUri, tenantId, clientId, clientSecret, managedIdentity,
accessToken, disableChallengeResourceVerification, getKeyVaultCertificateAliasFilterPatterns());
LOGGER.log(FINE, () -> String.format("Loaded Key Vault certificates: %s.", keyVaultCertificates.getAliases()));

classpathCertificates = new ClasspathCertificates();
LOGGER.log(FINE, String.format("Loaded classpath certificates: %s.", classpathCertificates.getAliases()));
Expand All @@ -168,6 +175,22 @@ Long getRefreshInterval() {
.orElse(0L);
}

Set<String> getKeyVaultCertificateAliasFilterPatterns() {
// Each pattern gets its own property because any delimiter character can be part of a regex.
Properties properties = System.getProperties();
String suffixedPropertyPrefix = CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY + ".";

return properties.stringPropertyNames()
.stream()
.filter(name -> name.equals(CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY)
|| name.startsWith(suffixedPropertyPrefix))
.map(properties::getProperty)
.filter(Objects::nonNull)
.map(String::trim)
.filter(pattern -> !pattern.isEmpty())
.collect(Collectors.toSet());
}

/**
* get key vault key store by system property
*
Expand Down Expand Up @@ -254,16 +277,22 @@ public boolean engineEntryInstanceOf(String alias, Class<? extends KeyStore.Entr
*/
@Override
public Certificate engineGetCertificate(String alias) {
Certificate certificate = allCertificates.stream()
.map(AzureCertificates::getCertificates)
.filter(a -> a.containsKey(alias))
.findFirst()
.map(certificates -> certificates.get(alias))
.orElse(null);
Certificate certificate = null;
for (AzureCertificates certificatesSource : allCertificates) {
if (certificatesSource instanceof KeyVaultCertificates) {
certificate = ((KeyVaultCertificates) certificatesSource).getCertificate(alias);
} else {
certificate = certificatesSource.getCertificates().get(alias);
}

if (certificate != null) {
break;
}
}

if (refreshCertificatesWhenHaveUnTrustCertificate && certificate == null) {
keyVaultCertificates.refreshCertificates();
certificate = keyVaultCertificates.getCertificates().get(alias);
certificate = keyVaultCertificates.getCertificate(alias);
}
Comment thread
rujche marked this conversation as resolved.

return certificate;
Expand All @@ -283,7 +312,7 @@ public String engineGetCertificateAlias(Certificate cert) {
List<String> aliasList = getAllAliases();
for (String candidateAlias : aliasList) {
Certificate certificate = engineGetCertificate(candidateAlias);
if (certificate.equals(cert)) {
if (certificate != null && certificate.equals(cert)) {
alias = candidateAlias;
break;
}
Expand All @@ -307,16 +336,23 @@ public String engineGetCertificateAlias(Certificate cert) {
*/
@Override
public Certificate[] engineGetCertificateChain(String alias) {
Certificate[] certificates = allCertificates.stream()
.map(AzureCertificates::getCertificateChains)
.filter(Objects::nonNull)
.filter(a -> a.containsKey(alias))
.findFirst()
.map(m -> m.get(alias))
.orElse(null);
Certificate[] certificates = null;
for (AzureCertificates certificatesSource : allCertificates) {
if (certificatesSource instanceof KeyVaultCertificates) {
certificates = ((KeyVaultCertificates) certificatesSource).getCertificateChain(alias);
} else {
Certificate[] certificateChain = certificatesSource.getCertificateChains().get(alias);
certificates = certificateChain == null ? null : certificateChain.clone();
}

if (certificates != null) {
break;
}
}

if (refreshCertificatesWhenHaveUnTrustCertificate && certificates == null) {
keyVaultCertificates.refreshCertificates();
return keyVaultCertificates.getCertificateChains().get(alias);
return keyVaultCertificates.getCertificateChain(alias);
}
return certificates;
}
Comment thread
rujche marked this conversation as resolved.
Expand Down Expand Up @@ -358,12 +394,20 @@ public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter
*/
@Override
public Key engineGetKey(String alias, char[] password) {
return allCertificates.stream()
.map(AzureCertificates::getCertificateKeys)
.filter(a -> a.containsKey(alias))
.findFirst()
.map(certificateKeys -> certificateKeys.get(alias))
.orElse(null);
Key key = null;
for (AzureCertificates certificatesSource : allCertificates) {
if (certificatesSource instanceof KeyVaultCertificates) {
key = ((KeyVaultCertificates) certificatesSource).getCertificateKey(alias);
} else {
key = certificatesSource.getCertificateKeys().get(alias);
}

if (key != null) {
break;
}
}

return key;
}

/**
Expand Down
Loading
Loading