containerd certs.d Registry CA Trust
Configure containerd to trust private registry CAs using /etc/containerd/certs.d. Set up hosts.toml for custom CA certificates and mirror registries.
π‘ Quick Answer: Place your registry CA certificate in
/etc/containerd/certs.d/<registry>/with ahosts.tomlconfiguration file. Forregistry.example.com: create/etc/containerd/certs.d/registry.example.com/hosts.tomlwith[host."https://registry.example.com"] ca = "/etc/containerd/certs.d/registry.example.com/ca.crt". No containerd restart needed β it readscerts.ddynamically.
The Problem
Kubernetes nodes using containerd fail to pull from private registries with self-signed or internal CA certificates:
failed to verify certificate: x509: certificate signed by unknown authoritytls: failed to verify certificate: x509: certificate signed by unknown authorityErrImagePullon pods targeting private registries
The Solution
Directory Structure
/etc/containerd/certs.d/
βββ registry.example.com/
β βββ hosts.toml β Configuration
β βββ ca.crt β CA certificate
βββ registry.example.com:5000/
β βββ hosts.toml
β βββ ca.crt
βββ docker.io/ β Mirror for Docker Hub
βββ hosts.tomlConfigure CA Trust for Private Registry
# Create directory for your registry
mkdir -p /etc/containerd/certs.d/registry.example.com
# Copy CA certificate
cp /path/to/ca-bundle.crt /etc/containerd/certs.d/registry.example.com/ca.crt
# Create hosts.toml
cat > /etc/containerd/certs.d/registry.example.com/hosts.toml << 'EOF'
server = "https://registry.example.com"
[host."https://registry.example.com"]
ca = "/etc/containerd/certs.d/registry.example.com/ca.crt"
EOFWith Port Number
mkdir -p "/etc/containerd/certs.d/registry.example.com:5000"
cat > "/etc/containerd/certs.d/registry.example.com:5000/hosts.toml" << 'EOF'
server = "https://registry.example.com:5000"
[host."https://registry.example.com:5000"]
ca = "/etc/containerd/certs.d/registry.example.com:5000/ca.crt"
EOFClient Certificate Authentication (mTLS)
# /etc/containerd/certs.d/registry.example.com/hosts.toml
server = "https://registry.example.com"
[host."https://registry.example.com"]
ca = "/etc/containerd/certs.d/registry.example.com/ca.crt"
client = [
["/etc/containerd/certs.d/registry.example.com/client.cert",
"/etc/containerd/certs.d/registry.example.com/client.key"]
]Skip TLS Verification (Development Only)
# /etc/containerd/certs.d/registry.example.com/hosts.toml
server = "https://registry.example.com"
[host."https://registry.example.com"]
skip_verify = true # β οΈ INSECURE β dev/test onlyHTTP Registry (No TLS)
# /etc/containerd/certs.d/registry.example.com:5000/hosts.toml
server = "http://registry.example.com:5000"
[host."http://registry.example.com:5000"]
capabilities = ["pull", "resolve"]Mirror Configuration
# /etc/containerd/certs.d/docker.io/hosts.toml
# Mirror Docker Hub through local registry
server = "https://docker.io"
[host."https://registry.example.com/docker-hub-cache"]
capabilities = ["pull", "resolve"]
ca = "/etc/containerd/certs.d/registry.example.com/ca.crt"
[host."https://registry-1.docker.io"]
capabilities = ["pull", "resolve"]Enable certs.d in containerd config
# /etc/containerd/config.toml
# Ensure config_path is set (default on modern containerd)
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"# Verify containerd sees the config
# No restart needed for certs.d changes, but verify config_path:
containerd config dump | grep config_path
# config_path = "/etc/containerd/certs.d"
# If config_path was just added, restart containerd:
systemctl restart containerdKubernetes Node Setup (All Nodes)
# DaemonSet to distribute CA certs to all nodes
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: registry-ca-setup
namespace: kube-system
spec:
selector:
matchLabels:
app: registry-ca-setup
template:
metadata:
labels:
app: registry-ca-setup
spec:
hostPID: true
containers:
- name: setup
image: busybox:1.36
command:
- sh
- -c
- |
mkdir -p /host/etc/containerd/certs.d/registry.example.com
cp /certs/ca.crt /host/etc/containerd/certs.d/registry.example.com/ca.crt
cat > /host/etc/containerd/certs.d/registry.example.com/hosts.toml << 'TOML'
server = "https://registry.example.com"
[host."https://registry.example.com"]
ca = "/etc/containerd/certs.d/registry.example.com/ca.crt"
TOML
echo "CA configured. Sleeping."
sleep infinity
volumeMounts:
- name: host-etc
mountPath: /host/etc
- name: ca-cert
mountPath: /certs
volumes:
- name: host-etc
hostPath:
path: /etc
- name: ca-cert
configMap:
name: registry-caOpenShift β MachineConfig Approach
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
metadata:
name: 99-registry-ca-trust
labels:
machineconfiguration.openshift.io/role: worker
spec:
config:
ignition:
version: 3.2.0
storage:
files:
- path: /etc/containers/registries.conf.d/010-registry-mirror.conf
mode: 0644
contents:
source: data:text/plain;charset=utf-8,%5B%5Bregistry%5D%5D%0Alocation%20%3D%20%22registry.example.com%22%0Ainsecure%20%3D%20false%0Ablocked%20%3D%20false%0A
- path: /etc/pki/ca-trust/source/anchors/registry-ca.crt
mode: 0644
contents:
source: data:text/plain;charset=utf-8,-----BEGIN%20CERTIFICATE-----%0A...%0A-----END%20CERTIFICATE-----%0AVerify
# Test pulling from the registry
crictl pull registry.example.com/myimage:latest
# Or with containerd directly
ctr images pull registry.example.com/myimage:latest
# Check containerd logs for TLS errors
journalctl -u containerd --since "5 min ago" | grep -i "tls\|cert\|x509"Common Issues
βconfig_pathβ not set β certs.d directory ignored
containerd needs config_path = "/etc/containerd/certs.d" in its config. Some older installations donβt have this. Add it and restart containerd.
CA cert works with curl but not containerd
The CA must be PEM-encoded (not DER). Convert: openssl x509 -in ca.der -inform DER -out ca.crt -outform PEM.
Changes not taking effect
certs.d changes are read dynamically β no restart needed. But if you changed config.toml, restart containerd: systemctl restart containerd.
Best Practices
- Use
certs.ddirectory β no containerd restart needed for cert updates - Never
skip_verify: truein production β always configure proper CA trust - Same CA cert on ALL nodes β use DaemonSet or MachineConfig for distribution
- PEM format only β containerd doesnβt support DER certificates in certs.d
- Test with
crictl pullbefore deploying workloads
Key Takeaways
/etc/containerd/certs.d/<registry>/hosts.toml+ca.crtconfigures CA trust- No containerd restart needed for
certs.dchanges (dynamic reading) config_pathmust be set in containerdβsconfig.tomlto enablecerts.d- Supports CA trust, client mTLS, TLS skip, HTTP registries, and mirrors
- Distribute certs to all nodes via DaemonSet (K8s) or MachineConfig (OpenShift)

Recommended
Kubernetes Recipes β The Complete Book100+ production-ready patterns with detailed explanations, best practices, and copy-paste YAML. Everything in one place.
Get the Book βLearn by Doing
CopyPasteLearn β Hands-on Cloud & DevOps CoursesMaster Kubernetes, Ansible, Terraform, and MLOps with interactive, copy-paste-run lessons. Start free.
Browse Courses βπ Deepen Your Skills β Hands-on Courses
Courses by CopyPasteLearn.com β Learn IT by Doing
