Skip to content

fix: don't delegate client alias choosing for ssl bundles#49838

Closed
MezinK wants to merge 8 commits intospring-projects:mainfrom
MezinK:alias-support-for-client-ssl-bundles
Closed

fix: don't delegate client alias choosing for ssl bundles#49838
MezinK wants to merge 8 commits intospring-projects:mainfrom
MezinK:alias-support-for-client-ssl-bundles

Conversation

@MezinK
Copy link
Copy Markdown

@MezinK MezinK commented Mar 29, 2026

Currently, the .key.alias specified inside of an SSL Bundle is not taken into account when dealing with client certificates.
This causes the delegate (usually SunX509KeyManagerImpl) to just pick the first alias it finds.
For requests requiring mTLS, this can cause issues if the certificates are inside of a shared keystore, as you may not get the certificate that you would want.

I have created an example repo demonstrating this behavior:
https://github.com/MezinK/spring-boot-mtls-demo

Signed-off-by: MezinK <mezinkocahal@hotmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 29, 2026
@MezinK MezinK force-pushed the alias-support-for-client-ssl-bundles branch from 4a3acd4 to 427cdd3 Compare March 29, 2026 18:39
@MezinK MezinK changed the title fix: don't delegate client alias choosing fix: don't delegate client alias choosing for ssl bundles Mar 29, 2026
@wilkinsona
Copy link
Copy Markdown
Member

We should consider the discussion in #44629 when reviewing this proposal.

@wilkinsona
Copy link
Copy Markdown
Member

wilkinsona commented Mar 30, 2026

This causes the delegate (usually SunX509KeyManagerImpl) to just pick the first alias it finds.

I don't think it picks the first alias. AIUI, it picks the first matching alias.

Regardless, when looking at #44629, I had this concern:

Previously, the alias only affected the server side. Applying it to the client side as well could be a breaking change, particularly if someone's sharing an SSL bundle between client and server components and they're used to the alias only affecting the server side.

I still have that concern and the change proposed here does not address it. If we want to provide some control over the alias that's used on the client-side rather than delegating to the matching performed by SunX509KeyManagerImpl, I think we'll need to make it opt-in some how.

@MezinK
Copy link
Copy Markdown
Author

MezinK commented Mar 30, 2026

I still have that concern and the change proposed here does not address it. If we want to provide some control over the alias that's used on the client-side rather than delegating to the matching performed by SunX509KeyManagerImpl, I think we'll need to make it opt-in some how.

Perhaps, this can be added under SslOptions?
As per documentation: "Configuration options that should be applied when establishing an SSL connection."

What do you think?

@wilkinsona
Copy link
Copy Markdown
Member

That would allow users to opt in but I wonder if some may need separate aliases for the client side and for the server side. I'm not sure how likely it is that someone would have an SSL setup that requires that configurability but perhaps we should deprecate alias and introduce serverAlias and clientAlias? The new serverAlias would behave as alias does today and clientAlias would be returned from the chooseEngineClientAlias and chooseClientAlias methods. I'll discuss it with the rest of the team.

@wilkinsona wilkinsona added the for: team-meeting An issue we'd like to discuss as a team to make progress label Mar 30, 2026
@MezinK
Copy link
Copy Markdown
Author

MezinK commented Mar 30, 2026

I've debugged the flow of the SunX509KeyManagerImpl and understood the following:

  1. Check if keyTypes were specified
  2. For every keyType specified, check if the underlying certificates public key uses that keyType
  3. If true -> put the alias of that certificate in a list, if none found -> skip and continue with the next keyType
  4. If alias(es) have been found, the first alias in the array is returned.

See this screenshot:
image

Here, we are in the CertificateATest.java class, but you can see that aliases[0] is equal to "certificate-b", therefore the test has the unexpected results.

@wilkinsona
Copy link
Copy Markdown
Member

We discussed this today. We'd like to deprecate alias and replace it with server-alias and introduce a new client-alias property. @MezinK, would you like to update your PR to that effect?

@wilkinsona wilkinsona added status: waiting-for-feedback We need additional information before we can continue and removed for: team-meeting An issue we'd like to discuss as a team to make progress labels Apr 1, 2026
@MezinK
Copy link
Copy Markdown
Author

