HardStacked
Share:

OpenSSL · Debian 13

update-ca-certificates "0 added, 0 removed": the same output whether your certificate worked or not

Published · HardStacked · 9 min

You dropped your internal CA's certificate where the docs say to, ran sudo update-ca-certificates, and got back 0 added, 0 removed; done. No error, exit 0. You have no idea if it worked.

Someone on askubuntu asked the exact question nine years ago and it still gets about 269 views a month:

"Update-ca-certificates: 0 added; 0 removed - how come?"

The forum answer, most of the time, is "rename it to .crt." Sometimes that's the fix. Sometimes it changes nothing, because that line isn't an error message. It's a counter, and it doesn't measure what you think it measures.

01. The two-line verdict

update-ca-certificates never reads the content of your certificate to decide whether it worked. It counts symbolic links created in /etc/ssl/certs. That's it. So 0 added, 0 removed comes out for a certificate that's already correctly installed, and it comes out, word for word, for a certificate that was silently ignored. 1 added can come out for a certificate that is not usable. 0 removed comes out right after you deleted one. The only way to know which case you're in is a separate command: openssl verify.

Measured 2026-09-26, Debian 13 (trixie), Raspberry Pi 5, ca-certificates 20250419, openssl 3.5.7-1~deb13u2+rpt1, no hook installed in update.d. The tool ran for real, inside a user namespace (unshare -rm) with throwaway copies of /etc/ssl/certs and /usr/local/share/ca-certificates: real binary, real paths, nothing touched on the actual machine.

02. Same command, two different endings

Drop a working certificate as .crt in /usr/local/share/ca-certificates/ and run the tool again once it's already installed:

These commands are provided as is, without warranty. Test on a non critical machine or a snapshot first.

.crt, already installed
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done. CAfile: srv1.demo.lan.pem: OK CApath: srv1.demo.lan.pem: OK

That's a success, confirmed by the CAfile/CApath check added for this test (not something update-ca-certificates prints itself). Now do the same thing, same certificate, saved as .pem instead of .crt:

same certificate, as .pem
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done. CAfile: error srv2.demo.lan.pem: verification failed CApath: error srv2.demo.lan.pem: verification failed

Read the first four lines of each block again: identical. Same command, same wording, same rc=0. One certificate is in the trust store. The other was never looked at, because it doesn't carry the extension the tool searches for.

03. Why, verified in the script

/usr/sbin/update-ca-certificates is a 224-line shell script, and the mechanism is right there to read:

04. Three more readouts that lie the same way

Put the certificate as .crt but at the wrong location, /usr/share/ca-certificates/, without registering it:

.crt in /usr/share, not registered
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done. CAfile: error srv6.demo.lan.pem: verification failed CApath: error srv6.demo.lan.pem: verification failed

Same "0 added, 0 removed" line, third cause for it in this article. The fix is to add the path to /etc/ca-certificates.conf, and then it goes to 1 added, 0 removed and checks out.

Save the certificate in DER (binary) instead of PEM, drop it in as .crt:

DER saved as .crt
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL rehash: warning: skipping chain.pem, it does not contain exactly one certificate or CRL rehash: warning: skipping der-ca.pem, it does not contain exactly one certificate or CRL 1 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done. CAfile: error srv4.demo.lan.pem: verification failed CApath: error srv4.demo.lan.pem: verification failed

1 added, the number you'd take as a green light, and the certificate is not usable. A known-good witness certificate still verified fine right after, ruling out a corrupted bundle. The tool linked the file and appended it to the bundle without ever checking its format.

Delete a certificate that's actually installed:

installed certificate deleted
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done. CAfile: error srv1.demo.lan.pem: verification failed CApath: error srv1.demo.lan.pem: verification failed

It's gone from the trust store, and the tool still says "0 removed." Its symlinks are left behind in /etc/ssl/certs, pointing at nothing.

05. The links that vanish without a sound

A second question, on Unix & Linux Stack Exchange (807031, Debian 12 and 13, no accepted answer), describes this exact mechanism from the receiving end:

"But when I update the package my links get lost."

That person keeps extra certificates in /etc/ssl/certs/myown/ and hand-places hashed links such as 098712345.0 in /etc/ssl/certs pointing to them, the same name shape as the links openssl rehash manages. Reproduced here with one certificate and one hand-made link. Measured: a run that changes nothing leaves the hand-made link alone.

hand-made link, nothing changes
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done.
$ ls /etc/ssl/certs/8d9873f6.0
/etc/ssl/certs/8d9873f6.0

