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.
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
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
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: trueEvery 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
@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.
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-ProtoOnly 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
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@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
# 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 -enddateCertificate 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?
Which TLS versions should I allow?
Do I still need HSTS if I redirect HTTP to HTTPS?
Related tutorials
- HTTP Security HeadersEvery security header worth setting: what each one prevents, the values to use, Spring configuration, and how to roll out a Content Security Policy without breaking the site.
- SQL Injection PreventionHow SQL injection actually works, why parameterised queries stop it, the JPA and JdbcTemplate patterns that are safe, the ones that are not, and how to test for it.
- CORS ConfigurationUnderstanding the same-origin policy, what preflight actually checks, configuring CORS in Spring correctly, and why allowedOrigins star with credentials is refused.
- XSS PreventionStopping cross-site scripting: the three XSS types, why encoding must be context-aware, Thymeleaf escaping, OWASP Java Encoder, nonce-based CSP and sanitising rich text.