MezinK commented Apr 1, 2026

Sure, I can do that

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 1, 2026
MezinK added 2 commits April 1, 2026 19:30
Signed-off-by: MezinK <mezinkocahal@hotmail.com>
@MezinK
Copy link
Copy Markdown
Author

MezinK commented Apr 1, 2026

@wilkinsona I have yet to add tests but I caught a case where I got a bit confused. I marked it with "TODO" in the code.
https://github.com/MezinK/spring-boot/blob/29dbc32194e56575b53990b412e335d31bc43e70/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java#L104
What do we do here? We have the option of clientAlias and serverAlias, but what if we have both AND they're different?

because this would currently be valid:

spring:
  ssl:
    bundle:
      jks:
        certificate:
          key:
            client-alias: "client-alias" # what happens here? there may only be one certificate at a time if the aliases are different
            server-alias: "server-alias"
          keystore:
            location: classpath:ssl/shared.p12
            password: ...

I assume, we want to load either a client alias or a server alias, but never both (unless they have the same value)
do we throw an error in this case telling the user they may not have different client and server aliases inside one ssl-bundle?

if this is the case, does it really make sense to branch it out into client-alias and server-alias?
because either way, it's only one certificate that is being loaded in one ssl bundle, which is going to be used for the server or the client, depending on what the user uses it in their code.

@wilkinsona
Copy link
Copy Markdown
Member

When both are configured, my expectation is that we'd pass both into AliasKeyManagerFactory and down into AliasX509ExtendedKeyManager and then the choose(Engine)ClientAlias methods would return the value of client-alias and the choose(Engine)ServerAlias methods would return the value of server-alias.

@wilkinsona wilkinsona added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Apr 2, 2026
MezinK added 2 commits April 6, 2026 13:29
Signed-off-by: MezinK <mezinkocahal@hotmail.com>
Signed-off-by: MezinK <mezinkocahal@hotmail.com>
@MezinK
Copy link
Copy Markdown
Author

MezinK commented Apr 6, 2026

I have tested the changes I pushed using ./gradlew publishToMavenLocal on the local demo repository.
Keeping .key.alias as the property causes the tests to pass, which is expected.
Setting .key.client-alias as the property now picks the correct private key -> the tests fail, which is also expected.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 6, 2026
Comment on lines 48 to 52
/**
* Return the alias of the key or {@code null} if the key has no alias.
* @return the key alias
*/
@Nullable String getAlias();
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 should be deprecated for removal in 4.3.0 in favor of getServerAlias() and getClientAlias().

/**
* Return the alias of the server key or {@code null} if the server key has no alias.
* @return the server key 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.

This should be marked as @since 4.1.0

/**
* Return the alias of the client key or {@code null} if the client key has no alias.
* @return the client key 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.

This should be marked as @since 4.1.0

* @param password the password used to access the key
* @param serverAlias the alias of the server key
* @param clientAlias the alias of the client key
* @return a new {@link SslBundleKey} instance
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 should be marked as @since 4.1.0.

Comment on lines +89 to +105
default void assertContainsAlias(@Nullable KeyStore keyStore, @Nullable String alias) {
if (keyStore == null) {
return;
}

try {
if (StringUtils.hasLength(alias)) {
Assert.state(keyStore.containsAlias(alias),
() -> String.format("Keystore does not contain alias '%s'", alias));
}
}
catch (KeyStoreException ex) {
throw new IllegalStateException(
String.format("Could not determine if keystore contains alias '%s'", alias), ex);
}
}

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 method is a little strange as callers are retrieving an alias from the key and then passing it back in. I think it should be removed.

Comment on lines -136 to -138

}

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.

These blank lines should not be removed.

Comment on lines +168 to +192
@Override
public SslStoreBundle getStores() {
return this.stores;
}

@Override
public SslBundleKey getKey() {
return this.key;
}

@Override
public SslOptions getOptions() {
return this.options;
}

@Override
public String getProtocol() {
return this.protocol;
}

@Override
public SslManagerBundle getManagers() {
return this.managers;
}

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.

Why are these changes needed? They seem to be unrelated.

Comment on lines +121 to +107
keyStore = keyStore.withAlias(properties.getKey().getAlias())
.withPassword(properties.getKey().getPassword());
keyStore = keyStore
.withAlias(properties.getKey().getAlias())
.withPassword(properties.getKey().getPassword());
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.

Please don't make unrelated formatting changes.

Comment on lines -70 to +83
return (key != null) ? SslBundleKey.of(key.getPassword(), key.getAlias()) : SslBundleKey.NONE;
}

private static SslOptions asSslOptions(SslBundleProperties.@Nullable Options options) {
return (options != null) ? SslOptions.of(options.getCiphers(), options.getEnabledProtocols()) : SslOptions.NONE;
}

@Override
public SslStoreBundle getStores() {
return this.stores;
}

@Override
public SslBundleKey getKey() {
return this.key;
}
if (key == null) {
return SslBundleKey.NONE;
}

@Override
public SslOptions getOptions() {
return this.options;
}
if (key instanceof JksKey jksKey) {
return SslBundleKey.of(jksKey.getPassword(), jksKey.getServerAlias(), jksKey.getClientAlias());
}

@Override
public String getProtocol() {
return this.protocol;
return SslBundleKey.of(key.getPassword(), key.getAlias());
}

@Override
public SslManagerBundle getManagers() {
return this.managers;
private static SslOptions asSslOptions(SslBundleProperties.@Nullable Options options) {
return (options != null) ? SslOptions.of(options.getCiphers(), options.getEnabledProtocols()) : SslOptions.NONE;
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.

Why are these changes needed? They seem to be unrelated.

Comment on lines +120 to +168
public static class JksKey extends Key {
/**
* The alias that identifies the server key in the key store.
*/
private @Nullable String serverAlias;