But the next run that adds even one unrelated certificate wipes it. A ca-certificates package upgrade that brings a new CA is exactly that kind of run:

next run adds one certificate
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL rehash: warning: skipping chain.pem, it does not contain exactly one certificate or CRL rehash: warning: skipping der-ca.pem, it does not contain exactly one certificate or CRL 1 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done.
$ ls /etc/ssl/certs/8d9873f6.0
ls: cannot access '/etc/ssl/certs/8d9873f6.0': No such file or directory CAfile: error srv5.demo.lan.pem: verification failed CApath: error srv5.demo.lan.pem: verification failed

man openssl-rehash says why: "all links in it that have a name in that syntax are first removed, even if they are being used for some other purpose." And nothing recreated the link afterwards: rehash rebuilt the links for the files sitting in /etc/ssl/certs, not for the one in myown/ (measured, the link stays gone). Fix: don't hand-place links in /etc/ssl/certs at all. Drop the certificate in /usr/local/share/ca-certificates/ as a .crt and let the tool own that directory.

06. The test that tells the truth

Stop reading update-ca-certificates' own output as the verdict. Check the actual chain against a real server certificate signed by your CA:

openssl verify, OK
$ openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.pem
server.pem: OK

or

openssl verify, failed
$ openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.pem
CN=srv4.demo.lan error 20 at 0 depth lookup: unable to get local issuer certificate error server.pem: verification failed

Check the format of the file first:

PEM or DER?
$ grep -c 'BEGIN CERTIFICATE' my-pem-ca.crt
1
$ grep -c 'BEGIN CERTIFICATE' my-der-ca.crt
0
$ file my-pem-ca.crt
my-pem-ca.crt: PEM certificate
$ file my-der-ca.crt
my-der-ca.crt: Certificate, Version=3

One trap here: openssl x509 -in file -noout -subject will happily print a subject for a DER file, without -inform DER and without complaint:

DER read without -inform DER
$ openssl x509 -in /usr/local/share/ca-certificates/der-ca.crt -noout -subject
subject=CN=Demo DER CA

OpenSSL 3 just guesses the format. That command tells you the file is readable, nothing about whether update-ca-certificates can use it. Not the test.

07. The fixes, all measured

Convert a DER file to PEM in place and rerun:

DER converted to PEM
$ sudo openssl x509 -inform DER -in /usr/local/share/ca-certificates/der-ca.crt -out /usr/local/share/ca-certificates/der-ca.crt
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done.
$ openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.pem
server.pem: OK

0 added, on a fix that worked, because the symlink for that name already existed from the earlier, broken attempt. The openssl verify line at the end tells you it's fixed, not the counter above it.

Rename a .pem to .crt:

.pem renamed to .crt
$ sudo mv /usr/local/share/ca-certificates/pemca.pem /usr/local/share/ca-certificates/pemca.crt
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL rehash: warning: skipping chain.pem, it does not contain exactly one certificate or CRL 1 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done.
$ openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.pem
server.pem: OK

For a certificate in /usr/share/ca-certificates/ without being registered, add its path to /etc/ca-certificates.conf, which turned the earlier case from 0 added to 1 added, 0 removed and working. dpkg-reconfigure ca-certificates is the official front end for that file; not run here, so treat it as the documented tool, not as captured output.

The recurring line rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL is noise you'll see on every successful add. The bundle file lives in the same hashed directory rehash scans, and it isn't a single certificate, so it gets skipped every time. Not a failure.

08. What this does and doesn't cover

The mechanism above is the code path for Debian 13. The Debian 12 script (20230311+deb12u1) shows the same logic on reading, not on a bench run there. Debian unstable's script (20260816) is identical to the one on this Pi.

This test validates the system trust store only. Firefox, Java, a Python virtualenv, Node often keep their own certificate store instead. Not measured here, no claim either way. Browser trusts the CA and curl doesn't (or vice versa)? Look at which store each one reads before blaming this tool.

One line for an edge case: a .crt holding two certificates is usable through the bundle only. rehash skips any file that doesn't contain exactly one certificate, so it never reaches the hashed directory.

And if you're the person behind 807031: this gives you the mechanism and the test, not a diagnosis of your specific intermediate certificate. Run openssl verify against it and read the error. That's the next step, outside what this piece covers.

The same shape runs through the rest of this series, where the system declares one thing and does another: a systemctl enable that reports failure while the service ends up enabled anyway and a firewall the systemd unit reports as enabled while it is switched off.

Share: