click the screen · press Enter
← back to blog
Kubernetes Journey · Topic 33

One-Way TLS vs mTLS: Which End Has to Prove Itself

One-way TLS where only the server shows a certificate compared with mutual TLS where the client presents a certificate too, both verified against a certificate authority - Kubernetes Journey

Every HTTPS site you visit checks one thing before it trusts the connection: is the server who it claims to be. It almost never checks the reverse. The server has no idea if you are who you say you are, not from the TLS layer anyway. It finds that out a second later, when you type a password.

That asymmetry is one-way TLS, and it's the right call for the web. Mutual TLS flips it so both sides prove themselves with a certificate before a single byte of the actual request goes across. Here's when you'd want that, and a lab you can run in ten minutes to watch a connection get refused for not having the right cert.

One-way TLS versus mutual TLS: one-way proves only the server and the client logs in with a password, while mutual TLS has both client and server present certificates verified by a certificate authority
The only real difference is whether the client also has to show a certificate. Everything else about the handshake is the same.

What TLS is actually doing

TLS (Transport Layer Security, the thing that puts the padlock on HTTPS) does two jobs at once. It proves you're talking to the right machine, and it encrypts everything after that so nobody in the middle can read or tamper with it.

The proof comes from a certificate, which is a public key plus a name (bank.example.com), signed by a Certificate Authority (a CA, an organisation your machine already trusts). Your browser ships with a list of those CAs. When a server hands over a certificate, the browser checks the signature traces back to a CA on its list, and that the name matches the site. If both hold, it trusts the key. I went through the certificate chain itself in Topic 7 on TLS and PKI, so I'll keep it light here.

Without the certificate check, encryption alone is useless. You'd have a beautifully encrypted channel to an attacker. The certificate is what stops you handing your password to a machine pretending to be your bank.

So TLS is encryption plus a check that the other end is who it claims to be. The only open question is which ends have to prove themselves.

One-way TLS: the server proves itself, you type a password

This is what runs on effectively every website. The server proves its identity with a certificate. You don't. You prove yours the human way, after the secure channel is up, by logging in.

01
Client says hello

Your browser reaches out to the server

02
Server sends its cert

Public key plus name, signed by a CA

03
Client verifies it

Signed by a trusted CA? Name matches? Good

04
Channel is up

Both share a session key, everything after is encrypted

Notice nobody ever checked the client's identity here. The server still has no idea who connected.

And that's completely fine for the web, because a bank can't hand a certificate to every customer and expect them to install and protect it. The server proving itself is the part that actually protects you, since it's what defeats the fake-bank attack. Who you are gets sorted separately, with a password, and one-way TLS makes no attempt to solve it.

SSL, TLS, two-way, mutual, all the names SSL is the old name for the protocol; it became TLS years ago, but "SSL" stuck in habit and product names. So "one-way SSL" and "one-way TLS" are the same thing, and "mutual TLS", "mTLS", "two-way TLS" and "two-way SSL" are all one concept too. I'll say TLS and mTLS from here.

Mutual TLS: both sides show a certificate

Mutual TLS is the same handshake with one addition. The server, after presenting its own certificate, turns around and demands one from the client. No valid client certificate, no connection. The request never even starts.

Both certificates get checked against a CA. The client verifies the server's cert like normal. Then the server verifies the client's cert the same way, tracing its signature back to a CA the server trusts. Only when both checks pass does the encrypted channel open.

The client's identity is now the certificate itself, proven by cryptography, not a password sent after the fact. For traffic between machines, where there's no human to type anything, this is exactly the right shape. Service A holds a certificate, service B will only talk to holders of a certificate its CA signed, and neither one is trusting a shared secret that could leak.

mTLS moves the client's identity into the handshake. The connection gets refused before the request, not after a failed login.

Same handshake, one extra check
 One-way TLSMutual TLS
Server proves itselfYes, certificateYes, certificate
Client proves itselfNo, a password laterYes, certificate, in the handshake
Best fitHumans on the webMachine-to-machine, no human present
Bad connection is refusedAfter login failsBefore the request even starts
One-way suits a bank and its customers. Mutual suits two services that never involve a person.

Hands-on: watch a connection get refused for missing a cert

This is the bit that made it click for me. You need nothing but openssl and curl, both already on any Linux box, and about ten minutes. We build a tiny CA, issue a server cert and a client cert from it, then prove that the server turns away anyone without the client cert.

1. Make a CA. This is our own little Certificate Authority, the thing both certs will trace back to:

openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -subj "/CN=unixsingh-lab-ca" -out ca.crt

2. Issue a server certificate signed by that CA, for the name localhost:

openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=localhost" -out server.csr
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -days 365 -sha256 -out server.crt

3. Issue a client certificate from the same CA. This is the credential the client will present:

openssl genrsa -out client.key 2048
openssl req -new -key client.key -subj "/CN=service-a" -out client.csr
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -days 365 -sha256 -out client.crt

4. Start a server that DEMANDS a client cert. The -Verify 1 flag is the whole point: it tells openssl's test server to require and verify a client certificate signed by our CA:

openssl s_server -accept 8443 \
  -cert server.crt -key server.key \
  -CAfile ca.crt -Verify 1 \
  -www

5. Call it two ways. First without the client cert, then with it. Leave the server running and use a second terminal:

curl --cacert ca.crt https://localhost:8443
curl: (56) OpenSSL SSL_read: error:0A00045C:SSL routines:: tlsv13 alert certificate required == the server hung up. no cert, no conversation ==
curl --cacert ca.crt --cert client.crt --key client.key https://localhost:8443
HTTP/1.0 200 ok s_server output showing the client cert: subject=CN=service-a verify return:1 == server verified the client. channel is open ==
Same command, same server. The only difference is --cert and --key. Without them the server refuses to talk at all.

6. Verify the identity, not just the success. The interesting part is the server log line in the second tab: subject=CN=service-a. The server didn't just accept a connection, it learned who connected, cryptographically, before answering. That CN=service-a is an identity you can make authorisation decisions on. Try it once with a self-signed client cert that our CA didn't issue, and the server rejects that too, because it doesn't trace back to a CA it trusts.

What you just proved In one-way TLS the server would have answered anyone and left the "who are you" question to a login form. Here the server closed the connection on an unauthenticated client before the request existed. That's the entire security value of mTLS in one curl.

Where this shows up in Kubernetes

Two places, and it's worth separating them because people blur them.

First, the control plane already runs on mTLS and has since you installed it. When the kubelet talks to the API server, when the API server talks to etcd, those are mutually authenticated with certificates from the cluster CA. That's most of what the PKI in Topic 7 was setting up. You've been using mTLS this whole series without calling it that.

Second, and this is where it gets interesting for application traffic, a service mesh like Istio or Linkerd can put mTLS between your pods automatically. Each pod gets a sidecar proxy, the mesh issues every workload a short-lived certificate tied to its identity, and pod-to-pod traffic becomes mutually authenticated and encrypted without the application knowing anything about it. That's the practical route to mTLS everywhere inside a cluster, and it's a big part of what "zero trust networking" means in this world: no pod trusts another just because they share a network.

mTLS is authentication, not authorisation This trips people up, so I'll be blunt. mTLS proves who the client is. It says nothing about what that client is allowed to do. A perfectly verified service-a certificate does not mean service-a should be allowed to hit the payments endpoint. You still need a policy layer on top deciding which identity may reach which service, whether that's RBAC, a mesh authorisation policy, or something like OPA. Authentication and authorisation are two jobs, and mTLS only does the first.

What I look for on a test

The interesting findings around TLS are less "is it encrypted" and more "does the check actually happen". A few things I'd probe on an authorised engagement:

# does the endpoint even require a client cert, or does it just offer TLS?
curl -k https://target:8443/                      # works without a cert? not mTLS
curl --cert c.crt --key c.key -k https://target:8443/

# what CA does the server trust for clients, and what does its own cert say
openssl s_client -connect target:8443 -showcerts </dev/null 2>/dev/null

# in a cluster: is the mesh actually enforcing mTLS, or is it in permissive mode
kubectl get peerauthentication -A -o yaml | grep -iE 'mode|mtls'

That last one is the common one. Istio has a permissive mode that accepts both mTLS and plain traffic, meant as a migration aid. Left on, it means the mTLS you think is enforced is optional, and a workload inside the mesh can talk plaintext to a service that looks locked down on the dashboard. "We use mTLS" and "we require mTLS" are different sentences, and the gap between them is where I'd push.

What I would tell past me

The thing I'd want past-me to understand sooner is that one-way and mutual TLS aren't a security ladder where mutual is just "more". They answer different questions. One-way asks "am I really talking to my bank", which is the right question when a human with a password is on one end. Mutual asks "are both of us who we claim", which is the right question when there's no human anywhere and a password would just be a secret waiting to leak.

My honest take: mTLS is genuinely great for service-to-service and a bit of a nightmare for humans. The hard part was never the handshake, it's the certificates. Issuing them, distributing them, rotating them before they expire, revoking them when a key leaks. That's the whole reason a service mesh feels like magic, it makes the certificate lifecycle somebody else's problem, and the whole reason client certs never took off for consumer logins, because you can't make a million people manage a keypair.

Where I'm still finding my feet: I've run the openssl lab and read a lot about Istio, but I haven't operated a mesh doing mTLS at scale, so I can't tell you first-hand what certificate rotation feels like when it goes wrong at 3am. If you've run Istio or Linkerd in anger, I'd like to hear whether the automatic cert handling really is as hands-off as it looks.

Next topic, staying on the theme of many things sharing one cluster safely: multi-tenancy, and how you actually keep tenants apart on shared infrastructure.

References

FAQ

What is the difference between one-way TLS and mutual TLS?

One-way TLS authenticates only the server: the client checks the server's certificate, then usually proves itself with a password. Mutual TLS makes both sides present a certificate, so the server cryptographically verifies the client too. mTLS suits machine-to-machine traffic where no human types a password.

Is mutual TLS the same as two-way SSL?

Yes. Mutual TLS, mTLS, two-way TLS and two-way SSL all name the same thing: a TLS handshake where the client also presents a certificate the server verifies. SSL is the old name for the protocol that became TLS, so the terms get used interchangeably even though modern deployments run TLS.

Does mutual TLS replace passwords?

For service-to-service traffic it usually does, because the client certificate is the identity. For human users it is rarely the only factor, since distributing and protecting client certificates on people's devices is awkward. Most human logins stay on passwords or tokens over one-way TLS.

Does mutual TLS mean I can stop doing authorisation?

No. mTLS proves who the client is, not what it may do. It is authentication, not authorisation. You still need a policy layer deciding which verified identity can reach which endpoint. In Kubernetes that is often a service mesh combined with RBAC or a policy engine.

How do I test mutual TLS locally?

Create a small certificate authority with openssl, issue a server certificate and a client certificate from it, start a server that requires client certificates, then call it with curl using --cert and --key. A call without the client certificate is refused, which is the whole point.