Mutual TLS
Changelog
- 2024-09-26: Init for nginx v1.24.0, OpenSSL v3.0.13
Comparison with VPN
mTLS requires clients to present a client certificate to authenticate usage. The mTLS flow goes roughly like:
- Client sends request to server
- Server responds with server cert (as usual) and set of accepted client cert
- Client validates server cert (as usual) and sends client cert
- Server validates client cert and performs authentication flow
Authentication is facilitated by PKI. The subset of cases where this may be undesirable includes:
- Threat model includes the TLS stack (incl. openssl) and web proxy vulnerabilities
- Plausible deniability of the service is required (vis a vis wireguard which will drop packets with authentication due to its use of UDP)
- An alternative would be to protect an arbitrary surface with mTLS and expose subsets, to achieve the same deniability (up until the existence of a web proxy).
- The server must respond with the list of accepted client certificates as part of the TLS flow, and it makes sense to avoid having the client to see all its client certificates to see which sticks (for efficiency)
Where this is desirable includes:
- No need to use an entire VPN to gain access to specific resources (limit exposure of network surface)
- No need extra dependencies on other libraries, since OpenSSL has had support for mTLS since forever
Setup
Assuming the basic web configuration has already been setup, together with certificates. Then the main changes are:
server { ssl_client_certificate <PATH_TO_CA_CERT>; ssl_verify_client optional; if ($ssl_client_verify != SUCCESS) { ... # reject clients } ... }
If no special behaviour based on the validity of the client certificate is required (processed based on the return code of $ssl_client_verify
), then ssl_verify_client
can be set to "on", and the response will be a simple 400 Bad Request.
Note the ssl_trusted_certificate
directive is mainly for OSCP stapling, not for hiding the list of accepted client certificates from the client. Using this in tandem with ssl_verify_client
without ssl_client_certificate
raises a configuration error.
Interplay with other topology
Consider the following additional considerations:
- For a DMZ setup, use
stream
to route TCP packets to the backend, and poke a hole in the firewall.- Consider using a different port for the webapp to limit access permissions to other virtual hosts.
- May need to add the
bind
option to the "listen" directives.
- Client certificates need to be issued for the specified domain, and installed on the client. One way is using .p12 bundles. The example code below uses EasyRSA with OpenSSL.
# Create CA user:~$ easyrsa init-pki # configure vars, e.g. for ECC user:~$ easyrsa build-ca # Create client certificate user:~$ easyrsa gen-req <MY_CLIENT> user:~$ easyrsa --san=DNS:<MY_DOMAIN> sign-req client <MY_CLIENT> # Verify client certificate, e.g. extended key usage of Client Authentication and SAN user:~$ cat pki/issued/<MY_CLIENT>.crt # Package into p12 bundle (set export password if desired) # Note there is no real need to generate the full chain user:~$ openssl pkcs12 -export \ -in pki/issued/<MY_CLIENT>.crt \ -inkey pki/private/<MY_CLIENT>.key \ -out pki/private/<MY_CLIENT>.p12
- After loading client certificates, do restart the web browser to clear caches regarding client cert requests coming from the webserver.
Client-side installation
Generally per usual for Linux/Windows.
For iOS devices, the .p12 file needs to be created using the -legacy
flag in OpenSSL 3, and then either:
- Distributed via HTTPS endpoint, and open in Safari
- Distributed via email, and open in the iOS Mail app
So many hurdles to leap through... *internal screaming* Even then, the client certificate is added only to Apple's keychain, so these certificates cannot be used in other browsers unless those apps support client cert importing mechanisms, see answer. *more internal screaming*