How to Add Your Own CA for Ubuntu to Trust
Use openssl to create a private CA, sign a leaf certificate, and install the root in /usr/local/share/ca-certificates so Ubuntu trusts internal HTTPS.
Add your own CA to Ubuntu trust store
To add your own CA to Ubuntu trust store, copy the root certificate enter /usr/local/share/ca-certificates/ under name wey dey end with .crt, then run sudo update-ca-certificates. CA (certificate authority) na key pair wey certificate fit sign other certificates. Once machine trust your root, e go accept every certificate wey that root sign, so HTTPS between your own services no go dey fail verification again.
This guide build the complete chain offline with openssl. You go create root key and root certificate, issue one leaf certificate for server, then install the root and monitor how the same verification command change the answer. This order na the important part: verify before and after installation so you fit see say na the installation change the result.
Ubuntu 24.04 ship with OpenSSL 3 and ca-certificates package for default image, so you no need install anything first (checked August 2026).
When you suppose run your own CA?
Public CA like Let's Encrypt need a name for public DNS and one server wey e fit reach. Internal names no qualify. Database wey dey private network or admin panel wey bind to tunnel no fit get public certificate, and you no suppose expose dem to internet just to get one.
A self-signed certificate for Ubuntu solve only one host. Every client must trust that one certificate, and the next host go start the same work again. Private CA move the decision one level up. Clients trust the root once, and every certificate wey the root sign afterwards go dey trusted, including certificates for hosts wey never exist yet.
The cost dey real. The root key fit sign anything wey the constraints allow, so anybody wey read ca.key fit issue certificates wey your machines go accept. Protect am the same way you protect private key for SSH key management. If service get public DNS name, skip all this and use public CA: Certbot with nginx and Let's Encrypt na less work, and e no need anything installed for client side.
Create CA key and root certificate
Do your work for directory wey na only your user fit open. Root key no go ever comot from there.
install -d -m 700 ~/ca
cd ~/ca
openssl genrsa -aes256 -out ca.key 4096
chmod 600 ca.key-aes256 dey encrypt the key with passphrase wey you choose, and every later command wey sign with this key go ask for am. If you leave -aes256 comot, the key go dey disk as clear text. Then backup or another admin account fit give person power to issue certificates wey your machines trust.
Now create the root certificate. The CA key go sign this certificate for itself.
openssl req -x509 -new -key ca.key -sha256 -days 3650 \
-subj "/O=Example Internal/CN=Example Internal Root CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "subjectKeyIdentifier=hash" \
-addext "nameConstraints=critical,permitted;DNS:internal.example" \
-out ca.crtReplace internal.example with the name suffix wey you really dey use. Read the next section before you keep that last extension.
Every extension get one job.
basicConstraintswithCA:TRUEna wetin make this a CA certificate. Without am, client go reject any certificate wey this key sign, even when the signature correct.pathlen:0talk say the CA fit sign leaf certificates, but e no fit create any further CA under itself.keyUsagerestrict the key to signing certificates and revocation lists. This stop person from mistakenly using the same key as TLS server key.subjectKeyIdentifiergive the root an identifier wey leaf certificates point back to. Na so client dey find the correct issuer inside store wey hold some hundred certificates.nameConstraintslimit the names wey this CA get permission to vouch for.
Read wetin you create instead of assuming say the command do exactly wetin you mean.
openssl x509 -noout -subject -issuer -serial -dates -in ca.crt
openssl x509 -noout -text -in ca.crtSubject and issuer go print the same string because root certificate dey sign itself. The serial and the two dates come from the file wey you just create. Take dem from that output instead of any guide.
Limit wetin your CA fit sign
A root wey dey inside system store dey trusted for every name for internet unless you talk otherwise. Na plenty authority to keep for one file on one server. nameConstraints dey reduce am. With permitted;DNS:internal.example for the root, chain from this CA for any name wey dey outside internal.example go reject am, even when the signature correct.
Test am instead of trusting am.
openssl req -new -newkey rsa:2048 -nodes -keyout /tmp/outside.key -out /tmp/outside.csr \
-subj "/CN=www.example.com"
openssl x509 -req -in /tmp/outside.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 30 -sha256 \
-extfile <(printf 'subjectAltName=DNS:www.example.com\n') -out /tmp/outside.crt
openssl verify -CAfile ca.crt /tmp/outside.crt
echo $?The certificate go issue, because your CA dey sign anything wey you ask am to sign. Na for verification e go fail: the exit status go non-zero, and OpenSSL go name the constraint wey e hit. Na this be the value of the extension. Even if person steal the CA key, e still no fit produce working certificate for any name outside the subtree. When you finish, delete the leftover files with rm /tmp/outside.*.
Know these four things before you commit to constraint. Dem mark am critical, so client wey no understand the extension must reject the chain instead of ignoring am. This na the safe direction, but e fit surprise old TLS library. A permitted subtree for DNS names no restrict IP address SANs, because any name type wey no get subtree listed go remain unrestricted. So add permitted;IP:10.0.0.0/255.255.0.0 for the same extension if your certificates carry IP addresses. The subtree must cover every name wey you go ever issue, including short hostnames. So certificate for the bare name app go fail against the example above. The constraint dey baked into the root, so if you change your mind, you need new root certificate and fresh installation for every client.
CA wey sign am issue leaf certificate
Leaf certificate na the certificate wey server dey present to clients. Start with im own key and CSR (certificate signing request). CSR carry the public key and the requested name. Leaf key go sign am to prove say requester hold the private half.
openssl req -new -newkey rsa:2048 -nodes \
-keyout app.key -out app.csr \
-subj "/CN=app.internal.example"
chmod 600 app.keyPut the important names for extension file, no be inside CSR. Clients dey compare hostname with subjectAltName (SAN) and dem dey ignore common name completely. So, certificate wey get CN but no SAN go fail hostname verification for every current client, no matter wetin CN talk.
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:app.internal.example, DNS:api.internal.example
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:alwaysSave am as app.ext, then use CA sign the request.
openssl x509 -req -in app.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 397 -sha256 -extfile app.ext -out app.crt-CAcreateserial dey write ca.srl beside CA. The file dey hold the next serial number, so two certificates from this CA no go get the same serial number. Keep the file inside CA directory. -days 397 na choice, e no be limit of the tool. Short lifetimes matter pass for public CA because private CA no get revocation infrastructure. No CRL or OCSP responder dey, unless you build one. So, if leaf key leak, e go still work until certificate expire.
Check the result before you touch trust store.
openssl x509 -noout -subject -issuer -serial -dates -in app.crt
openssl x509 -noout -ext subjectAltName -in app.crtThe issuer line now name CA instead of the leaf itself. SAN line list the names wey this certificate valid for. Client go match hostname against that list only.
Verify with an explicit -CAfile, before installing anything
openssl verify -CAfile ca.crt app.crt
echo $?Dis one dey ask one direct question: whether app.crt chain reach the certificate for ca.crt. E no talk anything about wetin dis machine trust, because you give OpenSSL the root for the command line. If dis check fail, na problem with the certificates themselves, so fix am before you continue.
Now ask the machine.
openssl verify app.crt
echo $?When -CAfile no dey there, OpenSSL go use the certificate directory wey e get by default. openssl version -d go print the base directory wey your build dey use, and for Ubuntu, the certs directory inside am go resolve to /etc/ssl/certs. Your root never dey there, so verification go fail: the chain reach issuer wey the store no get, and no other place remain to check. Take note of the exit status. Na dis one go change two steps from now.
Real client better pass openssl verify for testing, because e dey check the hostname together with the chain. Serve the certificate and fetch am.
openssl s_server -accept 8443 -cert app.crt -key app.key -www &
curl --resolve app.internal.example:8443:127.0.0.1 https://app.internal.example:8443/--resolve go send the connection to 127.0.0.1 while e still dey request app.internal.example, so the SAN go match and trust na the only thing wey remain open. curl go fail and print why e no fit verify the chain. Add -v to see more detail. Leave the test server running.
Root dem into /usr/local/share/ca-certificates
sudo cp ca.crt /usr/local/share/ca-certificates/example-internal-root.crt
sudo chmod 644 /usr/local/share/ca-certificates/example-internal-root.crt
sudo update-ca-certificatesNa these details dey decide whether this go work at all:
- Filename must end with
.crt.update-ca-certificatesmanual page talk say certificates wey get.crtextension and dey below/usr/local/share/ca-certificatesgo enter and system go trust dem automatically. If filename naroot.pemorroot.cer, system go skip am without any message. - Content must be PEM. Na the base64 block wey
BEGIN CERTIFICATEandEND CERTIFICATElines surround. If you rename DER file to.crt, e still remain binary and system no go read am. Convert am withopenssl x509 -inform DER -in ca.der -out ca.crt. - Na only root belong here. CA private key and leaf certificate no suppose dey inside trust store.
update-ca-certificates go print how many certificates e add and remove. If e add none, na extension or file format cause am.
Confirm the change from the system side, no be from that message.
ls -l /etc/ssl/certs/$(openssl x509 -noout -subject_hash -in /usr/local/share/ca-certificates/example-internal-root.crt).0
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crtThe first command go build filename from the subject hash of your certificate, then list am. update-ca-certificates create that symlink, and e point back to the file wey you install. The second command go count certificates inside the single-file bundle. Run am before installation too, so you fit see the number increase by one.
When you copy this root go other machines, confirm say the copy reach intact before you install am. Root certificate na the file wey error fit cause the biggest problem for system, so treat am like any download wey you go verify with checksum before use.
Verify am for the system store again
openssl verify app.crt
echo $?
curl --resolve app.internal.example:8443:127.0.0.1 https://app.internal.example:8443/Na still the same commands and same certificate files, but the answer don change. Nothing about app.crt change, and na the same server wey you start earlier. The only difference be say the root certificate don dey inside the store wey those clients dey read, so the chain fit complete. Na this mechanism you suppose remember: verification na search for an issuer wey the client already trust, and installing a CA na how the issuer enter the place wey e dey search.
Stop the test server with kill %1.
Why /etc/ssl/certs no be where you put your file
/etc/ssl/certs na generated output. update-ca-certificates fills am with symlinks wey point back to the real certificate files and writes the joined bundle /etc/ssl/certs/ca-certificates.crt beside dem.
Certificate wey you copy enter that directory by hand no go dey found by anything. OpenSSL directory lookup only opens files wey dem name after certificate subject hash, so file wey dem call myca.crt no dey visible to am. curl for Ubuntu reads the bundle file, and dem rebuild the bundle from the registered sources, so your copy no dey that path too. Run update-ca-certificates --fresh and dem go remove and rebuild the symlinks for the directory, so any link wey you make by hand go comot with dem.
The other side of the split na /usr/share/ca-certificates, wey belong to ca-certificates package and dem list am for /etc/ca-certificates.conf. Package updates dey rewrite am. /usr/local/share/ca-certificates na the directory reserved for local administrator, so your CA go survive every upgrade of the package wey dey manage the rest.
Which programs dey ignore the system trust store
Installing the root go fix every program wey dey ask OpenSSL or read /etc/ssl/certs. This one cover curl, wget, git, Python standard ssl module, and Go programs, wey dey read the system files for Linux. Runtimes wey carry their own certificate list no go change, and na this one dey cause most confusion after successful installation.
- Node.js dey use list wey dem compile inside am. Point am to your root with
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/example-internal-root.crt. Set am for environment before process start, because Node dey read the variable once when e start. Current Node releases also get option to read system store; runnode --help | grep -i system-cato check whether your version get am. - Python
requestslibrary dey usecertifibundle. SetREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crtfor that process, or passverify="/etc/ssl/certs/ca-certificates.crt"to the call.pipdey accept--certfor the same reason. - Java dey read keystore. For Ubuntu,
ca-certificates-javapackage dey install hook under/etc/ca-certificates/update.d/, soupdate-ca-certificatesgo refresh Java keystore too when that package dey present. If e no dey, import the root withkeytool -importcert. - Firefox get its own store and e no dey ever check
/etc/ssl/certs. Import am through the certificate settings. Chromium for Linux dey read per-user NSS database, wey you fit edit withcertutilfromlibnss3-toolspackage. - Containers get their own filesystem, so host store no mean anything inside dem. Copy the root enter the image and run
update-ca-certificatesduring the build. Plan for this if your services dey run under Docker Compose for VPS.
When program still reject the certificate after clean installation, find out which files e dey open before you change anything else. strace -f -e trace=openat <command> 2>&1 | grep -i cert blunt, and e go answer the question for one run.
Make CA remain usable as time dey pass
To reissue a leaf, repeat the CSR step and the signing step with the same app.ext file. Clients no need do anything, because the root wey dem trust never change. Keep ca.srl and every .ext file for the CA directory, so the next issue go be repeat of command wey don work, instead of reconstruction from memory.
Back up ca.key and ca.crt somewhere outside the machine, and keep dem encrypted. If you lose the key, you no fit issue anything new: you go need build second CA and install the root for every place wey the first one reach. Keep written list of every machine and every application store wey receive the root, because na this list make rotation and removal possible at all.
When the root itself dey near expiry, generate the replacement early and install both roots side by side. Two roots for the store no be problem, and client fit accept either one. Reissue the leaves against the new root, then remove the old one after nothing depend on am again.
Remove a CA from the trust store
sudo rm /usr/local/share/ca-certificates/example-internal-root.crt
sudo update-ca-certificates --fresh--fresh go remove the symlinks for /etc/ssl/certs and rebuild dem from the sources wey still dey there. This one make the deleted root comot from both the directory and the bundle. Prove say e don comot the same way wey you use prove the install.
openssl verify app.crt
echo $?
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt
ls -l /etc/ssl/certs/$(openssl x509 -noout -subject_hash -in ~/ca/ca.crt).0Verification fail again, the certificate count return to where e start, and the hash symlink don disappear.
That command touch only the system store. E no touch anything else. For the other places, undo the install by hand: clear NODE_EXTRA_CA_CERTS, delete the alias from any Java keystore, remove the root from each browser profile, and rebuild any container image wey include am. Removing the root no invalidate the certificates wey e sign. Dem remain valid for every machine wey still trust am. Na why private CA need a written list of everywhere wey the root enter. If you no fit withdraw a CA completely, e become permanent security hole. So test the removal for one machine the same day wey you set am up, while the list still short.
FAQ
Where I fit put CA certificate for Ubuntu?
Put am for /usr/local/share/ca-certificates/, use filename wey dey end with .crt and PEM content, then run sudo update-ca-certificates. That directory na for local administrator, so package upgrades no go touch am. /usr/share/ca-certificates belong to ca-certificates package, and /etc/ssl/certs dey generate from both, so any file wey you put for either place fit get overwrite or dem fit ignore am.
Why curl still dey reject the certificate after update-ca-certificates?
Check the possible causes one by one. The file fit no end with .crt, or e fit be DER instead of PEM. If so, update-ca-certificates skip am and add nothing. The certificate fit no get subjectAltName wey match the hostname. That one na hostname failure, no be trust failure; check am with openssl x509 -noout -ext subjectAltName -in app.crt. The server fit dey send only the leaf certificate when e also need an intermediate certificate. curl fit dey use another bundle through CURL_CA_BUNDLE or --cacert. Long-running service need restart too, because most programs read the trust store only once when dem start.
System trust store cover Firefox, Chrome, Node and Java?
No. curl, wget, git, Python standard ssl module and Go programs dey read the system files, so dem go work as soon as update-ca-certificates run. Firefox keep its own store. Chromium for Linux use per-user NSS database, and you fit edit am with certutil from libnss3-tools package. Node.js need NODE_EXTRA_CA_CERTS wey point to your root file. Java dey read keystore, and update-ca-certificates only refresh am when ca-certificates-java package dey installed. Python requests use certifi and e need REQUESTS_CA_BUNDLE.
How I fit remove CA from Ubuntu trust store?
Delete the file from /usr/local/share/ca-certificates/, then run sudo update-ca-certificates --fresh. The --fresh option clear the symlinks for /etc/ssl/certs and rebuild dem, so the certificate comot from the hash symlinks and ca-certificates.crt bundle at the same time. Confirm am by running openssl verify against certificate wey the CA sign, then check the exit status. After that, remove am from every other store wey you add am to, because that command no dey touch any of dem.
I fit use private CA instead of Let's Encrypt for public site?
No. Visitor browser never see your root before, so e go show full-page warning. You no fit install your root for machines wey you no control. Private CA na for names wey only your own machines fit resolve, and for clients wey you administer. For anything wey strangers go visit, get the certificate from public CA.