Add root CA certificates to trust stores on MacOS
NOTE: Exported from this Notion page
This article is about adding your own root CA certificate to your local root trust stores. And by doing that all the certificates (intermediate or leaf) signed by that is automatically trusted because of the “chain of trust”. I.e., when you have created one root certificate with mkcert you only have to add it once to the trust stores. When you need to create new certificates you can do so successfully as long as those are signed by your root certificate. The alternative would be to add every certificate you create to every trust store.
What I want to achieve looks something like this:
Diagram rendered by the amazing plantuml ☝️
There’s a lot of different CA (Certificate Authority) root trust stores, not all applications uses the system’s, e.g., macOS Trust Store. So if you want some applications to trust your certificate you need to add it to those particular store. E.g., if you’re using Google Chrome, you’re good as of now, because it uses the system’s store. But they are planning on creating their own in the future. Read about their “Root Program” here.
Out of scope of this article
- Explaining how TLS/HTTPS/SSL works, i.e., X509. See How HTTPS works comic for that.
- Root programs
I present multiple ways of installing certificates:
On all the commands below I use the $CERT
variable which point to my root CA certificate I want to trust:
CERT="/path/to/my/cert/my-root.crt"
Install certificates using step
Smallstep supports adding root certs to multiple trust stores. This is a lot easier than doing it manually. As of now it supports the following stores:
- System store (In my case: macOS Trust Store)
- Firefox
- Java
brew install step
step certificate install --all $CERT
Even though I got an error when installing cert to Firefox… But if it works it will save you a lot of time 🙂
Install certificates manually
macOS Trust Store
MacOS store is managed through “Keychain Access”. Install a cert with a command like this:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain $CERT
Nodejs
You add extra certs with an environment variable included in the node process:
export NODE_EXTRA_CA_CERTS=$CERT
See https://nodejs.org/api/cli.html#cli_node_extra_ca_certs_file
Curl
Curl uses openssl as its trust store. And by default (I think) openssl is using the system’s store. But if you only want to update openssl’s store follow along.
You do that by copying the cert to this directory: /usr/local/etc/[email protected]/certs/
. I take for granted openssl
is installed with version 1.1
with homebrew on it’s default location.
cp $CERT /usr/local/etc/[email protected]/certs/
And then run this to update openssl:
/usr/local/opt/[email protected]/bin/c_rehash
And that’s it. That was easy ☺️.
The above is explained by homebrew: brew info openssl
.
Optional approach to trust certificate per request (without adding it to store):
curl --cacert $CERT https://localhost
Firefox
Firefox is using Mozillas NSS root store. Firefox recommends installing custom certificates this way: https://support.mozilla.org/en-US/kb/setting-certificate-authorities-firefox#w_using-policies-to-import-ca-certificates-recommended. I haven’t tried that as I wanted a fully automated way and went for using certutil
instead.
Install with certutil
certutil
is a cli-tool and it’s dependent on nss
. Here is a resource with all the commands and arguments to certutil
: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/tools/NSS_Tools_certutil.
-
Install dependencies
brew install nss
-
Locate certutil (optional if it’s not added to your PATH)
When I installedd
nss
with homebrew it was symlinked into/usr/local/bin
and therefore in myPATH
already. So I could skip this step. If this command gives output you are fine as well:$ which certutil /usr/local/bin/certutil
If not let’s find
certutil
. Locatenss
with:brew --prefix nss
That should output the location of
nss
which containscertutil
. Appendbin/certutil
to the above output and you have/usr/local/opt/nss/bin/certutil
on a macOS. -
Locate your Firefox profile
You also need to find you Firefox profile path. It usually is here:
~/Library/Application Support/Firefox/Profiles/xxxxxxxx.default
with the.default
suffix on it. But maybe you use another profile. My profile was calledxxxxxxxx.dev-edition-default-1579772843598/
as I use Firefox Developer Edition. -
Add certificate
Ok. Now we can add the certificate when we have the path to our certificate (
$CERT
), the$PROFILE
and can executecertutil
. To identify the certificate it’s also required to set a “nickname” on it with-t
. I set it with$NAME
:PROFILE="~/Library/Application\ Support/Firefox/Profiles/df25fipq.dev-edition-default-1579772843598/" NAME=MyUniqueTestCAName1 certutil -A -d $PROFILE -t "C,," -n $NAME -i $CERT
You can now list the certificates in the profile to see if it got added. You should recognize it by the name you specified with
-n
parameter. -
List certificates
certutil -L -d $PROFILE
-
Extra: Remove certificate
It’s useful to be able to remove certificates. Do it by targetting the name of it:
certutil -D -d $PROFILE -n $NAME
iOS
Apple seems to recommend using Apple Configurator 2 from the app store. See https://support.apple.com/en-us/HT204477 and https://developer.apple.com/library/archive/qa/qa1948/_index.html. But I’m not sure if it’s still applicable for iOS13
Java
I’ll refer to IBM’s knowledge center: https://www.ibm.com/support/knowledgecenter/en/SSEP7J_10.2.2/com.ibm.swg.ba.cognos.adm_ba_pattern.1.2.0.doc/t_biblu_add_cacert.html or Oracles: https://docs.oracle.com/cd/E19906-01/820-4916/geygn/index.html
Docker environment
Container to Container communication
I’ve came across scenarios when I make HTTPS requests between containers, e.g., when I want to mock an external service and can’t change the protocol to HTTP. So if I do a HTTPS requests from my Nodejs-container app to https://stripe.com/api-call which is my mock-service I need to present a valid certificate for Nodejs from my mock service.
A good thing to know about is that you can redirect network domains in docker-compose with network aliasas. This is great for mocking external services, when you, e.g., want to handle all external traffic in a an internal service.
This example make all calls to [api.stripe.com](http://api.stripe.com)
getting routed to my-app
for example:
my-app:
image: my-app
ports:
- 80:80
- 443:443
networks:
default:
aliases:
- api.stripe.com