SSL certificates

Set up mutual TLS (client certificate authentication) on Nginx

Protect admin panels and APIs with mutual TLS: create a private CA with OpenSSL, issue client certificates, require them in Nginx, pass identity to the app and revoke access.

AdvancedLinux VPS3 min read

In normal HTTPS only the server proves its identity. With mutual TLS (mTLS) the client must present a certificate too, so only devices you issued a certificate to can even reach the application. It is a strong extra layer for admin panels, internal tools and server-to-server APIs. This guide sets up mutual TLS on Nginx.

Before you start

  • A Linux server with Nginx and a normal server certificate (for example from Let's Encrypt).
  • OpenSSL installed.
  • Keep a way in that doesn't depend on mTLS (SSH) while testing.

Create a private certificate authority

Client certificates are issued by your own CA, not a public one. Work in a protected directory such as /root/mtls-ca:

openssl ecparam -name prime256v1 -genkey -noout -out ca.key
openssl req -x509 -new -key ca.key -sha256 -days 3650 -subj "/CN=Example Internal CA" -out ca.crt

Keep ca.key secret and offline if possible – whoever has it can issue valid client certificates.

Issue a client certificate

openssl ecparam -name prime256v1 -genkey -noout -out alice.key
openssl req -new -key alice.key -subj "/CN=alice" -out alice.csr
openssl x509 -req -in alice.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -sha256 -out alice.crt
openssl pkcs12 -export -inkey alice.key -in alice.crt -certfile ca.crt -out alice.p12

Give alice.p12 (protected with the export password) to the user to import into their browser or OS keychain.

Require client certificates in Nginx

  1. Copy ca.crt to /etc/nginx/mtls/ca.crt.
  2. In the HTTPS server block:
    ssl_client_certificate /etc/nginx/mtls/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;
  3. To protect only one area, use ssl_verify_client optional; at server level and check in a location:
    location /admin/ {
        if ($ssl_client_verify != SUCCESS) { return 403; }
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Client-CN $ssl_client_s_dn;
    }
  4. Test and reload: sudo nginx -t && sudo systemctl reload nginx.

The app can read X-Client-CN to know which certificate holder connected (only trust it from the proxy).

Test it

  • Without a certificate: curl -I https://admin.example.com → 400 or 403.
  • With one: curl -I --cert alice.crt --key alice.key https://admin.example.com → 200.

Revoke access

  1. Maintain a CRL with an OpenSSL CA configuration (openssl ca -revoke and openssl ca -gencrl), or simply rotate the whole CA for small setups.
  2. Point Nginx at the CRL: ssl_crl /etc/nginx/mtls/ca.crl; and reload.
  3. Regenerate the CRL before its next-update date, or Nginx rejects all clients.

Troubleshooting

  • "400 No required SSL certificate was sent": the browser has no certificate from your CA installed, or didn't offer it – restart the browser after import.
  • "The SSL certificate error": the client certificate wasn't signed by the CA Nginx trusts, or has expired.
  • Behind Cloudflare or another proxy: the proxy terminates TLS, so client certificates don't reach Nginx. Use the proxy's own mTLS feature instead.

FAQ: mutual TLS

Does mTLS replace passwords?

It's best as an extra layer: certificate plus normal login. Certificates can be stolen with the device.

Can I use mTLS for APIs between servers?

Yes – it's a common way to authenticate services to each other without shared secrets in headers.

Why use my own CA instead of a public one?

Public CAs would let anyone with any public certificate in; your own CA means only certificates you issued are accepted.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.