Mutual TLS (mTLS) protects internal APIs, service endpoints, and admin interfaces by requiring the client to prove its identity during the TLS handshake. A PKCS#12 archive, usually stored as .p12 or .pfx, lets cURL present that identity from one password-protected file instead of separate certificate and key paths.
In cURL, --cert supplies the client certificate material and --cacert verifies the remote server against the issuing certificate authority. For file-based PKCS#12 archives, --cert-type P12 tells TLS backends that normally expect PEM to load the archive as PKCS#12 instead of trying to parse it as a PEM certificate.
TLS backend choice changes the exact command flow. File-loading backends such as SecureTransport and many OpenSSL-based builds can use a .p12 file directly, while Schannel builds on Windows require the archive to be imported into a certificate store first because cURL does not load PFX files from disk there. The archive and any config file that stores its password should stay readable only by the account that runs the request.
Steps to use PKCS#12 client certificates with cURL:
- Confirm the local cURL TLS backend before choosing the PKCS#12 loading path.
$ curl --version | sed -n '1p' curl 8.7.1 libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6
On PEM-default backends, --cert-type P12 is the switch that prevents the archive from being parsed as a PEM certificate.
If the backend line shows Schannel, import the .p12 or .pfx file into the Windows certificate store first and use a store path such as CurrentUser\MY\<sha1-thumbprint> with --cert. Schannel does not load PFX files directly from disk.
- Restrict the archive and CA bundle to a private certificate directory before the first request.
$ mkdir -p ~/pki/payments-api $ cp payments-api-client.p12 ~/pki/payments-api/ $ cp corp-root-ca.pem ~/pki/payments-api/ $ chmod 700 ~/pki/payments-api $ chmod 600 ~/pki/payments-api/payments-api-client.p12 ~/pki/payments-api/corp-root-ca.pem
A readable PKCS#12 archive exposes the private key needed to authenticate as the same mTLS client.
- Send the request with explicit PKCS#12 type and confirm a successful HTTP status.
$ curl --silent --show-error \ --cacert ~/pki/payments-api/corp-root-ca.pem \ --cert-type P12 \ --cert "~/pki/payments-api/payments-api-client.p12:<p12-password>" \ --write-out 'response=%{http_code}\n' \ https://payments-api.int.example.com:8443/health \ -o /dev/null response=200Replace the example hostname, PKCS#12 archive path, CA bundle path, and <p12-password> token with the values assigned to the protected endpoint. On PEM-default backends, leaving out --cert-type P12 usually ends with curl: (58) because the archive is treated as the wrong file format.
A literal :password suffix is acceptable for short-lived tests but leaves the secret visible in shell history and process arguments on many systems.
- Inspect the verbose handshake when proof of client-certificate exchange or failure details are needed.
$ curl --verbose --silent --show-error \ --cacert ~/pki/payments-api/corp-root-ca.pem \ --cert-type P12 \ --cert "~/pki/payments-api/payments-api-client.p12:<p12-password>" \ https://payments-api.int.example.com:8443/health \ -o /dev/null * Host payments-api.int.example.com:8443 was resolved. * IPv4: 192.0.2.24 * Trying 192.0.2.24:8443... * Connected to payments-api.int.example.com (192.0.2.24) port 8443 ##### snipped ##### * (304) (IN), TLS handshake, Request CERT (13): ##### snipped ##### * SSL certificate verify ok. > GET /health HTTP/1.1 > Host: payments-api.int.example.com:8443 > User-Agent: curl/8.7.1 ##### snipped ##### < HTTP/1.1 200 OK
Look for the server's Request CERT line, a successful certificate verification message, and a normal HTTP status before reusing the command in scripts or CI.
- Move the password into a restricted cURL config file before unattended runs.
$ cat > ~/pki/payments-api/payments-api-mtls.conf <<'EOF' silent show-error cert-type = P12 cert = "/home/service-account/pki/payments-api/payments-api-client.p12:<p12-password>" cacert = /home/service-account/pki/payments-api/corp-root-ca.pem url = https://payments-api.int.example.com:8443/health output = /dev/null write-out = "response=%{http_code}\n" EOF $ chmod 600 ~/pki/payments-api/payments-api-mtls.conf $ curl --config ~/pki/payments-api/payments-api-mtls.conf response=200The config file now contains the certificate password, so keep it outside version control and readable only by the service account that runs the transfer.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.
