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

  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.