caddyserver/caddy · error

source %T returned a nil certificate

Error message

source %T returned a nil certificate

What it means

Thrown while provisioning a 'combined' CA trust pool (tls.ca_pool.source.combined): one of the configured source modules returned a slice of certificates that contains a nil *x509.Certificate element. Caddy refuses to add nil certs to the x509.CertPool because a nil certificate cannot be parsed and would silently corrupt trust evaluation. The %T verb prints the offending source module's Go type, which identifies which source is broken.

Source

Thrown at modules/caddytls/capools.go:882

	for _, src := range sources.([]any) {
		ca, ok := src.(CA)
		if !ok {
			return fmt.Errorf("source module is not a CA pool provider")
		}
		ccp.sources = append(ccp.sources, ca)

		certProvider, ok := ca.(CertificateProvider)
		if !ok {
			return fmt.Errorf("source %T does not implement CertificateProvider (required for combining)", ca)
		}

		certs := certProvider.Certificates()
		if certs == nil {
			return fmt.Errorf("source %T returned nil certificates", ca)
		}
		for _, cert := range certs {
			if cert == nil {
				return fmt.Errorf("source %T returned a nil certificate", ca)
			}
			caPool.AddCert(cert)
			allCerts = append(allCerts, cert)
		}
	}

	ccp.pool = caPool
	ccp.certs = allCerts

	return nil
}

// Syntax:
//
//	trust_pool combined {
//		source <module_name> {
//			<module_config>
//		}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Read the %T type name in the error to identify which source module is at fault
  2. Fix that module's Certificates() so it never returns nil elements: skip the entry, or return an error explaining why the certificate could not be loaded
  3. Verify the certificate files/inputs feeding that source are valid and readable
  4. If the module is third-party, report the bug upstream and pin a working version

Example fix

// before (inside a custom source's Certificates())
certs := make([]*x509.Certificate, len(files))
for i, f := range files {
	der, _ := os.ReadFile(f) // error ignored
	cert, _ := x509.ParseCertificate(der)
	certs[i] = cert // can be nil
}
return certs

// after
var certs []*x509.Certificate
for _, f := range files {
	der, err := os.ReadFile(f)
	if err != nil {
		return nil, fmt.Errorf("reading %s: %v", f, err)
	}
	cert, err := x509.ParseCertificate(der)
	if err != nil {
		return nil, fmt.Errorf("parsing %s: %v", f, err)
	}
	certs = append(certs, cert)
}
return certs
Defensive patterns

Strategy: validation

Validate before calling

// Before returning certs from a custom CA source, self-check:
func validateCerts(certs []*x509.Certificate) error {
	for i, c := range certs {
		if c == nil {
			return fmt.Errorf("certificate at index %d is nil", i)
		}
	}
	return nil
}

Prevention

When it happens

Trigger: Provisioning a CombinedCertPool whose SourcesRaw includes a custom CA source module (or a third-party plugin) whose Certificates() implementation returns a slice like []*x509.Certificate{validCert, nil}. Also reachable if a source builds its slice with make([]*x509.Certificate, n) and fails to fill every index, or appends a nil on a parse failure instead of returning an error.

Common situations: A plugin implementing the CA/CertificateProvider interfaces with sloppy slice handling; a source that reads a directory of DER files, skips unreadable ones by appending nil, and the operator points it at a partially-readable directory.

Understand the failure class

Related errors


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