JuliusBrussee/caveman · error

%s (%s) contains no valid PEM certificate

Error message

%s (%s) contains no valid PEM certificate

What it means

Thrown by rootsWithCAFile (chhttp.go:191) when the PEM decode loop finished without adding a single certificate: the file decoded, contained no unparseable CERTIFICATE blocks, but also zero CERTIFICATE-type blocks. Typical content is a PRIVATE KEY block, a CSR, or some other PEM/non-PEM payload. Setting a CA-file env var to such a file is treated as a configuration error, not an empty success.

Source

Thrown at shared/platform/chhttp/chhttp.go:191

		block, rest = pem.Decode(rest)
		if block == nil {
			break
		}
		if block.Type != "CERTIFICATE" {
			continue
		}
		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return nil, fmt.Errorf("%s (%s): certificate %d is unparseable, so the bundle is incomplete and must not be half-trusted: %w", caFileEnv, path, added+1, err)
		}
		roots.AddCert(cert)
		added++
	}
	if bytes.Contains(rest, []byte("-----BEGIN")) {
		return nil, fmt.Errorf("%s (%s): trailing PEM block is truncated after %d certificate(s), so the bundle is incomplete and must not be half-trusted", caFileEnv, path, added)
	}
	if added == 0 {
		return nil, fmt.Errorf("%s (%s) contains no valid PEM certificate", caFileEnv, path)
	}
	return roots, nil
}

// errTransport fails every request with the configuration error that produced
// it. A client constructor cannot return an error, and falling back to the
// default transport would silently trade a rejected TLS configuration for
// unpinned verification — so the client is built, and refuses to send.
type errTransport struct{ err error }

func (t errTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, t.err }

// queryTransport is the shared connection pool behind ClickHouse query clients.
//
// http.DefaultTransport keeps only DefaultMaxIdleConnsPerHost (2) idle
// connections per host, so the 3rd+ concurrent ClickHouse read dials a fresh
// connection and throws it away on completion — paying a TCP + TLS handshake per
// query. Report handlers fan several queries out at once and production reads

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Confirm the file actually contains -----BEGIN CERTIFICATE----- blocks: grep 'BEGIN CERTIFICATE' <file>.
  2. If the material is PKCS#12, convert it first: openssl pkcs12 -in store.p12 -nokeys -out bundle.pem.
  3. If it is a DER certificate, convert: openssl x509 -inform der -in ca.der -out bundle.pem.
  4. Point the env var at the CA certificate chain, never at the private key.

Example fix

# before
export CLICKHOUSE_CA_FILE=/certs/tls.key   # wrong artifact

# after
export CLICKHOUSE_CA_FILE=/certs/ca-bundle.pem  # contains CERTIFICATE blocks
Defensive patterns

Strategy: validation

Validate before calling

data, _ := os.ReadFile(path)
if !bytes.Contains(data, []byte("-----BEGIN CERTIFICATE-----")) {
    return fmt.Errorf("%s contains no PEM certificate; not a CA bundle", path)
}

Try / catch

if err := validateBundleHasCerts(path); err != nil {
    return err // do not default to an empty pool silently
}

Prevention

When it happens

Trigger: Pointing the CA-file env var at the wrong artifact: a TLS private key (-----BEGIN PRIVATE KEY-----), a certificate signing request, a keystore in another format (PKCS#12/.p12, DER binary), or a documentation file. All non-CERTIFICATE blocks are skipped by the loop, added stays 0, and this error returns.

Common situations: Confusing tls.key/tls.crt during setup; putting a .p12 file where PEM was expected; leaving a placeholder file from an install template; generating a key but never exporting the certificate.

Understand the failure class

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/c09d8405958d72ca. Report an issue: GitHub.