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:

  1. Generate a private key and CSR on the client machine.

  2. Submit the CSR to your CA (Active Directory Certificate Services, Vault PKI, etc.).

  3. Receive the signed certificate.

  4. 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 dsec (age encryption) if they are also password-protected. Double encryption adds operational complexity with no security benefit — choose one layer. Store unencrypted .key files with chmod 600 and let dsec manage the path references.

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

MTLS_CERT_PATH

Path to client certificate file

MTLS_KEY_PATH

Path to client private key file

MTLS_CA_PATH

Path to CA certificate (or chain) file

MTLS_KEY_PASSPHRASE

Passphrase for encrypted private keys

PXGRID_CERT

pxGrid-specific client certificate

PXGRID_KEY

pxGrid-specific client key

PXGRID_CA

pxGrid-specific CA certificate

Vendor-specific variables (e.g., PXGRID_*) take precedence over the generic MTLS_* variables.