Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

SSL/TLS & HTTPS in Spring Boot

Configuring TLS properly: the handshake, keystores and PKCS12, HTTP to HTTPS redirect, mutual TLS for service-to-service, cipher policy, and where to terminate.

Intermediate5 min readUpdated
On this page

TLS is mostly configuration rather than code, and the configuration decisions — where to terminate, which versions and ciphers, whether to require client certificates — have real consequences.

Key Takeaways

  • Terminate at the edge unless compliance or mTLS requires otherwise.
  • Allow TLS 1.2 and 1.3 only; disable everything older.
  • Redirect HTTP to HTTPS and send HSTS — the redirect alone leaves a first-request gap.
  • Mutual TLS authenticates the client cryptographically, replacing shared secrets between services.
  • Automate renewal, and alert on expiry well in advance.

The handshake, briefly

Asymmetric cryptography establishes a shared secret; everything after that is symmetric, which is why TLS is cheap once connected.

The expensive part is the handshake. Connection reuse and HTTP/2 multiplexing matter for performance precisely because they amortise it — which is one reason keeping connections alive is a performance setting with security-adjacent consequences.

Terminating in the application

application.yml
server:
  port: 8443
  ssl:
    enabled: true
    key-store: 'file:/etc/tls/keystore.p12'
    key-store-password: ${KEYSTORE_PASSWORD}
    key-store-type: PKCS12
    key-alias: app
    # 1.2 and 1.3 only. Older versions have practical downgrade attacks.
    enabled-protocols: 'TLSv1.2,TLSv1.3'
    ciphers:
      - TLS_AES_256_GCM_SHA384
      - TLS_AES_128_GCM_SHA256
      - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
      - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
  http2:
    enabled: true

Every cipher in that list provides forward secrecy through ECDHE and authenticated encryption through GCM. Forward secrecy is the property that matters most: a future compromise of the private key does not decrypt traffic recorded today.

Use PKCS12 rather than the older JKS format — it is the standard, it is interoperable, and JKS is deprecated.

Redirecting HTTP

HttpRedirectConfig.java
@Configuration
public class HttpRedirectConfig {
 
    @Bean
    ServletWebServerFactory servletContainer() {
        var tomcat = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                var constraint = new SecurityConstraint();
                constraint.setUserConstraint("CONFIDENTIAL");
                var collection = new SecurityCollection();
                collection.addPattern("/*");
                constraint.addCollection(collection);
                context.addConstraint(constraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(redirectConnector());
        return tomcat;
    }
 
    private Connector redirectConnector() {
        var connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(8080);
        connector.setSecure(false);
        connector.setRedirectPort(8443);
        return connector;
    }
}

Pair it with HSTS. The redirect handles the request; HSTS ensures the next one never leaves in plaintext at all.

Behind a load balancer

The common production shape: TLS terminates at the ingress, and the application speaks HTTP inside the trusted network.

application.yml
server:
  # Trust the proxy's forwarding headers so the application knows the original
  # scheme. Without this, redirects and generated absolute URLs come out as
  # http:// and browsers refuse them as mixed content.
  forward-headers-strategy: framework
  tomcat:
    remoteip:
      remote-ip-header: X-Forwarded-For
      protocol-header: X-Forwarded-Proto

Only enable this when a proxy you control is genuinely in front. If the application is directly reachable, a client can spoof X-Forwarded-Proto: https and any security check based on request.isSecure() becomes trivially bypassable.

Mutual TLS

application.yml
server:
  ssl:
    client-auth: need          # 'want' makes it optional; 'need' requires it
    trust-store: 'file:/etc/tls/truststore.p12'
    trust-store-password: ${TRUSTSTORE_PASSWORD}
    trust-store-type: PKCS12
X509Config.java
@Bean
SecurityFilterChain mtlsChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/internal/**")
        .x509(x509 -> x509
                // Map the certificate CN to a principal name.
                .subjectPrincipalRegex("CN=(.*?)(?:,|$)")
                .userDetailsService(serviceAccountDetailsService))
        .authorizeHttpRequests(auth -> auth.anyRequest().hasRole("SERVICE"));
    return http.build();
}

mTLS gives each service a cryptographic identity instead of a shared secret. A compromised service can only present its own certificate, so it can only reach what that identity is permitted to reach — whereas a leaked API key is usable by anyone who has it.

One detail on the truststore is worth getting right, because the failure is silent: it defines who may connect. Loading a public CA bundle into it means any certificate that CA has ever issued authenticates successfully — which is every site on the internet, not your services. Issue workload certificates from a private CA and keep that CA the only entry in the truststore.

The operational cost is certificate lifecycle for every workload. A service mesh handles this automatically with short-lived certificates rotated hourly, which is the main practical argument for adopting one.

Verifying and monitoring

terminal
# What was actually negotiated
openssl s_client -connect app.acme.com:443 -tls1_3 </dev/null 2>/dev/null | head -20
 
# Confirm old versions are refused — this SHOULD fail
openssl s_client -connect app.acme.com:443 -tls1_1 </dev/null
 
# Days until expiry
echo | openssl s_client -connect app.acme.com:443 2>/dev/null \
  | openssl x509 -noout -enddate

Certificate expiry is one of the most reliable causes of a total outage, and it is entirely preventable. Automate renewal with cert-manager or ACME, and alert at thirty days remaining — not seven, because a renewal that fails needs time to diagnose.

Monitor the negotiated protocol distribution too. A sudden appearance of TLS 1.2 where you expected 1.3 usually means a client or an intermediate proxy changed, and it is worth knowing before someone reports it.

Certificate pinning

Pinning a specific certificate or public key in a client prevents a compromised or coerced certificate authority from issuing a valid certificate for your domain.

It is also a reliable way to break your own application. When the pinned certificate rotates and a client has not been updated, that client cannot connect at all — and for a mobile app, updating means an app-store release cycle.

If you pin, pin the public key rather than the certificate so renewal with the same key does not break anything, and always pin a backup key. For most web applications, HSTS plus Certificate Transparency monitoring gives most of the benefit with none of the outage risk.

What to take away

Terminate at the edge unless you need end-to-end encryption or client certificates. Allow only TLS 1.2 and 1.3 with forward-secret AEAD ciphers. Redirect HTTP and send HSTS, since the redirect alone leaves a gap. Use mutual TLS for service-to-service identity, and automate renewal with an alert at thirty days.

Frequently Asked Questions

Should Spring Boot terminate TLS itself?
Usually not. Terminating at a load balancer or ingress centralises certificate management and renewal, and offloads the handshake. Terminate in the application when you need end-to-end encryption for compliance, or when you are doing mutual TLS with client certificates the application must inspect.
Which TLS versions should I allow?
TLS 1.2 and 1.3 only. TLS 1.0 and 1.1 are deprecated by every major standards body and browser, and SSLv2 and SSLv3 are long broken. Disabling the old ones is one configuration line and removes an entire class of downgrade attack.
Do I still need HSTS if I redirect HTTP to HTTPS?
Yes. The redirect means the first request still goes out in plaintext, where it can be intercepted and the redirect suppressed. HSTS makes the browser upgrade before any request leaves, which closes that window entirely for repeat visits.

Related tutorials