JuliusBrussee/caveman · error

ca bundle

Error message

ca bundle: %w

What it means

After collecting certificates from ca_bundle and the inherited CA env vars, loadRootCAs calls cabundle.PoolOf to build an x509.CertPool (proxy/internal/config/config.go:229). If pooling fails — e.g. no usable certificates among the parsed ones — the error is wrapped as 'ca bundle: <err>' and startup fails. Distinct from the ca_bundle file-level error, this one concerns assembling the parsed certificates into a usable pool.

Solutions

  1. Ensure the bundle contains at least one valid PEM CERTIFICATE block
  2. Re-export the corporate chain: openssl s_client -showcerts or your MITM tool's export function, saving full chain PEM
  3. Check inherited env vars (SSL_CERT_FILE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS) point at PEM certificate files
  4. Convert non-PEM formats: openssl pkcs7 -print_certs -in file.p7b -out chain.pem

Example fix

// before (bundle.pem holds only a key)
-----BEGIN PRIVATE KEY-----
...
// after (bundle.pem holds the chain)
-----BEGIN CERTIFICATE-----
MIID...
-----END CERTIFICATE-----
Defensive patterns

Strategy: validation

Validate before calling

pemBytes, _ := os.ReadFile(bundlePath)
block, _ := pem.Decode(pemBytes)
if block == nil || block.Type != "CERTIFICATE" {
    return errors.New("bundle has no PEM CERTIFICATE blocks")
}

Try / catch

if err := cfg.loadRootCAs(); err != nil {
    if strings.Contains(err.Error(), "ca bundle:") {
        logger.Error("CA bundle parsed but produced no usable certificates; export a full PEM chain")
    }
    return err
}

Prevention

When it happens

Trigger: ca_bundle or inherited SSL_CERT_FILE/REQUESTS_CA_BUNDLE/NODE_EXTRA_CA_CERTS files parse to zero usable certificates (e.g. a file containing only comments or keys), causing PoolOf to reject the empty/invalid certificate set.

Common situations: Bundle file containing only a private key; an empty placeholder file created by a bootstrap script; env var pointing at the wrong file format (e.g. PKCS#7 or DER instead of PEM certs).

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/836b18562321ff9e. Report an issue: GitHub.

Appendix: source

Thrown at proxy/internal/config/config.go:229

	}
	for _, name := range inheritedCABundleEnv {
		path := strings.TrimSpace(env.String(name, ""))
		if path == "" {
			continue
		}
		loaded, err := cabundle.Certificates(path)
		if err != nil {
			c.SkippedCABundles = append(c.SkippedCABundles, SkippedCABundle{Env: name, Error: err.Error()})
			continue
		}
		certs = append(certs, loaded...)
	}
	if len(certs) == 0 {
		return nil
	}
	pool, err := cabundle.PoolOf(certs)
	if err != nil {
		return fmt.Errorf("ca bundle: %w", err)
	}
	c.rootCAs = pool
	return nil
}

// RootCAs returns the provider TLS trust store, or nil for Go's default.
func (c Config) RootCAs() *x509.CertPool { return c.rootCAs }

// UpstreamProxyFunc returns the Transport.Proxy selector for UpstreamProxy, or
// nil for a direct client. Load parses UpstreamProxy once and rejects bad values
// there, so for a loaded Config this is a cached-field accessor like RootCAs.
// A Config built by hand (tests) never went through that gate, so it parses
// here. An unparseable value dials direct rather than taking a request path
// down with a panic: falling back to the environment default would both hide
// the bad value and quietly move the SSRF boundary to a proxy the caller never
// named.
func (c Config) UpstreamProxyFunc() func(*http.Request) (*url.URL, error) {
	if c.upstreamProxyParsed {

View on GitHub (pinned to 3ee70a1026)