Skip to main content
RunBook Academy

Docker & ContainersXXV Β· Certificates & PKITrust stores

CA trust inside the container β€” works on the host, fails in the image

Intermediate⏱ ~22 min

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

Not yet marked complete on this device.

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:

Read-only / Safeinside the container
$ docker exec api curl -sS https://internal-api.example.com/health
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

Illustrative output

Or, from a Go binary, the same fact in different words:

Read-only / Safeinside the container
$ docker logs --tail 3 api
2026-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 authority

Illustrative 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 imageBundle read by OpenSSLDrop new CAs inRefresh 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.20 has no ca-certificates package, so every TLS verification fails, not just internal ones. apk add --no-cache ca-certificates is what most Alpine Dockerfiles are missing.
  • scratch and distroless images have no store unless you put one there. A statically linked Go binary in a scratch image cannot verify any certificate until you COPY a bundle in.
Read-only / Safeis there a store at all
$ 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 directory

Illustrative 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.

Configuration changeDockerfile β€” Debian family
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-certificates
Configuration changeDockerfile β€” RHEL family
FROM 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 extract

Runtimes 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:

RuntimeWhere it trusts fromHow to add a CA
Gosystem store on Linuxworks after update-ca-certificates; SSL_CERT_FILE overrides
Python requests / httpxthe certifi bundle, not the system storeREQUESTS_CA_BUNDLE or SSL_CERT_FILE
Node.jsa Mozilla list compiled into the binaryNODE_EXTRA_CA_CERTS=/path/to/ca.crt
Javaits own cacerts keystorekeytool -importcert into the JDK truststore
curl / OpenSSL CLIsystem storeworks 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.

Configuration changecompose.yaml
services:
api:
  image: myapp:1.4.2
  environment:
    NODE_EXTRA_CA_CERTS: /etc/ssl/certs/example-corp-internal-root.crt

The 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

  1. 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.
  2. See what the server actually presents, including the chain it sends: openssl s_client -connect internal-api.example.com:443 -showcerts run from inside the container.
  3. 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.
  4. 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

  1. 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?

  2. 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?

  3. Q3. Which of these are sound ways to make an internal CA available to a containerised application? Select all that apply.

  4. 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.