Docker & ContainersXXV Β· Certificates & PKITrust stores
CA trust inside the container β works on the host, fails in the image
What you'll learn
- Locate the CA trust store for Debian, Alpine and RHEL-family images
- Add an internal CA to an image correctly, at build time
- Recognise runtimes that ignore the system trust store entirely
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11
The ticket always reads the same way. It works on my machine, it works on the server, it works from a shell on the Docker host, and inside the container it fails:
$ docker exec api curl -sS https://internal-api.example.com/healthcurl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.htmlIllustrative output
Or, from a Go binary, the same fact in different words:
$ docker logs --tail 3 api2026-08-11T09:14:02Z ERROR upstream call failed
Get "https://internal-api.example.com/health": tls: failed to verify
certificate: x509: certificate signed by unknown authorityIllustrative output
Nothing is wrong with the network, the certificate, or the clock. The container simply does not know who signed the certificate, because the list of trusted signers lives in the image, and the hostβs list is not shared with it.
The trust store is part of the filesystem
A container has its own root filesystem. /etc/ssl/certs inside the
container is the imageβs /etc/ssl/certs, not the hostβs. Three
families, three layouts:
| Base image | Bundle read by OpenSSL | Drop new CAs in | Refresh with |
|---|---|---|---|
| Debian / Ubuntu | /etc/ssl/certs/ca-certificates.crt | /usr/local/share/ca-certificates/ | update-ca-certificates |
| Alpine | /etc/ssl/certs/ca-certificates.crt | /usr/local/share/ca-certificates/ | update-ca-certificates |
| RHEL / UBI / Fedora | /etc/pki/tls/certs/ca-bundle.crt | /etc/pki/ca-trust/source/anchors/ | update-ca-trust |
Two details bite people:
- Alpine ships no CA bundle at all by default.
alpine:3.20has noca-certificatespackage, so every TLS verification fails, not just internal ones.apk add --no-cache ca-certificatesis what most Alpine Dockerfiles are missing. scratchand distroless images have no store unless you put one there. A statically linked Go binary in ascratchimage cannot verify any certificate until youCOPYa bundle in.
$ docker exec api sh -c 'ls -l /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt 2>&1'ls: /etc/ssl/certs/ca-certificates.crt: No such file or directory
ls: /etc/pki/tls/certs/ca-bundle.crt: No such file or directoryIllustrative output
That output ends the investigation. There is nothing to debug about the certificate; there is no trust store.
Adding an internal CA, correctly
Do it at build time, in the Dockerfile, so the image is self-contained and reproducible.
FROM debian:12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# The file MUST end in .crt and MUST be PEM, or update-ca-certificates
# silently skips it.
COPY example-corp-internal-root.crt \
/usr/local/share/ca-certificates/example-corp-internal-root.crt
RUN update-ca-certificatesFROM registry.access.redhat.com/ubi9/ubi-minimal
COPY example-corp-internal-root.crt \
/etc/pki/ca-trust/source/anchors/example-corp-internal-root.crt
RUN update-ca-trust extractRuntimes that ignore the system store
Adding the CA to /etc/ssl/certs fixes curl and every OpenSSL
consumer. It does not necessarily fix the application, because
several major runtimes maintain their own trust:
| Runtime | Where it trusts from | How to add a CA |
|---|---|---|
| Go | system store on Linux | works after update-ca-certificates; SSL_CERT_FILE overrides |
Python requests / httpx | the certifi bundle, not the system store | REQUESTS_CA_BUNDLE or SSL_CERT_FILE |
| Node.js | a Mozilla list compiled into the binary | NODE_EXTRA_CA_CERTS=/path/to/ca.crt |
| Java | its own cacerts keystore | keytool -importcert into the JDK truststore |
| curl / OpenSSL CLI | system store | works after the update command |
This is why βI added the CA and curl works but the app still
failsβ is such a common second ticket. The two are looking in
different places, and both are behaving as designed.
services:
api:
image: myapp:1.4.2
environment:
NODE_EXTRA_CA_CERTS: /etc/ssl/certs/example-corp-internal-root.crtThe corporate TLS-inspecting proxy
If the network runs a TLS-inspecting proxy, nothing on the internet presents its real certificate. The proxy terminates the connection and re-signs with its own CA. Hosts on the corporate network trust that CA because IT put it in their store; containers do not.
The symptom is distinctive: docker build fails at the first RUN
step that fetches anything, with an unknown-authority error against
a well-known public site. pypi.org and registry.npmjs.org are
usually the first casualties.
The fix is the same as for any internal CA β install the proxyβs CA
into the image β and it must happen before the first network RUN.
Diagnosing it in four commands
- Confirm a store exists at all:
docker exec api ls -l /etc/ssl/certs/ca-certificates.crt. No file means no trust, and no further debugging is needed. - See what the server actually presents, including the chain it sends:
openssl s_client -connect internal-api.example.com:443 -showcertsrun from inside the container. - Verify the chain against the store the container has:
docker exec api openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /tmp/server-chain.pem. - If curl now works but the application does not, identify the runtime and check its own trust store from the table above.
Sanity check
Knowledge check Β· 4 questions
Q1. A statically linked Go binary in a `scratch` image fails every HTTPS call with x509: certificate signed by unknown authority, including calls to well-known public sites. What is the cause?
Q2. You add an internal CA to a Debian-based image and run update-ca-certificates. curl inside the container now works, but the Python application still fails verification. Why?
Q3. Which of these are sound ways to make an internal CA available to a containerised application? Select all that apply.
Q4. A server that sends only its leaf certificate and omits the intermediates produces the same curl error as a container with no CA for that issuer.
Passing score: 75%. Answers are checked in this browser.