Mutual TLS / Client Certificates
How mTLS Works
In standard TLS, only the server proves its identity — the client verifies the server’s certificate. In mutual TLS (mTLS), both sides present certificates. The server verifies the client’s certificate against a trusted CA, establishing cryptographic identity without passwords or tokens.
Client Server
| |
|-- ClientHello ---------------------->|
|<- ServerHello + Server Certificate --|
|<- CertificateRequest ---------------| <-- Server asks for client cert
|-- Client Certificate + Verify ----->|
| |-- Verify client cert against CA
|<========= Encrypted channel ========>|
Key properties:
-
No shared secrets. Authentication is based on PKI, not passwords.
-
Non-repudiable. The client proved possession of a private key.
-
CA-controlled. Revoking a certificate immediately revokes access.
Certificate Generation
Self-Signed CA (Lab/Development)
# Generate a CA key and certificate
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -keyout ca.key -out ca.crt \
-subj "/CN=Lab Root CA/O=DomusDigitalis"
# Generate a client key and CSR
openssl req -newkey rsa:2048 -nodes \
-keyout client.key -out client.csr \
-subj "/CN=netapi-client/O=DomusDigitalis"
# Sign the client certificate with the CA
openssl x509 -req -in client.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt -days 365 -sha256
# Verify the chain
openssl verify -CAfile ca.crt client.crt
Enterprise CA (Production)
In production, certificates come from your organization’s PKI. The process is typically:
-
Generate a private key and CSR on the client machine.
-
Submit the CSR to your CA (Active Directory Certificate Services, Vault PKI, etc.).
-
Receive the signed certificate.
-
Install the certificate and key with correct permissions.
# Generate key and CSR for enterprise submission
openssl req -newkey rsa:2048 -nodes \
-keyout client.key -out client.csr \
-subj "/CN=netapi-client.inside.domusdigitalis.dev/O=DomusDigitalis"
# Submit client.csr to your CA, receive client.crt back
# Lock down permissions
chmod 600 client.key
chmod 644 client.crt
curl with Client Certificates
# Basic mTLS request
curl -s \
--cert client.crt \
--key client.key \
--cacert ca.crt \
https://api.example.com/v1/resources
Flags explained:
-
--cert— the client certificate to present -
--key— the private key matching the certificate -
--cacert— the CA certificate to verify the server (use system CA store if omitted)
pxGrid Example
curl -s \
--cert ~/.secrets/certs/d000/ise/pxgrid-client.cer \
--key ~/.secrets/certs/d000/ise/pxgrid-client.key \
--cacert ~/.secrets/certs/d000/ise/ROOT-CA.crt \
-H "Accept: application/json" \
-X POST \
-d '{"startTimestamp": "2026-01-01T00:00:00Z"}' \
https://ppan.inside.domusdigitalis.dev:8910/pxgrid/ise/radius
PKCS#12 Bundle
Some systems use a combined .p12 / .pfx file containing both the certificate and key:
# Create a PKCS#12 bundle
openssl pkcs12 -export \
-in client.crt -inkey client.key \
-out client.p12 -name "netapi-client"
# Use with curl
curl -s \
--cert-type P12 \
--cert client.p12:passphrase \
--cacert ca.crt \
https://api.example.com/v1/resources
Use Cases
-
Cisco pxGrid 2.0 — mTLS is the only authentication method. Certificates are registered through ISE admin.
-
Banking and financial APIs — PSD2/Open Banking mandates mTLS for API consumers.
-
Service mesh — Istio, Linkerd, and Consul Connect use mTLS between all services.
-
Internal microservices — Zero-trust architectures use mTLS to eliminate network-based trust.
-
IoT device authentication — Devices carry certificates provisioned at manufacturing time.
CA Chain Validation
When the server’s certificate is signed by an intermediate CA, you must provide the full chain:
# Concatenate the chain (order matters: intermediate first, root last)
cat intermediate-ca.crt root-ca.crt > ca-chain.crt
# Use the chain for verification
curl -s \
--cert client.crt \
--key client.key \
--cacert ca-chain.crt \
https://api.example.com/v1/resources
Debugging Certificate Issues
# View certificate details
openssl x509 -in client.crt -text -noout
# Test the TLS handshake (shows full chain exchange)
openssl s_client -connect api.example.com:443 \
-cert client.crt -key client.key -CAfile ca-chain.crt
# Verify certificate dates
openssl x509 -in client.crt -noout -dates
# Check if key matches certificate
diff <(openssl x509 -in client.crt -noout -modulus) \
<(openssl rsa -in client.key -noout -modulus)
dsec Pattern
Certificate files are not environment variables — they are files on disk. dsec stores the paths and any associated passphrases:
dsec edit d000 dev/network
Inside the encrypted file:
MTLS_CERT_PATH=~/.secrets/certs/d000/ise/pxgrid-client.cer
MTLS_KEY_PATH=~/.secrets/certs/d000/ise/pxgrid-client.key
MTLS_CA_PATH=~/.secrets/certs/d000/ise/ROOT-CA.crt
MTLS_KEY_PASSPHRASE=optional-key-passphrase
For pxGrid specifically, use the dedicated variables:
PXGRID_CERT=~/.secrets/certs/d000/ise/pxgrid-client.cer
PXGRID_KEY=~/.secrets/certs/d000/ise/pxgrid-client.key
PXGRID_CA=~/.secrets/certs/d000/ise/ROOT-CA.crt
PXGRID_NODE_NAME=netapi-pxgrid-01
|
Never encrypt private keys with |
netapi Pattern
netapi reads certificate paths from the environment or CLI flags:
# From environment (after dsource)
netapi --auth mtls ise pxgrid sessions
# Explicit flags
netapi --auth mtls \
--cert ~/.secrets/certs/d000/ise/pxgrid-client.cer \
--key ~/.secrets/certs/d000/ise/pxgrid-client.key \
--cacert ~/.secrets/certs/d000/ise/ROOT-CA.crt \
ise pxgrid sessions
Environment Variables
| Variable | Purpose |
|---|---|
|
Path to client certificate file |
|
Path to client private key file |
|
Path to CA certificate (or chain) file |
|
Passphrase for encrypted private keys |
|
pxGrid-specific client certificate |
|
pxGrid-specific client key |
|
pxGrid-specific CA certificate |
Vendor-specific variables (e.g., PXGRID_*) take precedence over the generic MTLS_* variables.