HTTPS & SSL/TLS

The transport-layer encryption every authentication mechanism in this section silently depends on — the handshake, the certificate chain, and where TLS termination actually leaves your data unprotected

← Back to Index

What is HTTPS/TLS — and Why Does Every Other Security Mechanism Depend On It?

HTTPS is HTTP running over TLS (Transport Layer Security) — the same request/response protocol, but every byte on the wire is encrypted, integrity-checked, and tied to a verified server identity before a single header is exchanged. "SSL" is the name everyone still uses out of habit; the actual protocol has been TLS since 1999, and every version of SSL itself (2.0, 3.0) is now considered broken and disabled by every modern browser and server.

This matters to every other page in this section. A password sent over plain HTTP is readable by anyone on the network path. A JWT in an Authorization header is a bearer token — whoever has the bytes has the access, no password needed. A session cookie is exactly the same. None of the authentication mechanisms covered elsewhere in this Bible provide any protection on their own if the channel carrying them is not encrypted — TLS is not one security feature among several, it is the precondition that makes the others meaningful.

// BEFORE — checkout form posts over plain HTTP
@PostMapping("/checkout")
public ResponseEntity<OrderConfirmation> checkout(@RequestBody CheckoutRequest request) {
    // The session cookie, the JWT in the Authorization header, and the card
    // details in the request body all travel as plaintext bytes on the wire.
    // Any router, ISP, or Wi-Fi eavesdropper between browser and server can
    // read every one of them — no exploit required, just a packet capture.
    return orderService.process(request);
}

// AFTER — the channel itself is the control, enforced at the security layer
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .requiresChannel(channel -> channel.anyRequest().requiresSecure())
        .headers(headers -> headers.httpStrictTransportSecurity(hsts -> hsts
            .includeSubDomains(true)
            .maxAgeInSeconds(31536000)))
        .build();
    // Now the same checkout endpoint rejects plain HTTP outright, and every
    // subsequent visit is told by HSTS to never even attempt HTTP again.
}

The TLS Handshake — How Two Strangers Agree on a Secret

TLS solves a specific bootstrapping problem: the browser and the server have never spoken before and share no secret, yet they need to agree on a symmetric encryption key strong enough to protect an entire session — without an eavesdropper who sees every message being able to derive that same key. Asymmetric cryptography (the server's certificate key pair) solves the bootstrap; a symmetric cipher does the actual bulk encryption, because asymmetric operations are far too slow to encrypt every request and response.

/*
 * TLS 1.3 handshake (1 round trip — the current standard, RFC 8446):
 *
 * Client → Server : ClientHello (supported cipher suites, a key share)
 * Server → Client : ServerHello (chosen cipher suite, its own key share,
 *                                its certificate, a "Finished" message)
 *                   — both sides can now derive the same symmetric session key
 *                     from the exchanged key shares (Diffie-Hellman)
 * Client → Server : Finished — handshake complete, application data now flows,
 *                   already encrypted with the session key
 *
 * TLS 1.2 needed 2 round trips for the equivalent negotiation, and permitted
 * legacy cipher suites (RC4, static RSA key exchange with no forward secrecy)
 * that TLS 1.3 removed outright. This is why "downgrade attacks" — tricking
 * a client into negotiating TLS 1.0/1.1 or a weak cipher — were a real class
 * of vulnerability against 1.2 deployments and are largely closed by 1.3
 * refusing to offer the weak options at all.
 */
Forward secrecy: why a stolen private key shouldn't decrypt yesterday's traffic

Modern TLS uses ephemeral Diffie-Hellman key exchange (ECDHE) — a fresh key pair is generated for every single handshake and discarded immediately after. Even if the server's long-term certificate private key is later compromised, an attacker who recorded encrypted traffic months ago still cannot decrypt it, because the actual session keys were never derived from — or recoverable from — that long-term key. This is why the older, static RSA key-exchange cipher suites (where the session key was derivable from the certificate's private key) are disabled by default in every current server and browser.

Certificates and the Chain of Trust

