Skip to content

KeyVault JCA: lazy-load certificate material and add alias regex filtering - #49774

Open
Rujun Chen (rujche) wants to merge 39 commits into
mainfrom
rujche/main/Just-load-configured-certificates-in-azure-security-keyvault-jca
Open

KeyVault JCA: lazy-load certificate material and add alias regex filtering#49774
Rujun Chen (rujche) wants to merge 39 commits into
mainfrom
rujche/main/Just-load-configured-certificates-in-azure-security-keyvault-jca

Conversation

@rujche

@rujche Rujun Chen (rujche) commented Jul 10, 2026

Copy link
Copy Markdown
Member

Description

This PR completes #39487 in azure-security-keyvault-jca and also finalizes the follow-up lazy-loading/thread-safety refinements requested during review.

What changed

  • sdk/keyvault/azure-security-keyvault-jca/checkstyle-suppressions.xml updates are script-generated via eng/scripts/linting_suppression_generator.py (not manually edited).
  • Added lazy loading in KeyVaultCertificates for certificate, key, and certificate chain by alias.
  • Added alias regex filtering via system property:
    • azure.keyvault.jca.certificate-alias-filter-pattern configures a single filter.
    • Append a suffix 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.
    • Include patterns are plain regex entries.
    • Exclude patterns are prefixed with !.
    • Patterns use full-alias matching (Pattern.matcher(alias).matches()).
  • Improved concurrent behavior and refresh flow in KeyVaultCertificates:
    • Reduced lock contention during lazy loads.
    • Alias listing is performed under the instance lock, so concurrent refreshes can no longer apply their results out of order, and only one listing request is issued per refresh.
    • Added retry semantics for transient/null load failures.
    • Preserved alias/cache consistency after refresh and client updates.
  • Updated KeyVaultKeyStore lookup pathing and filter-pattern collection.
  • Updated module docs and release notes (README.md, CHANGELOG.md).
  • Added/updated unit tests in:
    • KeyVaultKeyStoreUnitTest
    • KeyVaultCertificatesTest

Why one property per filter

Each property value is a regex, so no delimiter is safe: a comma is valid regex syntax, and a bounded quantifier such as \d{1,5} would be split into \d{1 and 5} and fail to compile. Escaping does not help either, because \ is already the regex escape character, so \, cannot be distinguished from a legitimate regex escape. Giving each filter its own property removes the delimiter entirely, so a pattern may contain any character.

A suffix only keeps property names unique and does not affect evaluation, because include patterns are matched with anyMatch and exclude patterns with noneMatch. Property names are case-sensitive, so .prod and .PROD are two distinct filters.

-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$'

Quote the value as required by your shell. On cmd.exe in particular, -D...='^prod-.*' arrives as 'prod-.*', because single quotes are not stripped and ^ is the escape character; the result still compiles as a regex but matches nothing. The README documents the correct quoting per shell.

Validation

  • mvn -f sdk/keyvault/azure-security-keyvault-jca/pom.xml clean test: 115 tests pass.
  • mvn -f sdk/keyvault/azure-security-keyvault-jca/pom.xml checkstyle:check spotbugs:check: 0 Checkstyle violations, 0 SpotBugs findings.

Notes

  • The filter property is introduced by this PR and has never shipped, so renaming it from the earlier comma-separated azure.keyvault.jca.certificate-alias-filter-patterns to azure.keyvault.jca.certificate-alias-filter-pattern is not a breaking change, and the old name is removed rather than deprecated.
  • main has been merged in after the 2.12.0 release, so the release notes now target 2.13.0-beta.1.

All SDK Contribution checklist:

  • The pull request does not introduce [breaking changes]
  • CHANGELOG is updated for new features, bug fixes or other significant changes.
  • I have read the contribution guidelines.

General Guidelines and Best Practices

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, see this page.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances azure-security-keyvault-jca to reduce unnecessary Key Vault reads by (1) allowing users to configure a subset of certificate aliases to consider and (2) lazily loading certificate details only when a specific alias is requested—addressing the scenario described in #39487 (iterating/fetching all aliases when only one is configured).

Changes:

  • Added azure.keyvault.jca.certificates system property support to filter Key Vault certificate aliases to a configured subset.
  • Implemented lazy loading of Key Vault certificate key/certificate/chain data per alias (instead of eagerly loading all details on refresh).
  • Updated tests and documentation (README + CHANGELOG) to cover and describe the new behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java Wires configured alias filtering into keystore initialization and routes Key Vault lookups through lazy-loading accessors.
sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificates.java Implements configured-alias filtering and lazy loading of certificate details per alias.
sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/KeyVaultKeyStoreUnitTest.java Adds unit coverage for parsing configured aliases and verifying they are passed into KeyVaultCertificates.
sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java Adds unit coverage ensuring alias listing is not eager and that only requested/configured aliases trigger Key Vault reads.
sdk/keyvault/azure-security-keyvault-jca/README.md Documents the new azure.keyvault.jca.certificates configuration option.
sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md Records the new filtering + lazy-loading features for the upcoming release.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Listing aliases ran outside the instance lock, so two concurrent
refreshes could apply their results in completion order rather than
start order. A slow refresh could overwrite a newer alias list and
still stamp lastRefreshTime, pinning the stale list for the whole
refresh interval. Every caller also issued its own list request while
only one result was kept.

Move the listing into the existing synchronized block and hoist the
double check ahead of it. The stale-client identity guard is no longer
reachable there because updateKeyVaultClient holds the same lock, so it
is removed. The per-alias lazy loaders keep their guards and still run
their remote calls outside the lock.
The comma separated azure.keyvault.jca.certificate-alias-filter-patterns
property could not carry arbitrary regexes: a comma is valid regex
syntax, so a bounded quantifier such as \d{1,5} was split into \d{1 and
5} and failed to compile. No delimiter is safe here, because a regex can
contain any printable character.

Replace it with azure.keyvault.jca.certificate-alias-filter-pattern,
optionally suffixed to configure more than one filter. The suffix only
keeps property names unique and does not affect evaluation, since
include patterns are matched with anyMatch and exclude patterns with
noneMatch. The ! prefix for exclude patterns is unchanged.

The replaced property was introduced in this unreleased version, so it
is removed rather than deprecated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/KeyVaultKeyStore.java:64

  • The configured system property name for alias filtering is hard-coded here as azure.keyvault.jca.certificate-alias-filter-pattern, but the PR description/notes indicate the property was renamed to azure.keyvault.jca.certificate-alias-filter-patterns (plural). This inconsistency will confuse users and makes it unclear which property is actually supported. Please align the property name across code, README, CHANGELOG, and tests (or explicitly support both names for backward/forward compatibility).
    static final String CERTIFICATE_ALIAS_FILTER_PATTERN_PROPERTY
        = "azure.keyvault.jca.certificate-alias-filter-pattern";

@vcolin7 vcolin7 left a comment

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.

Suggesting some wording changes for clarity.

Comment on lines +161 to +163
-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$'

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$'

Comment on lines +166 to +170
* 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.

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.

* 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.

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:


#### 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:


### 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))

@vcolin7 vcolin7 left a comment

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.

Some more functional comments

if (!Objects.isNull(key)) {
certificateKeys.put(alias, key);
synchronized (this) {
if (loadedCertificateChainAliases.contains(alias) || !aliases.contains(alias)) {

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.

Multiple callers can pass this check before the alias is marked as loaded, so concurrent TLS handshakes can issue duplicate Key Vault requests; the loaded set only deduplicates publication. We should consider adding per-alias/per-material single-flight state, like an in-flight future, so callers share one fetch and avoid startup latency and throttling.

}

try {
Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias);

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.

The chain and key are fetched independently through versionless aliases, so a rotation between their first accesses can cache a v1 chain with a v2 private key and break TLS indefinitely when refresh is disabled. We should resolve and pin one certificate version per alias, ideally caching its material as a single immutable bundle.

try {
Certificate[] loadedCertificateChain = currentKeyVaultClient.getCertificateChain(alias);
synchronized (this) {
if (currentKeyVaultClient != keyVaultClient

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.

This guard handles client replacement but not a refresh on the same client, so a load started before refreshCertificates() can repopulate the cleared cache with stale material. We could track a cache generation, capture it before the remote call, and discard the result if a refresh or invalidation advanced that generation.

Certificate[] certificateChain = keyVaultClient.getCertificateChain(alias);
if (!Objects.isNull(certificateChain)) {
certificateChains.put(alias, certificateChain);
} catch (RuntimeException exception) {

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.

This converts every non-2xx failure, including 403 and 429, into a null chain, conflating a failed load with an absent entry and potentially causing lower-priority fallback or an unrelated NPE in KeyVaultKeyManager. I'd keep the entry retryable but propagate or wrap service failures, reserving an empty chain for a genuine no-chain result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

azure-spring All azure-spring related issues azure-spring-jca KeyVault

Projects

Status: Untriaged
Status: In Progress

Development

Successfully merging this pull request may close these issues.

4 participants