| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| defaults | ||
| handlers | ||
| meta | ||
| tasks | ||
| README.md | ||
ansible-role-ca-trust
Adds CA certificates to the Debian/Ubuntu system trust store, so tools
that use /etc/ssl/certs/ca-certificates.crt (system curl,
openssl, git, python3, apt...) trust them. Typical use: a private
CA, such as Caddy's local CA for LAN-only sites.
What it does
- Fails unless every
ca_trust_certsentry has anameand asrc, and eachsrcis a single PEM certificate. - Installs
ca-certificates. - Copies each certificate to
/usr/local/share/ca-certificates/<name>.crt. - Runs
update-ca-certificateswhen any of them changed. That links the certificate as/etc/ssl/certs/<name>.pemand appends it to the combined bundle/etc/ssl/certs/ca-certificates.crt.
Variables
| Variable | Default | Description |
|---|---|---|
ca_trust_certs |
[] |
{name, src}: src is a PEM file on the control machine |
name becomes the file name, so keep it a plain slug (no spaces or
slashes). The .crt extension is added by the role:
update-ca-certificates ignores files without it.
Example playbook
- hosts: all
vars:
ca_trust_certs:
- name: caddy-local-root
src: "{{ playbook_dir }}/files/caddy-local-root.crt"
roles:
- ca-trust
Record the certificate's SHA-256 fingerprint next to the variable (see below for how to get it), so a reader can tell which CA it is.
Checking that it worked
In the commands below, <name> is the entry's name, and
<site> is a host name served with a certificate from that CA.
1. The certificate is installed
ls -l /etc/ssl/certs/<name>.pem
Expect a symlink to /usr/local/share/ca-certificates/<name>.crt,
created by update-ca-certificates.
openssl x509 -in /usr/local/share/ca-certificates/<name>.crt -noout -subject -enddate -fingerprint -sha256
Compare the fingerprint with the one you recorded (or with the CA's
own: openssl x509 -in <the CA's root.crt> -noout -fingerprint -sha256).
It proves the right certificate was installed. notAfter is when the
root expires.
awk -v cmd='openssl x509 -noout -subject' '/BEGIN CERT/{close(cmd)} {print | cmd}' \
/etc/ssl/certs/ca-certificates.crt | grep -i '<part of the subject>'
This lists the subject of every certificate in the combined bundle
(grep -c on the file itself can't work, it's base64). Expect the
root's subject. This is the file most tools read.
2. Tools trust it
System curl (also what git uses over HTTPS):
/usr/bin/curl -sv -o /dev/null https://<site>/ 2>&1 | grep -E 'issuer:|verify ok'
Expect the CA's name as issuer: (often an intermediate) and
SSL certificate verify ok.
The full chain, as OpenSSL sees it:
echo | /usr/bin/openssl s_client -connect <site>:443 -servername <site> -verify_return_error 2>&1 \
| grep -E 'Verify return code|^ *[0-9] s:|i:'
Expect the chain to end at the root (for example leaf, issued by the
intermediate, issued by the root) and Verify return code: 0 (ok).
-servername matters: without it, the server may not know which
certificate to send.
System Python:
/usr/bin/python3 -c "import urllib.request; print(urllib.request.urlopen('https://<site>/').status)"
Expect an HTTP status (redirects are followed, so usually 200)
rather than CERTIFICATE_VERIFY_FAILED.
3. Tools with their own trust store
These don't read the system store, so the role alone doesn't make them trust the CA:
| Tool | Reads | Fix |
|---|---|---|
Homebrew openssl, curl, python3 (ssl, urllib) |
Homebrew's bundle, $(brew --prefix)/etc/ca-certificates/cert.pem |
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt |
Python requests, pip |
certifi's bundle |
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt |
| Chrome/Chromium, Firefox | NSS databases (~/.pki/nssdb, the Firefox profile) |
certutil from libnss3-tools, or the browser's settings |
| Docker containers | the image's own store | install the certificate in the image, or mount the bundle |
Set the environment variables in the shell's startup files rather than
editing Homebrew's cert.pem: the ca-certificates formula owns that
file and an upgrade can replace it. The system bundle includes the
public roots as well, so nothing else stops trusting anything. The
variables only reach programs started from that shell; cron jobs and
systemd services don't see them.
To check Homebrew's tools, run in a new shell:
echo $SSL_CERT_FILE
echo | openssl s_client -connect <site>:443 -servername <site> -verify_return_error 2>&1 | grep 'Verify return code'
Expect /etc/ssl/certs/ca-certificates.crt and 0 (ok). To confirm
it's the variable doing the work, run the same check without it:
echo | env -u SSL_CERT_FILE openssl s_client -connect <site>:443 -servername <site> 2>&1 | grep 'Verify return code'
Expect 20 (unable to get local issuer certificate), since Homebrew's
own bundle doesn't have the CA.
Troubleshooting
- Step 1 fails (no symlink, or the subject isn't in the bundle): re-run
the playbook. If the file is in
/usr/local/share/ca-certificates/but not linked, runsudo update-ca-certificatesand check that it reports1 added(a name not ending in.crtis skipped silently). - Wrong fingerprint:
srcpoints at an old or different certificate. Fixsrcand re-run; the file is replaced and the bundle rebuilt. - Step 1 passes but a tool still fails: the tool has its own store
(see the table), or a variable isn't set in that shell. Check it with
echo $SSL_CERT_FILE; a shell started before the variable was added won't have it. - Verify code 20 even from system tools: the server isn't sending its
intermediate, or the certificate isn't from this CA at all. Look at
the
s:/i:lines of the chain check: the lasti:must match the root's subject. - Verify code 10 (certificate has expired): check
notAfterfrom step 1, then the site's certificate. A private CA's leaf certificates are usually short-lived and renewed by the server.
Rotating or removing a certificate
-
New root: replace the file at
src(samename) and re-run. Update the recorded fingerprint. -
Removing: deleting the entry doesn't uninstall it. Remove it by hand:
sudo rm /usr/local/share/ca-certificates/<name>.crt sudo update-ca-certificates --fresh--freshrebuilds/etc/ssl/certsfrom scratch, so the stale symlink goes too.