A TLS certificate (X.509 format) binds a public key to a domain name and is itself signed by a Certificate Authority (CA). The browser doesn't trust your certificate directly — it trusts a short list of root CAs baked into the OS or browser, and verifies a signature chain from your certificate up to one of those roots.

/*
 * The chain, leaf to root:
 *
 * shop.example.com certificate   ← the "leaf" cert, issued to your domain
 *        │  signed by
 *        ▼
 * Intermediate CA certificate    ← issued by the CA, rarely the root itself
 *        │  signed by
 *        ▼
 * Root CA certificate            ← pre-trusted, shipped with the OS/browser
 *
 * A server must send the leaf AND the intermediate certificate(s) — the root
 * is never sent, since the client already has it. Forgetting to include the
 * intermediate is one of the most common TLS misconfigurations: it works
 * fine in browsers that cache the intermediate from a previous visit to any
 * site using the same CA, and fails mysteriously for API clients, mobile
 * apps, and curl, which don't have that cache.
 */
Certificate typeValidation performed by CATypical use
DV (Domain Validated) Proves control of the domain only (DNS record or HTTP file) Default for most sites — Let's Encrypt issues DV certificates, free, auto-renewable
OV (Organization Validated) Domain control plus verified legal business identity Corporate sites wanting the organization name embedded in the cert
EV (Extended Validation) Rigorous legal and operational identity checks Largely obsolete — browsers stopped giving EV certs distinct UI treatment years ago, so the extra cost rarely buys anything today
A self-signed certificate is not "less secure encryption" — it's zero identity verification

The encryption strength of a self-signed certificate is identical to a CA-issued one — same algorithms, same key sizes. What's missing is the chain of trust: nobody vouches that the public key actually belongs to shop.example.com rather than to whoever is currently sitting in the middle of the connection. Self-signed certificates are fine for local development and internal service-to-service traffic on a network you control (see mTLS below); they are never appropriate for a public-facing endpoint, because the browser's "not trusted" warning trains users to click through security warnings — which is precisely the habit that makes real phishing attacks against them succeed later.

Configuring HTTPS in Spring Boot

Two legitimate topologies exist, and conflating them is the source of most production TLS confusion.

Topology A — Spring Boot terminates TLS directly

# application.yml — the embedded Tomcat/Netty server holds the certificate
server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: ${KEYSTORE_PASSWORD}   # never hardcode — inject via environment/secrets manager
    key-store-type: PKCS12
    key-alias: shop-example-com
    protocol: TLS
    enabled-protocols: TLSv1.3,TLSv1.2   # explicitly exclude TLS 1.0/1.1

Topology B — TLS terminates upstream (load balancer / reverse proxy)

## nginx / ALB / Cloudflare terminates HTTPS; the app receives plain HTTP
## on a private network segment the public internet cannot reach directly.
## This is the far more common production setup — certificate rotation,
## OCSP stapling, and cipher policy are centralized at the edge instead of
## being redeployed with every service.

# application.yml — tell Spring the original request was HTTPS even though
# the connection it actually received was plain HTTP from the load balancer
server:
  forward-headers-strategy: framework   # trusts X-Forwarded-Proto / X-Forwarded-For
"TLS terminates at the load balancer" does not mean the data is protected end-to-end

Once the load balancer decrypts the request, the hop from load balancer to application server is a separate connection — and if that hop runs over plain HTTP because "it's all internal," it is only as safe as the network it runs on. On a shared cloud VPC, a compromised neighboring workload or a misconfigured security group can put an attacker on that internal segment. Two acceptable fixes: run the internal hop over TLS as well (re-encryption at the load balancer), or use mutual TLS between internal services so both ends authenticate each other, not just encrypt. Also: server.forward-headers-strategy=framework means Spring will trust an X-Forwarded-Proto header from whatever sent the request — if that setting is enabled on a server reachable directly from the public internet (bypassing the load balancer), a client can forge X-Forwarded-Proto: https and trick the app into treating an actually-insecure connection as secure, silently defeating requiresSecure() and Secure-flagged cookies. Only enable it on servers that are network-unreachable except through the trusted proxy.