/**
* The alias that identifies the client key in the key store.
*/
private @Nullable String clientAlias;

public @Nullable String getServerAlias() {
return this.serverAlias;
}

public void setServerAlias(@Nullable String serverAlias) {
this.serverAlias = serverAlias;
}

public @Nullable String getClientAlias() {
return this.clientAlias;
}

public void setClientAlias(@Nullable String clientAlias) {
this.clientAlias = clientAlias;
}

/**
* Alias that identifies the key in the key store. Deprecated in favor of {@link #getServerAlias()}
* @return the server key alias
* @deprecated in favor of {@link #getServerAlias()}
*/
@Override
@Deprecated(since = "4.0.0", forRemoval = true)
public @Nullable String getAlias() {
return super.getAlias();
}

/**
* Alias that identifies the key in the key store. Deprecated in favor of {@link #setServerAlias(String)}
* @param alias the server key alias to set
* @deprecated in favor of {@link #setServerAlias(String)}
*/
@Override
@Deprecated(since = "4.0.0", forRemoval = true)
public void setAlias(@Nullable String alias) {
super.setAlias(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.

Why is a custom Key sub-class needed? I expect the changes to be made directly to the existing org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key class.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This was done because of the way that JKS and PEM SSL Bundles behave:
JKS based bundles may use a keystore with multiple certificate/key entries (here alias based picking makes sense, otherwise there is no way to pick the proper certificate)

PEM based bundles may only have a single certificate/private-key pair, which means you cannot have this multi certificate/key behavior like you do with JKS bundles, therefore, alias picking doesn't really make sense here in my opinion. (you can of course check if it's the correct alias, but other than that, it's always 1 certificate, therefore that 1 certificate will be used)

of course, if it's still intended to have client/server aliases for PEM aswell, I'll adjust these changes accordingly.

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.

I see. Thanks.

Unfortunately, I think we need to take a few steps back in that case. A JKS-specific feature would, ideally, only affect code in org.springframework.boot.ssl.jks and the more generic code in org.springframework.boot.pem would be unchanged.

I think we need to do some design work and figure out what to do here so I'm going to close this one. Please open an issue for your problem and we can take things from there.

Signed-off-by: MezinK <mezinkocahal@hotmail.com>
Signed-off-by: MezinK <mezinkocahal@hotmail.com>
@wilkinsona
Copy link
Copy Markdown
Member

Closing as per #49838 (comment). We need to figure out how to allow different client and server alias configuration when it should probably be JKS-specific.

@wilkinsona wilkinsona closed this Apr 7, 2026
@wilkinsona wilkinsona added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided labels Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: declined A suggestion or change that we don't feel we should currently apply

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants