caddyserver/caddy · error

loading trusted root CA's PEM file

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.

Solutions

  1. Verify the path and spelling, then check readability as the service user: sudo -u caddy head -1 /path/to/root.pem
  2. Fix permissions/ownership: chown caddy:caddy file && chmod 644 file
  3. In containers, mount the root CA: - ./ca/root-ca.crt:/etc/caddy/roots/root-ca.crt:ro and reference that path
  4. 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

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


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/b6278d3540ed3ac2. Report an issue: GitHub.

Appendix: 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)