It would be great and useful to have such functionality out-of-box. I'm not sure if all web-server implementations support SNI, but tomcat and reactor-netty definitely support it.
Comment From: wilkinsona
Thanks for the suggestion, @sokomishalov. How would you expect to configure the auto-configuration if it supported SNI? I'm wondering what you had imagined the configuration properties would be so that you can provide multiple key stores and map each to a host pattern.
Comment From: sokomishalov
Thanks for the quick reply, @wilkinsona! Well, I suppose it could look like that:
server:
ssl:
enabled: true
key-store: /path/to/key-store
key-store-password: foobar
key-alias: fallback
sni:
- mapping: foo.example.com
ssl:
key-store: ${server.ssl.key-store}
key-store-password: ${server.ssl.key-store-password}
key-alias: foo
- mapping: bar.example.com
ssl:
key-store: ${server.ssl.key-store}
key-store-password: ${server.ssl.key-store-password}
key-alias: bar
If you'd want to use SNI, you still have to set up a fallback SSL context in that rare case when the user's client does not support SNI.
I'm not sure if it's ok to reuse SSL properties in that recursive way or just to provide new higher-level property for SNI mappings inside ServerProperties, but I think it could be possible to reuse Ssl-properties POJO somehow.
Also, maybe it could be useful to provide a property that will automatically extract DNS/IP from SAN or just CN to provide default SNI-mapping for this SSL context.
Here is my gist - netty customizer to create SNI from a single key-store with multiple aliases with current spring-boot SSL properties, which maps SSL context to domain names from SAN and/or CN. Maybe someone will find it useful.
Comment From: wilkinsona
@SidneyLann Please use a thumbs up reaction (👍) on the opening description rather than commenting as it avoids notifying everyone watching the repository.
Comment From: SidneyLann
server.ssl.enabled=true server.ssl.key-store=/home/sidney/app/ssl/pc9g.com.p12 server.ssl.key-store-password=password server.ssl.key-store-type=PKCS12 server.ssl.sni[0].mapping=gsz.pc8g.com server.ssl.sni[0].ssl.key-store=/home/sidney/app/ssl/gsz.pc9g.com.p12 server.ssl.sni[0].ssl.key-store-password=password
Why the settings of sni not work? exception accour: javax.net.ssl.SSLException: Received fatal alert: certificate_unknown
Comment From: wilkinsona
@SidneyLann This issue is still open as SNI is not yet supported.
Comment From: SidneyLann
When will be supported then?
Comment From: snicoll
@SidneyLann There's no commitment to the next feature release 2.7.x as the milestone on the issue indicates (2.x at the moment). If someone contributes a PR, we might get to it sooner.
Comment From: SidneyLann
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.server.WebServerSslBundle;
import org.springframework.stereotype.Component;
import com.pcng.gateway.handler.SniSslServerCustomizer;
@Component
public class NettyServerSniCustomizer implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
private static final String KEY_STORE = ".pc.com.p12";
private static final String SSL_FOLDER = "/home/sidney/app/ssl/";
@Value("${server.http2.enabled}")
private boolean enableHttp2;
@Override
public void customize(NettyReactiveWebServerFactory serverFactory) {
String domain2s = System.getProperty("domain2s");
String[] domain2sArr = domain2s.split(",");
String[] hostNames = new String[domain2sArr.length];
Ssl.ClientAuth[] clientAuths = new Ssl.ClientAuth[domain2sArr.length];
Http2[] http2s = new Http2[domain2sArr.length];
SslBundle[] sslBundles = new SslBundle[domain2sArr.length];
for (int i = 0; i < domain2sArr.length; i++) {
hostNames[i] = domain2sArr[i] + ".pc.com";
Ssl ssl = new Ssl();
ssl.setKeyStore(SSL_FOLDER + domain2sArr[i] + KEY_STORE);
ssl.setKeyStorePassword("mypassw0rd");
ssl.setKeyStoreType("PKCS12");
ssl.setClientAuth(Ssl.ClientAuth.NONE);
clientAuths[i] = ssl.getClientAuth();
Http2 http2 = new Http2();
http2.setEnabled(enableHttp2);
http2s[i] = http2;
sslBundles[i] = WebServerSslBundle.get(ssl);
}
try {
serverFactory.addServerCustomizers(new SniSslServerCustomizer(hostNames, http2s, clientAuths, sslBundles));
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.web.embedded.netty.NettyServerCustomizer;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Ssl;
import io.netty.handler.ssl.ClientAuth;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.Http2SslContextSpec;
import reactor.netty.http.server.HttpServer;
import reactor.netty.tcp.AbstractProtocolSslContextSpec;
import reactor.netty.tcp.SslProvider;
public class SniSslServerCustomizer implements NettyServerCustomizer {
private final String[] hostNames;
private final Http2[] http2;
private final Ssl.ClientAuth[] clientAuth;
private final SslBundle[] sslBundle;
public SniSslServerCustomizer(String[] hostNames, Http2[] http2, Ssl.ClientAuth[] clientAuth, SslBundle[] sslBundle) {
this.hostNames = hostNames;
this.http2 = http2;
this.clientAuth = clientAuth;
this.sslBundle = sslBundle;
}
@Override
public HttpServer apply(HttpServer server) {
try {
AbstractProtocolSslContextSpec<?> sslContextSpec = null;
Map<String, Consumer<? super SslProvider.SslContextSpec>> domainMap = new HashMap<>();
for (int i = 1; i < sslBundle.length; i++) {
sslContextSpec = createSslContextSpec(i);
final AbstractProtocolSslContextSpec<?> sslContextSpec2 = sslContextSpec;
domainMap.put(this.hostNames[i], spec -> spec.sslContext(sslContextSpec2));
}
return server.secure(spec -> spec.sslContext(createSslContextSpec(0)).addSniMappings(domainMap));
} catch (Exception e) {
return null;
}
}
protected AbstractProtocolSslContextSpec<?> createSslContextSpec(int i) {
AbstractProtocolSslContextSpec<?> sslContextSpec = (this.http2[i] != null && this.http2[i].isEnabled()) ? Http2SslContextSpec.forServer(this.sslBundle[i].getManagers().getKeyManagerFactory())
: Http11SslContextSpec.forServer(this.sslBundle[i].getManagers().getKeyManagerFactory());
sslContextSpec.configure((builder) -> {
builder.trustManager(this.sslBundle[i].getManagers().getTrustManagerFactory());
SslOptions options = this.sslBundle[i].getOptions();
builder.protocols(options.getEnabledProtocols());
builder.ciphers(SslOptions.asSet(options.getCiphers()));
builder.clientAuth(org.springframework.boot.web.server.Ssl.ClientAuth.map(this.clientAuth[i], ClientAuth.NONE, ClientAuth.OPTIONAL, ClientAuth.REQUIRE));
});
return sslContextSpec;
}
}
I have a spring boot 3 workaround now.