---
title: "mTLS vs TLS Explained: Two Identities, One Connection"
description: "mTLS vs TLS explained through the handshake, certificate trust, authorization and proxy boundaries, with a reproducible local demo and eight checked outcomes."
author: Aleksei Aleinikov
date: 2026-11-04
lang: en
tags: [mtls, tls, mutual-tls, authentication, api-security, certificates, zero-trust]
canonical: https://www.alekseialeinikov.com/en/blog/topics/security/mtls-vs-tls-explained
source: alekseialeinikov.com
---

# mTLS vs TLS Explained: Two Identities, One Connection

Your client checks the certificate, the connection is encrypted, and the API shows a reassuring padlock. None of that tells the API who is calling. A stranger can establish the same ordinary HTTPS connection and ask for the same endpoint. The application still needs to decide what to do with the request.

**Mutual TLS adds a second identity check: the server asks the client to prove itself too.** That is a useful boundary for services, devices and partner APIs. But the surprising part comes next: a client can present a perfectly valid certificate, complete mTLS, and still deserve `403 Forbidden`. This article follows that distinction from the handshake to a working local example.

![mTLS vs TLS: two identity passports face a separate access gate, illustrating mutual authentication and the authorization decision that follows.](https://www.alekseialeinikov.com/blog/mtls-vs-tls.webp)

## What is mTLS?

**mTLS is TLS with mutual certificate authentication, not a separate transport protocol.** In the familiar certificate-based HTTPS case, the client authenticates the server. With mTLS, the server also requests and verifies a client certificate and proof that the client controls its private key. This is the meaning used in [RFC 8705, section 1.2](https://www.rfc-editor.org/rfc/rfc8705.html#section-1.2).

The qualification matters: TLS also has PSK and resumption modes. “TLS authenticates only the server” is a useful description of ordinary certificate-based HTTPS, not a definition covering every possible TLS handshake.

## mTLS vs TLS: the key differences

| Question | Ordinary server-authenticated TLS | mTLS |
|---|---|---|
| Does the client authenticate the server? | Yes | Yes |
| Does TLS authenticate the client with a certificate? | No | Yes, when required and successfully verified |
| Does the client need its own certificate and private key? | No | Yes |
| Is application data encrypted? | Yes | Yes |
| Is the encryption automatically stronger? | No | No extra encryption layer is implied |
| Who decides whether `/orders` is allowed? | Application or gateway policy | Application or gateway policy |
| What must operations manage? | Server credentials and trust | Both sets of credentials, trust and client policy |

A useful analogy is calling a business whose number you have verified. You know whom you reached; the person answering has not established who you are. mTLS adds the return identity check. It does not give the caller permission to approve a refund.

### mTLS vs HTTPS: can an HTTPS API use mTLS?

Yes. HTTPS is HTTP communication secured with TLS, as described in [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.2). An HTTPS endpoint can additionally require client-certificate authentication: that is HTTPS using mTLS. They are not competing choices. The extra certificate establishes the connecting client's identity; the application can still require a session or access token and enforce permissions.

## The TLS 1.3 handshake: a certificate is only half the proof

A certificate is public. Copying `client.crt` does not copy the ability to authenticate as that client. The private key supplies the missing proof: **CertificateVerify signs handshake material**, binding possession of that key to this connection.

![mTLS vs TLS handshake: both flows authenticate the server; the mutual flow adds CertificateRequest, client Certificate and client CertificateVerify before the client Finished.](https://www.alekseialeinikov.com/blog/mtls-vs-tls-handshake.webp "A condensed full TLS 1.3 handshake. The extra client proof is inside the encrypted handshake.")

In a full certificate-based TLS 1.3 handshake:

1. `ClientHello` and `ServerHello` establish the parameters and key exchange.
2. The server sends its encrypted handshake messages, including its certificate, proof and `Finished`. For mTLS it also sends `CertificateRequest`.
3. The client verifies the server. If requested, it sends its certificate and `CertificateVerify`, followed by `Finished`.
4. The server verifies the client before admitting it under the configured client-authentication policy.

TLS 1.3 encrypts the client-certificate exchange. In a typical initial TLS 1.2 handshake, that certificate was visible to a passive observer. If the client has no suitable certificate, it sends an empty certificate message; the server can abort or continue without certificate authentication. **Required mTLS must actually reject the unauthenticated case.**

The current TLS 1.3 specification is Eric Rescorla's [RFC 9846](https://datatracker.ietf.org/doc/rfc9846/), published in July 2026 and obsoleting RFC 8446. The wire version remains TLS 1.3. Its publication is not a claim that every library has implemented every revision.

The picture shows a full handshake. Resumption and 0-RTT are separate cases; do not assume a fresh client certificate was checked for early data. Early data can also be replayed. For a sensitive API, keep it disabled unless the application deliberately implements an appropriate replay-safe design.

## Three checks that should never collapse into one

Consider two services: `checkout` and `reporting`. Both have certificates from the same trusted issuer. Only `checkout` may submit orders.

| Check | Question | Example failure |
|---|---|---|
| Certificate validation | Does this credential satisfy the configured trust, validity and purpose rules? | Unknown issuer, expired certificate, wrong EKU |
| Peer identity | Which identity or exact credential did the verified peer prove? | Credential belongs to `reporting`, not `checkout` |
| Authorization | May that peer perform this operation on this resource? | Authenticated `reporting` calls a checkout-only endpoint |

![mTLS authorization: certificate verification, peer identity and request permission are three gates; a trusted reporting client passes TLS but receives HTTP 403 at the orders policy.](https://www.alekseialeinikov.com/blog/mtls-vs-tls-authorization.webp "Trusting an issuer does not mean authorizing every certificate it issues.")

An issuer can legitimately sign credentials for hundreds of workloads. Accepting every one as an administrator would be an application-policy mistake, even if all cryptography were correct. A certificate's identity also needs an agreed meaning: an expected SAN, a registered credential, or a workload identity whose issuer and trust domain you control. Reading a plausible common name from an arbitrary certificate is insufficient.

This is also why “mTLS blocks malicious API requests” is too broad. It can reject clients that cannot satisfy the certificate policy. A compromised client with a valid key can still send harmful requests. Input validation, authorization, rate limits and containment remain necessary.

> [!KEY]
> A valid certificate answers a trust question. Permission to read another customer's order is a different question.

## A local mTLS example with eight checked outcomes

The [complete demo](https://www.alekseialeinikov.com/examples/mtls-vs-tls/demo.mjs) uses Node.js built-ins and the installed OpenSSL CLI. It creates separate temporary server and client CAs, issues test certificates, binds only to `127.0.0.1`, runs the cases and removes the temporary keys afterward. No npm dependencies or production credentials are needed.

Download the source, inspect it, and run it in an empty working directory:

```bash
node --version
openssl version
node demo.mjs
```

The core server configuration is small:

```js title="demo.mjs — TLS verification"
const server = https.createServer({
  key: serverKey,
  cert: serverCertificate,
  ca: clientCa,
  requestCert: true,
  rejectUnauthorized: true,
  minVersion: 'TLSv1.3',
  maxVersion: 'TLSv1.3',
}, handler);
```

`requestCert` asks for the client credential. `rejectUnauthorized` makes verification failures fatal. The custom `ca` identifies the client issuer trusted by this server; the client separately trusts the server CA and checks the server's hostname. These are independent directions of trust. [Node.js TLS options](https://nodejs.org/api/tls.html#tlscreateserveroptions-secureconnectionlistener).

Then the handler makes a deliberately narrow authorization decision:

```js title="demo.mjs — application authorization"
const allowed = new X509Certificate(approvedClientPem).fingerprint256;

function handler(req, res) {
  const permitted = req.method === 'GET' && req.url === '/orders' &&
    req.socket.authorized &&
    req.socket.getPeerCertificate().fingerprint256 === allowed;
  res.writeHead(permitted ? 200 : 403);
  res.end(permitted ? 'orders accepted\n' : 'access denied\n');
}
```

This lab pins one exact leaf certificate to make the second decision visible. It is not a general identity directory: renewing that certificate requires updating the pin. Production workload policies usually map an established identity to permissions instead of maintaining leaf fingerprints by hand.

The following outcomes were reproduced on November 4, 2026, with Node.js 24.22.2 and 26.13.0, using OpenSSL CLI 4.1.0:

| Case | Observed result | Where it stops |
|---|---|---|
| Approved client, correct server name | HTTP `200` | Request accepted |
| Approved client requests `/admin` | HTTP `403` | Application resource policy |
| No client certificate | Connection rejected | TLS client authentication |
| Certificate from an untrusted client CA | Connection rejected | TLS certificate validation |
| Trusted issuer, but only `serverAuth` EKU | Connection rejected | TLS certificate purpose validation |
| Valid client certificate not on the application allowlist | HTTP `403` | Application authorization |
| Same unapproved client adds a forged `Client-Cert` header | HTTP `403` | Application still uses the TLS peer, not the header |
| Approved client connects using the wrong server name | Connection rejected | Client's server-identity check |

The success and `403` cases negotiated TLS 1.3. The rejected connections produced no application response. Error strings vary by runtime; for example, the client saw `ECONNRESET` for some server-side verification failures while the server reported a TLS verification error. The script checks those failures rather than counting any timeout as success.

These are functional checks, **not a throughput benchmark**. They demonstrate the boundary between a failed handshake and a denied authenticated request. The forged-header case uses a direct server that ignores such headers; it does not simulate or certify a production proxy deployment.

## The reverse-proxy trap: where does mTLS end?

A load balancer changes the story. If the client completes mTLS with the edge, the edge authenticated the client. A new TLS connection from edge to application proves something about that second peer. It does not automatically transport the original client's identity.

![mTLS reverse proxy trust boundary: the client authenticates to the edge, a separate protected connection reaches the application, and direct access or forged certificate headers must be blocked.](https://www.alekseialeinikov.com/blog/mtls-vs-tls-proxy.webp "TLS termination creates an identity handoff. The application must trust the handoff's sender and its processing rules.")

An application that accepts `Client-Cert: admin` from any reachable caller has turned a proof of private-key possession into a user-controlled string.

There is an IETF specification for this handoff: **RFC 9440, Client-Cert HTTP Header Field**, published in 2023 as an Informational RFC. It defines `Client-Cert` and `Client-Cert-Chain`; it does not make arbitrary headers trustworthy. The proxy removes or overwrites incoming instances, validates the client under its policy and supplies the verified certificate information. The origin needs a trustworthy relationship with that proxy and protection against bypassing it. [RFC 9440, processing rules and security considerations](https://www.rfc-editor.org/rfc/rfc9440.html#section-2.4).

For an actual deployment, establish three properties together:

- The application is reachable through the intended proxy path; alternate ingress cannot inject an identity.
- The proxy-to-origin connection and peer are appropriately protected and authenticated.
- Identity headers come from the proxy's validated connection state, with client-supplied values removed or rejected.

“HTTPS on both hops” alone does not establish these properties. Inspect the selected product's forwarding format and trust configuration; `X-Forwarded-Client-Cert` conventions are not interchangeable with RFC 9440's encoding.

## mTLS vs OAuth and JWT: different jobs, useful together

mTLS can establish the calling service's identity. OAuth can describe delegated access, and a JWT may carry user or permission claims. An order placed by a checkout service on behalf of a customer needs both stories: **which service connected, and what that request is allowed to do for that customer**. The [JWT explainer](https://www.alekseialeinikov.com/en/blog/topics/security/jwt-explained-json-web-token) covers the token's verification side.

Merely adding mTLS to a token endpoint does not make every access token resistant to theft. RFC 8705 defines **certificate-bound access tokens**. The resource server must enforce the binding to the certificate presented on its connection, using `cnf.x5t#S256` for the JWT thumbprint representation. Authentication at the token endpoint and binding the resulting token are distinct features. Certificate changes must also be coordinated with bound tokens. [RFC 8705, sections 3 and 6.3](https://www.rfc-editor.org/rfc/rfc8705.html#section-3).

For a public browser application, ordinary HTTPS with suitable session or OAuth handling is often the practical choice. Managed devices and machine-to-machine clients are a more natural fit for client certificates. For webhook delivery, transport identity can be an additional check alongside the provider's request signature; see [webhook authentication and reliable delivery](https://www.alekseialeinikov.com/en/blog/topics/architecture/webhooks-explained-signatures-retries-idempotency).

## Certificate operations: the part a handshake diagram hides

The CA is where the identity story starts. Decide who may enroll, which workload or partner receives which identity, and what the verifier is allowed to trust. Giving every environment the same broad trust bundle makes it easier to accept a valid credential in the wrong place.

A useful production checklist is concrete:

| Decision | Operational question |
|---|---|
| Enrollment | What evidence allows a workload to obtain this identity? |
| Purpose | Does the leaf permit the intended client or server authentication usage? |
| Key custody | Which process can access the private key, and what happens if it is stolen? |
| Rotation | Can a replacement key, certificate and trust bundle arrive without an outage? |
| Emergency withdrawal | How do you stop an identity now, including active sessions and resumption policy? |
| Authorization | Which identity may call which method and resource? |

Short lifetimes reduce the exposure window. They do not instantly revoke a stolen credential, and certificate expiry is not a timer that necessarily tears down an already established application session. Plan policy withdrawal and connection handling, not just renewal alerts.

SPIFFE gives workload identity a defined form; an X.509-SVID carries a SPIFFE ID within a trust domain. That is useful when workloads are shorter-lived than machines. It still needs a trusted issuance and validation system. [SPIFFE concepts](https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/).

In Istio, **PeerAuthentication** controls whether peers must use mTLS; `PERMISSIVE` accepts both mTLS and plaintext, while `STRICT` requires mTLS in its applicable scope. **AuthorizationPolicy** supplies the request-access rules. Enabling strict peer authentication is not itself a checkout-only allowlist. [PeerAuthentication](https://istio.io/latest/docs/reference/config/security/peer_authentication/), [AuthorizationPolicy](https://istio.io/latest/docs/reference/config/security/authorization-policy/). The [Cilium vs Calico comparison](https://www.alekseialeinikov.com/en/blog/topics/devops/cilium-vs-calico-kubernetes-cni) covers where this fits alongside network encryption and policy.

## A change that can break a working certificate setup

Let's Encrypt ended issuance with the **clientAuth EKU** on July 8, 2026; the default `classic` profile had removed it on February 11. Its server certificates remain useful for authenticating servers. If a deployment reused them as client credentials, it needs a suitable replacement on the client side. [Let's Encrypt's announcement and updated schedule](https://letsencrypt.org/2025/05/14/ending-tls-client-authentication).

This is not a ban on every client-authentication certificate from every CA. An internal service fleet commonly uses a private PKI because it controls enrollment and trust. The server certificate and client certificate can come from different issuers, as the demo demonstrates.

## Which setup fits your system?

| Situation | Starting point |
|---|---|
| Public website with arbitrary browsers | Server-authenticated TLS plus application sessions or OAuth |
| Internal services with controlled workload identities | mTLS plus explicit service and request authorization |
| Partner API with a defined client population | mTLS with a clear enrollment, renewal and incident process |
| Delegated API access where token theft matters | Evaluate certificate-bound tokens and enforce the resource-side binding |
| Application behind a TLS-terminating edge | Design and verify the identity handoff before trusting forwarded claims |

The useful question is not whether another certificate makes a connection look more secure. It is **which identity is established, who trusts the issuer, where that identity survives termination, and which action it authorizes**. If the answer stops at “the handshake succeeded,” the access-control design is unfinished.
