Mutual TLS (mTLS) protects sensitive HTTPS endpoints by requiring a client certificate in addition to the usual server certificate. Command-line access through cURL allows automated jobs, CI pipelines, and troubleshooting tools to authenticate to these endpoints with the same assurance normally provided by browser-based clients.
A PKCS#12 archive, typically stored as .p12 or .pfx, bundles a private key, the corresponding X.509 client certificate, and optionally the issuing certificate chain into a single encrypted file. During the TLS handshake, cURL passes this archive to the TLS backend via the --cert and --cert-type options so the client certificate can prove possession of the private key and satisfy server-side client authentication rules.
PKCS#12 archives carry sensitive private keys and usually require a passphrase, so safe usage depends on restrictive file permissions, explicit declaration of the archive type, and minimal exposure of secrets in shell history or process listings. On Ubuntu, cURL builds linked against OpenSSL treat PEM as the default certificate format, so PKCS#12 usage benefits from an explicit --cert-type P12 flag and careful passphrase handling.
Steps to use PKCS#12 client certificates with cURL:
- Display the installed cURL version and TLS backend in a terminal on Ubuntu.
$ curl --version curl 8.5.0 (aarch64-unknown-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13 zlib/1.3 brotli/1.1.0 zstd/1.5.5 libidn2/2.3.7 libpsl/0.21.2 (+libidn2/2.3.7) libssh/0.10.6/openssl/zlib nghttp2/1.59.0 librtmp/2.3 OpenLDAP/2.6.7 Release-Date: 2023-12-06, security patched: 8.5.0-2ubuntu10.6 Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM PSL SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd ##### snipped #####
The line containing OpenSSL, GnuTLS, or another TLS backend indicates which library handles PKCS#12 parsing and certificate presentation.
- Create a private directory for the PKCS#12 archive under the home folder with restrictive permissions.
$ mkdir -p ~/certs $ chmod 700 ~/certs $ cp /work/docker/certs/client-cert.p12 ~/certs/ $ cp /work/docker/certs/server-ca.pem ~/certs/ $ chmod 600 ~/certs/client-cert.p12 $ ls -l ~/certs/client-cert.p12 -rw------- 1 root root 2611 Jan 10 06:22 /root/certs/client-cert.p12
World-readable archives such as /home/user/certs/client-cert.p12 expose the embedded private key and allow any local user with access to the file to impersonate the mTLS client.
- Observe the error produced when the PKCS#12 archive is used without declaring its type.
$ curl --silent --show-error --cert ~/certs/client-cert.p12:'p12-password' \ --cacert ~/certs/server-ca.pem \ https://api.example.net:4443/mtls curl: (58) could not load PEM client certificate from /root/certs/client-cert.p12, OpenSSL error error:0480006C:PEM routines::no start line, (no key found, wrong pass phrase, or wrong file format?)
This error commonly appears when cURL expects a PEM-encoded certificate but receives a PKCS#12 archive instead.
- Send an mTLS request with cURL using the PKCS#12 archive while explicitly setting the certificate type.
$ curl --silent --cert-type P12 --cert ~/certs/client-cert.p12:'p12-password' \ --cacert ~/certs/server-ca.pem \ https://api.example.net:4443/mtls { "message": "client certificate accepted", "status": "ok" }Placing the PKCS#12 passphrase directly on the command line records it in shell history and can expose it in process listings on multi-user systems.
- Provide the PKCS#12 password through a prompt and reuse it for the request.
$ read -s P12_PASS $ curl --silent --cert-type P12 --cert ~/certs/client-cert.p12:"p12-password" \ --cacert ~/certs/server-ca.pem \ https://api.example.net:4443/mtls { "message": "client certificate accepted", "status": "ok" }Secrets entered with read -s are not echoed to the terminal, and clearing shell history reduces exposure on shared systems.
- Verify mutual TLS authentication by repeating the request with verbose output enabled.
$ curl -v --silent --cert-type P12 --cert ~/certs/client-cert.p12:"p12-password" \ --cacert ~/certs/server-ca.pem \ https://api.example.net:4443/mtls * Host api.example.net:4443 was resolved. * IPv6: (none) * IPv4: 127.0.0.1 * Trying 127.0.0.1:4443... * Connected to api.example.net (127.0.0.1) port 4443 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (IN), TLS handshake, Server hello (2): ##### snipped ##### * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS * Server certificate: > GET /mtls HTTP/1.1 > Host: api.example.net:4443 > User-Agent: curl/8.5.0 > Accept: */* ##### snipped ##### < HTTP/1.1 200 OK < Server: sg-httpbin-lite/1.0 Python/3.12.3 < Date: Sat, 10 Jan 2026 06:22:22 GMT < Content-Length: 64 < Content-Type: application/json ##### snipped ##### { "message": "client certificate accepted", "status": "ok" }Success signals include an SSL connection using TLSv1.x line for the handshake, a HTTP/1.1 200 OK status, and the expected protected resource body instead of certificate or authentication errors.
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.
