caddyserver/caddy · error
loading trusted root CA's PEM file: %s: %v
Error message
loading trusted root CA's PEM file: %s: %v
What it means
Returned by ACMEIssuer.Provision (modules/caddytls/acmeissuer.go:237) when one of the trusted root CA PEM files (tls_trust_pool / TrustedRootsPEMFiles) cannot be read with os.ReadFile. The wrapped error is the OS error: file not found, permission denied, or (on Windows) sharing violations. Issuer provisioning aborts, so the config fails to load.
Source
Thrown at modules/caddytls/acmeissuer.go:237
DNSManager: certmagic.DNSManager{
DNSProvider: prov,
TTL: time.Duration(iss.Challenges.DNS.TTL),
PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay),
PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
Resolvers: iss.Challenges.DNS.Resolvers,
OverrideDomain: iss.Challenges.DNS.OverrideDomain,
Logger: iss.logger.Named("dns_manager"),
},
}
}
// add any custom CAs to trust store
if len(iss.TrustedRootsPEMFiles) > 0 {
iss.rootPool = x509.NewCertPool()
for _, pemFile := range iss.TrustedRootsPEMFiles {
pemData, err := os.ReadFile(pemFile)
if err != nil {
return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err)
}
if !iss.rootPool.AppendCertsFromPEM(pemData) {
return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err)
}
}
}
var err error
iss.template, err = iss.makeIssuerTemplate(ctx)
if err != nil {
return err
}
return nil
}
func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssuer, error) {
template := certmagic.ACMEIssuer{View on GitHub (pinned to 50e54ee279)
Solutions
- Verify the path and spelling, then check readability as the service user: sudo -u caddy head -1 /path/to/root.pem
- Fix permissions/ownership: chown caddy:caddy file && chmod 644 file
- In containers, mount the root CA: - ./ca/root-ca.crt:/etc/caddy/roots/root-ca.crt:ro and reference that path
- Use an absolute path; avoid ~ or shell variables that Caddy does not expand
Example fix
# before
{
acme_ca https://acme.internal/dir
tls_trust_pool file
}
# after
{
acme_ca https://acme.internal/dir
}
(defined in a site's issuer with the file present)
example.com {
tls {
issuer acme https://acme.internal/dir {
trusted_roots /etc/caddy/roots/root-ca.pem
}
}
}
# docker: - ./ca/root-ca.crt:/etc/caddy/roots/root-ca.pem:ro Defensive patterns
Strategy: validation
Validate before calling
# preflight every trusted-roots path referenced by the config
for f in $(grep -oE 'trusted_roots [^ ]+' Caddyfile | cut -d' ' -f2); do
test -r "$f" || { echo "missing/unreadable: $f"; exit 1; }
done Try / catch
if err := issuer.Provision(ctx); err != nil {
if strings.Contains(err.Error(), "loading trusted root CA's PEM file") {
// the message includes the failing path: fix it, mount it, or fix permissions
}
return err
} Prevention
- Mount CA bundles into containers at fixed absolute paths and reference those
- Check file readability as the service user (sudo -u caddy test -r) in deploy scripts
- Keep the set of trusted-root files under configuration management so drift is visible
When it happens
Trigger: Configuring an ACME issuer with a custom trust pool pointing at a path that does not exist in the container/host, a file the service user cannot read, or a path with a typo; also paths valid on the author's machine but absent in the deployment image.
Common situations: Custom trust pools for private ACME CAs (step-ca, smallstep) where the root was never mounted into the container; file owned by root with mode 600 while Caddy runs as another user; paths written for bare-metal used in a container where the file lives at a different mount point.
Related errors
- making folder for CA database: %v
- unable to add %s to trust pool: %v
- reading %s: %v
- filling in global issuer defaults for AP %d, issuer %d: %v
- acme_dns specified without DNS provider config, but no provi
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/b6278d3540ed3ac2.
Report an issue: GitHub.