Enforcing HTTPS and HSTS at the application layer

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            // Reject any request that didn't arrive over HTTPS (or a trusted
            // X-Forwarded-Proto: https from the load balancer)
            .requiresChannel(channel -> channel.anyRequest().requiresSecure())

            // HSTS tells the browser to never attempt plain HTTP for this domain
            // again, for the given max-age — this closes the window where a
            // user typing "example.com" gets an initial plaintext request
            // that a network attacker could intercept before any redirect.
            .headers(headers -> headers
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .preload(true)
                    .maxAgeInSeconds(31536000)   // 1 year — the realistic minimum for HSTS preload list eligibility
                )
            )
            .build();
    }
}

Mutual TLS (mTLS) — When the Server Also Needs to Verify the Client

Standard TLS only authenticates the server to the client — the browser verifies it's really talking to shop.example.com, but the server accepts connections from anyone. mTLS adds a second certificate check in the other direction: the client also presents a certificate, and the server verifies it against a trusted CA before accepting the connection. This is the right tool for service-to-service traffic where both parties are known in advance — the classic e-commerce example is your order service calling an internal payment or inventory service, or a payment provider's webhook endpoint verifying that a callback genuinely originated from the provider's infrastructure and not from a forged request.

# application.yml — require and verify a client certificate
server:
  ssl:
    enabled: true
    key-store: classpath:server-keystore.p12
    key-store-password: ${KEYSTORE_PASSWORD}
    trust-store: classpath:client-truststore.p12   # the CA that signs YOUR internal services' client certs
    trust-store-password: ${TRUSTSTORE_PASSWORD}
    client-auth: need   # NEED = reject the connection outright without a valid client cert
mTLS is not a substitute for application-level authorization

A valid client certificate proves which service is calling, not what that service is allowed to do. In a payment flow, mTLS correctly establishes that the caller really is the internal inventory-service and not an impostor on the network — but the inventory service should still enforce its own authorization rules on top (does this caller have permission to decrement stock for this specific warehouse?). Treat the client certificate's identity the same way you'd treat an authenticated principal anywhere else in this section: it answers "who," not "what."

Common Misconfigurations

