spring-cloud / spring-cloud/spring-cloud-gateway

Support dynamic SNI configuration ?

Open
#3,902 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

waiting-for-triage
Dominant language
Java
Stars
4.9k
Forks
3.5k
Avg merge
20h 57m
Merged PRs (30d)
8

Description

Is your feature request related to a problem? Please describe.
I am using SCG in an environment where my backends are AWS ALBs configured with a certificate that does not contains the ALB's own hostname. This setup works because those ALBs are behind cloudfront, that accepts origin certificates that match either of the origin's hostname or the hostnames for which it is itself configured (e.g. cloudfront configured for www.example.com will connect to an ALB whose certificate is www.example.com).

I am trying to integrate SCG in this environment and obviously it results in TLS errors because the ALB's hostname does not match its certificate.

I have found a way to do this, by providing an overridden implementation of the NettyRoutingFilter that reproduces cloudfront's behavior, by using the Host header as the SNI server name:

public class SniChangeNettyRoutingFilter extends NettyRoutingFilter {

    private static final Log log = LogFactory.getLog(SniChangeNettyRoutingFilter.class);
    private final SslProvider.GenericSslContextSpec<?> clientSslContext;

    public SniChangeNettyRoutingFilter(HttpClient httpClient, ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider, HttpClientProperties properties, ServerProperties serverProperties) {
        super(httpClient, headersFiltersProvider, properties);
        this.clientSslContext = (serverProperties.getHttp2().isEnabled())
            ? Http2SslContextSpec.forClient() : Http11SslContextSpec.forClient();
    }

    @Override
    protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) {
        var httpClient = super.getHttpClient(route, exchange);

        var host = exchange.getRequest().getHeaders().getFirst(HttpHeaders.HOST);

        if (host == null) {
            host = exchange.getRequest().getURI().getHost();
        }
        log.debug("Configuring SSL context with SNI hostname: " + host);

        var sni = new SNIHostName(host);

        HttpClient client = httpClient.secure(sslContextSpec -> sslContextSpec.sslContext(clientSslContext).serverNames(sni));
        return client;
    }
}

The main issue here (apart from: is this even the right approach?) is that I have to provide a context spec to sslContextSpec.sslContext() in order to obtain the builder on which I can call serverNames(), but the default context spec built by HttpClientSslCustomizer is not exposed via a bean or method call.

This in turns makes it necessary to reproduce the SSL context configuration contained in HttpClientSslCustomizer sos the other SSL configuration options are honored (in my example you can see I am only honoring the http2 setting).

Describe the solution you'd like
There should be a better way to dynamically configure the SNI sent in the TLS handshake while keeping the configuration options set in the gateway's config.

Describe alternatives you've considered

  • Statically configure the SNI hostnames via an override of HttpClientCustomizer: this option only supports static configurations and in my use-case, there are tens of backends, it is not practical to send all of those in the SNI header.
  • Provide my own TrustManager that does not check the SNI host: IMHO this is a less secure option, I still want SNI verification. Moreover, I cannot override "parts" of a TrustManager as they usually come as final classes so I would have to implement one "from scratch" or copy existing code, with the security implications it has and it would be difficult to provide the hostname from the request to the trust manager, it does not have access to the request's context.
  • Create an HttpClientCustomizer that adds a doOnRequest hook that extracts the host header an stores it in an attribute followed by a doOnChannelInit() hook that takes this attribute and modifies the SSLEngine on the fly by providing new SSLParameters. I am unsure on the safety of this approach, I am assuming the SSLEngine is shared across channels.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading NettyRoutingFilter and HttpClientSslCustomizer to understand how the gateway currently builds outbound TLS clients, then compare the extension points in HttpClientCustomizer. Define how the request host should become the dynamic SNI while preserving the gateway's configured SSL and HTTP/2 options. Done means the behavior is covered by gateway tests and the existing TLS configuration remains effective.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, java, spring
Domain
backend-api-design, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.