Trust stores are not one thing
Certificates are part of my day job. I manage them across a set of production sites, and the development and staging environments there run on internal self-signed certs. So putting HTTPS on the staging site for this project should have been routine, and mostly it was.
Then Chrome trusted my new root and Firefox didn't, which was a good reminder that "install the root certificate on my machine" is not one action. A machine has more than one idea of what it trusts.
Why a private CA at all
Staging runs on a home server on my LAN. The hostname only resolves through a hosts file entry on my desktop, and the server takes no inbound traffic from the internet. That rules out the usual way of getting a free certificate, where Let's Encrypt connects to your server over port 80 to verify you control the domain.
The alternative is a DNS challenge, where you prove control by writing a TXT record instead. My registrar denies API access to accounts that don't meet a spending threshold, so automating that was off the table.
So I used Caddy's internal CA instead. One directive:
staging.mabel.ink {
tls internal
reverse_proxy mabel-staging:80
}
Caddy generates a self-signed root certificate, keeps the private key, and uses it to sign a short-lived leaf certificate for the site. It renews the leaf automatically. There is nothing to install, nothing to renew by hand, and no external service involved.
The warning is the feature
The first time I loaded the site I got a full page certificate warning. That is the correct result, and it is worth sitting with for a second.
My browser received a certificate, checked who signed it, walked the chain up to Caddy's root, and found a root it had never heard of. So it refused. That is certificate validation doing exactly the job it exists to do. The whole system rests on the client having a list of authorities it trusts in advance, and mine wasn't on the list.
Fixing it means adding my root to that list.
Adding it to Windows
Export the root certificate. The public certificate only, the private key never leaves the server:
docker exec caddy cat /data/caddy/pki/authorities/local/root.crt
Then, in an elevated PowerShell:
Import-Certificate -FilePath .\caddy-root.crt -CertStoreLocation Cert:\LocalMachine\Root
Cert:\LocalMachine\Root is the Trusted Root Certification Authorities store, the same one
certlm.msc opens. PowerShell exposes certificate stores as a drive you can navigate, which is
a nicer interface than the MMC snap-in once you know it's there.
Restart the browser, load the site, padlock. Done.
Except it wasn't.
Firefox did not care
Chrome and Edge trusted the certificate immediately. Firefox still threw the same warning.
Firefox ships its own certificate store. It uses NSS, maintains its own list of trusted authorities, and by default ignores the operating system entirely. Installing a root into Windows tells Firefox nothing.
The fix is one setting. In about:config, set security.enterprise_roots.enabled to true,
then fully restart Firefox. That tells it to also read roots from the Windows store. You can
alternatively import the certificate directly under Settings, Privacy and Security,
Certificates, View Certificates, Authorities.
The general shape of this is worth remembering, because it isn't a Firefox quirk. There is no such thing as "the trust store on this machine." There are several, maintained independently:
- Windows has its own, used by Chrome, Edge, and most native applications
- Firefox has its own via NSS
- Java has its own,
cacerts - Most language HTTP clients ship their own CA bundle, which is why Python and Node can each fail on a certificate the browser accepts
"Install the root certificate" is always a per-store operation. If something trusts your cert and something else doesn't, that's usually the reason.
The part that should make you uncomfortable
I now have a certificate authority in my Windows trust store. Consider what that actually means.
A root CA can sign a certificate for any hostname. Not just mine. Whoever holds that private key can mint a valid certificate for any site on the internet, and my machine will accept it without a warning. That is the entire point of a root: unconditional trust, applied to anything it vouches for.
That private key is sitting in a Docker volume on a server in my house.
For a home lab I control, that's a fine trade. But it reframes what a trust store is. Every root in that list is an entity that can silently impersonate anything to your machine. Browsers ship with well over a hundred of them by default. Scrolling that list in Firefox is a genuinely useful exercise, because it makes concrete how much implicit trust you're extending to organizations you've never thought about.
It also explains why a compromised or malicious root CA is treated as a catastrophic event rather than an inconvenience, and why enterprises guard their internal CAs the way they do.
One operational trap
The root's private key lives in Caddy's data volume. If that volume is lost, Caddy generates a brand new root on next start. Every device that trusted the old one stops trusting the site, and every one of them needs the new root installed.
So the volume belongs in your backups, and "let me just delete the volume and recreate it" is a troubleshooting step to avoid.
Was it worth it
For a host that only resolves through a hosts file on one machine, a publicly trusted certificate would have been mostly ceremonial. Nobody else can reach the name.
What I got instead was reinforcing my understanding of what a certificate authority actually is, why the warning page exists, and where trust is really stored. That turned out to be worth more than the padlock.