caproni ca trust on Linux keeps only the last profile's dev CA trusted
Overview
On Linux, caproni ca trust installs every profile's dev CA under the same file name, caproni-dev-ca.crt (internal/truststore/system_linux.go).
With more than one profile, for example through cluster.name.strategy: directory-hash or the cells setup, only the most recently trusted profile's CA is trusted on the host.
caproni ca untrust in any profile removes whichever CA is currently installed.
The docs describe trust as per profile. The quick start calls caproni ca trust "a one-time step per profile", and each profile has its own CA under ~/.local/share/caproni/<profile>/pki/.
Note
On macOS this works better, because every add-trusted-cert adds a separate System keychain entry.
Expected behavior
caproni ca trustin profile B leaves profile A's CA trusted, so both profiles' services verify from the host.caproni ca untrustin a profile removes only that profile's CA.
Actual behavior
caproni ca trustin profile B runssudo install -m 0644 <B>/pki/current/ca.crt /usr/local/share/ca-certificates/caproni-dev-ca.crt(or/etc/pki/ca-trust/source/anchors/on RHEL-like systems), followed byupdate-ca-certificates/update-ca-trust. This overwrites profile A's CA, so HTTPS to A's hostnames fails verification on the host until A is trusted again, and that in turn breaks B.caproni ca untrustin profile A runssudo rm -f .../caproni-dev-ca.crt, which removes B's CA if B was trusted last.- The Firefox/Chrome NSS step also uses one fixed nickname,
Caproni Dev CA(internal/truststore/nss.go). How several profiles interact there is not tested.
Reproduction steps
Verified on Ubuntu 26.04.1 LTS (aarch64, UTM VM) with caproni built from main at 6bd40802. No cluster is needed: the external driver points at a kubeconfig context that doesn't exist, so the cluster reports stopped and ca trust uses the configured cluster_domain.
The external driver needs kubectl on PATH, even for that.
-
Create two profiles, each in its own directory:
mkdir -p ~/caproni-test/a ~/caproni-test/b for p in a b; do cat > ~/caproni-test/$p/caproni.yaml <<Y version: 2 cluster: driver: external name: strategy: directory-hash external: context: does-not-exist network: hostname: $p.caproni.test cluster_domain: cluster.local Y done~/caproni-test/a/caproni.yamlthen reads:version: 2 cluster: driver: external name: strategy: directory-hash external: context: does-not-exist network: hostname: a.caproni.test cluster_domain: cluster.local -
Generate each profile's CA and note the paths:
(cd ~/caproni-test/a && caproni ca inspect) # Path: ~/.local/share/caproni/caproni-a-<hash>/pki/current/ca.crt (cd ~/caproni-test/b && caproni ca inspect) # Path: ~/.local/share/caproni/caproni-b-<hash>/pki/current/ca.crt A=$(ls ~/.local/share/caproni/caproni-a-*/pki/current/ca.crt) B=$(ls ~/.local/share/caproni/caproni-b-*/pki/current/ca.crt) F=/usr/local/share/ca-certificates/caproni-dev-ca.crt check() { openssl x509 -noout -fingerprint -sha256 -in "$F" 2>/dev/null || echo "$F absent" openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt "$A" >/dev/null 2>&1 && echo "A trusted" || echo "A NOT trusted" openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt "$B" >/dev/null 2>&1 && echo "B trusted" || echo "B NOT trusted" } openssl x509 -noout -fingerprint -sha256 -in "$A"; openssl x509 -noout -fingerprint -sha256 -in "$B" -
Trust A, then B, then untrust A, running
checkafter each step:(cd ~/caproni-test/a && caproni ca trust); check (cd ~/caproni-test/b && caproni ca trust); check (cd ~/caproni-test/a && caproni ca untrust); checkObserved:
Step caproni-dev-ca.crtholdsA trusted B trusted before absent no no ca trustina/A yes no ca trustinb/B (A's file overwritten) no yes ca untrustina/absent (B's CA deleted) no no When B replaces A,
update-ca-certificatesprints0 added, 0 removed, although the trusted bundle does change. -
NSS (
sudo apt-get install libnss3-tools), with a fresh Chrome database. Repeat step 3 and list the store after each command:mkdir -p ~/.pki/nssdb && certutil -d sql:$HOME/.pki/nssdb -N --empty-password certutil -L -d sql:$HOME/.pki/nssdb certutil -L -d sql:$HOME/.pki/nssdb -n "Caproni Dev CA" -a | openssl x509 -noout -fingerprint -sha256 # first entry onlyObserved:
Step Entries under nickname Caproni Dev CAca trustina/A ( C,,)ca trustinb/A and B, both C,,ca untrustina/A only: certutil -D -n "Caproni Dev CA"removed B's entry, not A's
Workaround
Re-run caproni ca trust in the profile you're working in whenever you switch profiles. Alternatively, copy each profile's pki/current/ca.crt into the trust anchors directory by hand under distinct names and run update-ca-certificates.
Potential solutions
- Install one file per profile, for example
caproni-dev-ca-<profile>.crt, and haveuntrustremove only that file. Do the same for the NSS nickname. - Every profile's CA has the same subject,
CN=Caproni Dev CA, O=Caproni. Tools that read the bundle file (ca-certificates.crt), and Go, choose between same-subject roots by key identifier, so they're fine. Lookup through a hashed directory (CApath,<hash>.0,<hash>.1) may only try the first root (openssl#16304). A per-profile subject for newly generated CAs, as mkcert does withuser@host, would remove that ambiguity. Existing CAs have to keep their subject, or the certificates they issued stop verifying.