MisconfigurationSymptomFix
Missing intermediate certificate Works in most browsers, fails in curl/mobile apps/some API clients Serve the full chain (leaf + intermediate), not just the leaf
Mixed content Page loads over HTTPS but an image/script/stylesheet is fetched via a hardcoded http:// URL — the browser blocks or warns Use protocol-relative or explicit https:// URLs for every asset, never hardcode http://
Expired certificate Full-page browser interstitial warning, all traffic effectively stops Automate renewal (Let's Encrypt/certbot with a cron/systemd timer) and monitor expiry with alerting well before the 90-day window closes
Trusting X-Forwarded-Proto from an untrusted network requiresSecure() and Secure cookies silently pass even over a real plaintext connection Only enable forward-headers-strategy on servers unreachable except via the trusted proxy
Legacy protocol/cipher still enabled Passes casual testing, fails a security scan (SSLv3, TLS 1.0/1.1, RC4, static RSA key exchange) Explicitly set enabled-protocols: TLSv1.3,TLSv1.2 and let the modern JDK/server defaults choose ciphers rather than hand-picking a legacy list

Best Practices and Common Pitfalls

✅ Do

  • Enforce HTTPS at the security layer (requiresSecure()), don't rely on "we just don't link to the HTTP version"
  • Enable HSTS with a realistic max-age (1 year) and includeSubDomains once you're confident every subdomain also serves HTTPS
  • Automate certificate renewal and alert on expiry well in advance — a manual renewal process will eventually be forgotten
  • Serve the full certificate chain (leaf + intermediate), and verify it with an external tool, not just your own browser's cache
  • Use mTLS for internal service-to-service calls where both sides' identities matter, not just encryption
  • Explicitly restrict to TLS 1.2/1.3 and let the platform's modern cipher defaults apply, rather than hand-maintaining a cipher list

❌ Don't

  • Don't hardcode http:// in any asset URL on an HTTPS page — mixed content gets blocked and is a code smell that active-content injection was possible
  • Don't trust X-Forwarded-Proto from a server reachable directly from the public internet — it lets a client spoof "this connection was secure" over a plaintext request
  • Don't treat "TLS terminates at the load balancer" as "the data is protected end-to-end" — the internal hop is a separate connection with its own trust requirements
  • Don't use a self-signed certificate on a public-facing endpoint — it trains users to click through trust warnings
  • Don't assume mTLS replaces application-level authorization — a valid client certificate answers "who," never "what they're allowed to do"

Interview Questions

🎓 Junior level

Q: What problem does TLS solve that plain HTTP doesn't?
Plain HTTP sends every byte — headers, cookies, tokens, request and response bodies — as readable plaintext across every network hop between client and server. TLS encrypts that traffic, verifies the server's identity via its certificate, and detects tampering in transit. Without it, any authentication mechanism carried over the connection — a password, a session cookie, a JWT — is exposed to anyone who can observe the network path.

Q: What is a certificate chain, and why does a server need to send more than just its own certificate?
A certificate chain links the server's certificate (the "leaf") up through one or more intermediate CA certificates to a root CA that the browser already trusts. The server must send the leaf and the intermediate certificate(s) — the root is never sent, since the client already has it pre-installed. Omitting the intermediate is a common misconfiguration that happens to work in browsers that cached it from a prior visit elsewhere, but fails for API clients that have no such cache.

Q: What is HSTS and what attack does it protect against?
HTTP Strict Transport Security is a response header that tells the browser to never attempt a plain HTTP connection to this domain again for a specified duration. It protects against SSL-stripping attacks, where a network attacker intercepts a user's very first plaintext request (before any redirect to HTTPS can happen) and proxies the rest of the session over HTTP while presenting HTTPS to the user.

🔥 Senior level

Q: Your load balancer terminates TLS and forwards requests to application servers over plain HTTP inside the VPC. Is this acceptable, and what would make you change it?
It's a common and often acceptable topology, but it relies entirely on the internal network being a genuine trust boundary — no untrusted workloads can reach that segment. On shared infrastructure, a compromised neighboring container, a misconfigured security group, or an internal actor changes that calculus. I'd re-encrypt the internal hop (TLS all the way to the application server) or adopt mTLS for service-to-service traffic once the blast radius of a network compromise includes systems I don't fully control — a payment or PII-handling service is exactly where I'd stop treating "it's internal" as sufficient.

Q: Explain why TLS 1.3 reduced the handshake to one round trip, and why downgrade attacks were a real risk against TLS 1.2 deployments.
TLS 1.3 requires the client to send its key share in the very first message (ClientHello), so the server can respond with its own key share, certificate, and a Finished message in a single reply — both sides derive the shared secret from that one exchange. TLS 1.2 negotiated the cipher suite and key exchange method in a separate step before deriving keys, and — critically — still offered legacy cipher suites (RC4, static RSA key exchange with no forward secrecy) for backward compatibility. An attacker positioned on the network path could manipulate the negotiation to force both sides down to the weakest mutually supported option. TLS 1.3 closes this by removing the weak options from the protocol entirely rather than just discouraging their use.

Q: A Spring Boot service sets server.forward-headers-strategy=framework so it can trust X-Forwarded-Proto from its load balancer. What goes wrong if that same service is also reachable directly, bypassing the load balancer?
Any client that reaches the service directly can forge an X-Forwarded-Proto: https header on a genuinely plaintext HTTP request. The framework will treat the connection as secure — Spring Security's requiresSecure() check passes, cookies marked Secure get set and sent over that "internally trusted" plaintext connection, and the entire channel-security guarantee is silently defeated. The fix isn't in application code at all: the application server must be network-unreachable except through the trusted proxy — a security group, firewall rule, or private subnet that makes the direct path physically impossible, not just discouraged.