· ~4 min read
Building a PKI with mutual TLS, and what a MitM does without it
A two-tier certificate authority, an Apache server on elliptic-curve keys that only accepts clients with a certificate, and a man-in-the-middle simulation on an unprotected LAN that shows why these controls exist.
Goal
Build the certificate infrastructure a small organisation would need, lock a web service behind mutual TLS, and then look at the same kind of network from the attacker's side, to see what goes wrong when layer 2 and TLS are not protected. The defensive part is the core of this lab; the attack is there to justify it.
The commands and screenshots are in the repository on GitHub.
A two-tier certificate authority
| Tier | Role | Where it lives |
|---|---|---|
| Root CA (RSA 4096, key encrypted with AES-256) | Signs only the intermediate | Offline |
| Intermediate CA (RSA 4096) | Signs servers and clients day to day | Online, replaceable |
| Server (ECDSA P-256) | The Apache web server | Web server |
| Client (RSA 4096, exported as .p12) | A user's browser | User's machine |
The point of the second tier is damage control. If the intermediate key is compromised, the root revokes it and issues a new one; nothing that trusts the root has to change. If the root itself were online and stolen, the whole hierarchy would have to be rebuilt.
The server uses an elliptic-curve key (P-256): the same security level as a much longer RSA key, with smaller handshakes and less CPU per connection.
Mutual TLS on Apache
Ordinary TLS proves the server to the client. Mutual TLS also makes the client prove itself, with a certificate from a CA the server trusts:
SSLCertificateFile server.crt
SSLCertificateKeyFile server.key
SSLCertificateChainFile chain.crt
SSLCACertificateFile chain.crt
SSLVerifyClient require
SSLVerifyDepth 2
SSLVerifyClient require refuses the TLS handshake to anyone without a valid client certificate; there is no login page to attack because the connection never gets that far. SSLVerifyDepth 2 allows exactly the chain client, intermediate, root. The configuration was then checked with sslscan.
The other side: a man-in-the-middle on an unprotected LAN
In a separate simulation, on an isolated lab network, I played the attacker against a victim machine on the same LAN. ARP poisoning put the attacker between the victim and the gateway, and a cloned login page on a plain HTTP address collected the credentials the victim typed. Nothing about it was sophisticated, and that is the lesson: on a flat LAN with no layer-2 protection, a user who does not check the address bar is one step from handing over a password.
The same attack does nothing against the mTLS service above: the attacker cannot present the server's certificate, and cannot complete the handshake without a client certificate either.
Mitigations
- Dynamic ARP Inspection (with DHCP snooping) on the switches, so forged ARP replies are dropped at the port.
- Port security or 802.1X, so an unknown machine does not get a working port in the first place.
- HSTS on real sites, so browsers refuse to talk to them over plain HTTP and SSL-stripping fails.
- Phishing-resistant authentication (FIDO2 security keys or passkeys) and, for internal services, mTLS: credentials that cannot be typed into a fake page cannot be harvested by one.
- User awareness, last rather than first: training helps, but controls that do not depend on someone noticing a missing padlock are stronger.
What I learned
Re-reading my own write-up, I would correct and add a few things.
- HSTS does not stop a cloned page on another address. I originally wrote that HSTS would neutralise the cloned login page. It protects the real domain from being downgraded to HTTP; it does nothing for a lookalike page served from a different address. Against credential phishing, the controls that work are phishing-resistant authentication and network controls, not HSTS.
- Make certificate extensions explicit. Browsers ignore the common name and require a subjectAltName for the server's hostname, and a CA certificate should say so with
basicConstraints = CA:TRUE(withpathlen:0on the intermediate). Signing withopenssl x509 -reqand no extension file leaves these to chance; I would sign from a config file with the extensions written out. - Protect the exported client key. A .p12 file holds the client's private key; it needs a strong export password and should be treated like the password it replaces.
- Plan revocation. As with any PKI, there should be a CRL or OCSP responder so a lost laptop's client certificate can be revoked before it expires.
A personal lab on an isolated virtual network; the attack ran only against my own lab machines.
Want the configs or code from this lab, or have a question? Email me at papas@viotnet.com