Mutual TLS is common on internal APIs, private package feeds, and regulated download endpoints where a password prompt is not acceptable. Presenting a client certificate from wget keeps those transfers scriptable while still binding access to a revocable machine or service identity.
During the TLS handshake, wget can present a client certificate with --certificate and the matching private key with --private-key, while --ca-certificate points to the CA bundle that validates the server. Current GNU wget still treats PEM as the default format, with --certificate-type and --private-key-type available when the files are stored as DER instead.
Client-certificate workflows fail most often because the key does not match the certificate, the server CA chain is incomplete, or the files are readable by other accounts. Keep certificate material in a private directory, test the handshake with spider mode before pulling a large payload, and persist defaults in ~/.wgetrc only when the same identity is reused regularly.
Steps to use client certificates with wget:
- Confirm that the installed wget build exposes the client-certificate options needed for mutual TLS.
$ wget --help | grep -E -- '--certificate=FILE|--private-key=FILE|--ca-certificate=FILE' --certificate=FILE client certificate file --private-key=FILE private key file --ca-certificate=FILE file with the bundle of CAsCurrent GNU wget still accepts PEM by default; add --certificate-type=DER or --private-key-type=DER only when the files are not in PEM format.
- Export PEM files only when the client identity arrives as a .p12 or .pfx bundle instead of separate certificate and key files.
$ openssl pkcs12 -in client.p12 -clcerts -nokeys -out client-cert.pem $ openssl pkcs12 -in client.p12 -nocerts -nodes -out client-key.pem
The second command writes the private key without passphrase protection so non-interactive jobs can use it; keep that file in a restricted directory.
- Stage the certificate, private key, and CA bundle in a restricted directory and lock down the file permissions before the first request.
$ install -d -m 700 "$HOME/.local/share/wget/mtls" $ install -m 600 client-cert.pem "$HOME/.local/share/wget/mtls/client-cert.pem" $ install -m 600 client-key.pem "$HOME/.local/share/wget/mtls/client-key.pem" $ install -m 600 ca-chain.pem "$HOME/.local/share/wget/mtls/ca-chain.pem" $ ls -l "$HOME/.local/share/wget/mtls" total 24 -rw------- 1 user user 1946 Mar 27 06:55 ca-chain.pem -rw------- 1 user user 1824 Mar 27 06:55 client-cert.pem -rw------- 1 user user 3272 Mar 27 06:55 client-key.pem
Readable private keys let any local user impersonate the client identity for the protected service.
- Inspect the client certificate metadata before presenting it to the remote service.
$ openssl x509 -in "$HOME/.local/share/wget/mtls/client-cert.pem" -noout -subject -issuer -enddate subject=CN = build-agent-01.internal.example issuer=CN = Internal Issuing CA notAfter=Dec 12 06:15:01 2029 GMT
Confirm the subject, issuing CA, and expiry against the record from the PKI team before the first live connection.
- Validate the handshake first with spider mode so certificate and trust errors appear before a full download starts.
$ wget --spider \ --certificate="$HOME/.local/share/wget/mtls/client-cert.pem" \ --private-key="$HOME/.local/share/wget/mtls/client-key.pem" \ --ca-certificate="$HOME/.local/share/wget/mtls/ca-chain.pem" \ https://mtls.internal.example/reports/nightly.csv Spider mode enabled. Check if remote file exists. --2026-03-27 06:55:21-- https://mtls.internal.example/reports/nightly.csv Resolving mtls.internal.example (mtls.internal.example)... 192.0.2.50 Connecting to mtls.internal.example (mtls.internal.example)|192.0.2.50|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 84231 [text/csv] Remote file exists.
A certificate or trust failure at this stage usually points to the wrong CA bundle, the wrong certificate/key pair, or a host name that does not match the server certificate.
- Run the actual transfer with the same certificate, key, and CA bundle after the handshake check succeeds.
$ wget \ --certificate="$HOME/.local/share/wget/mtls/client-cert.pem" \ --private-key="$HOME/.local/share/wget/mtls/client-key.pem" \ --ca-certificate="$HOME/.local/share/wget/mtls/ca-chain.pem" \ https://mtls.internal.example/reports/nightly.csv --2026-03-27 06:55:29-- https://mtls.internal.example/reports/nightly.csv Resolving mtls.internal.example (mtls.internal.example)... 192.0.2.50 Connecting to mtls.internal.example (mtls.internal.example)|192.0.2.50|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 84231 [text/csv] Saving to: 'nightly.csv' 0K ...................................................... 100% 11.8M=0.007s 2026-03-27 06:55:29 (11.8 MB/s) - 'nightly.csv' saved [84231/84231]Keep the first authenticated transfer small when possible so certificate problems are resolved before a large artifact is requested.
- Persist the identity in the user startup file only when the same account repeatedly talks to the same protected service.
~/.wgetrc certificate = /home/user/.local/share/wget/mtls/client-cert.pem private_key = /home/user/.local/share/wget/mtls/client-key.pem ca_certificate = /home/user/.local/share/wget/mtls/ca-chain.pem
Only write account-specific absolute paths into ~/.wgetrc; do not copy shared examples with someone else's home directory.
- Verify the downloaded payload before handing it to a downstream job.
$ head -n 3 nightly.csv date,total,status 2026-03-27,42,complete $ ls -lh nightly.csv -rw-r--r-- 1 user user 82K Mar 27 06:55 nightly.csv
Successful mutual TLS is not enough on its own; the saved file still needs the expected name, size, and content marker.
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.
