grafana/k6 · error

failed to append ca certificate [%d] from PEM

Error message

failed to append ca certificate [%d] from PEM

What it means

During grpc.connect() TLS setup, every entry of tls.cacerts (or the single-string form) is passed to x509.CertPool.AppendCertsFromPEM; when an entry yields no parsable CERTIFICATE block, buildTLSConfig fails with 'failed to append ca certificate [<index>] from PEM' (internal/js/modules/k6/grpc/client.go:157). The index is the position in the cacerts array, identifying exactly which entry is broken. Values must be the PEM content itself, never a file path.

Source

Thrown at internal/js/modules/k6/grpc/client.go:157

	*/
	decryptedKey, err := x509.DecryptPEMBlock(block, password) //nolint:staticcheck
	if err != nil {
		return nil, err
	}
	key = pem.EncodeToMemory(&pem.Block{
		Type:  blockType,
		Bytes: decryptedKey,
	})
	return key, nil
}

func buildTLSConfig(parentConfig *tls.Config, certificate, key []byte, caCertificates [][]byte) (*tls.Config, error) {
	var cp *x509.CertPool
	if len(caCertificates) > 0 {
		cp, _ = x509.SystemCertPool()
		for i, caCert := range caCertificates {
			if ok := cp.AppendCertsFromPEM(caCert); !ok {
				return nil, fmt.Errorf("failed to append ca certificate [%d] from PEM", i)
			}
		}
	}

	// Ignoring 'TLS MinVersion is too low' because this tls.Config will inherit MinValue and MaxValue
	// from the vu state tls.Config

	tlsCfg := &tls.Config{
		CipherSuites:       parentConfig.CipherSuites,
		InsecureSkipVerify: parentConfig.InsecureSkipVerify, //nolint:gosec
		MinVersion:         parentConfig.MinVersion,
		MaxVersion:         parentConfig.MaxVersion,
		Renegotiation:      parentConfig.Renegotiation,
		RootCAs:            cp,
	}
	if len(certificate) > 0 && len(key) > 0 {
		cert, err := tls.X509KeyPair(certificate, key)
		if err != nil {

View on GitHub (pinned to 93accf6570)

Solutions

  1. Fix the entry indicated by the index: provide full PEM text including the -----BEGIN CERTIFICATE----- / -----END CERTIFICATE----- lines
  2. Load PEMs from files with open() rather than env vars, or restore real newlines (str.split('\\n').join('\n') style repair)
  3. Validate each cert with a PEM-shape check before calling connect (see defense)

Example fix

// before (env var lost the newlines)
connect(addr, { tls: { cacerts: [__ENV.CA_PEM] } })

// after (read the PEM file directly)
connect(addr, { tls: { cacerts: [open('ca.pem')] } })
Defensive patterns

Strategy: validation

Validate before calling

const PEM_CERT = /-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----[\s]*$/;
function assertCaCerts(cacerts) {
  const list = Array.isArray(cacerts) ? cacerts : [cacerts];
  list.forEach((c, i) => {
    if (!PEM_CERT.test(c)) throw new Error(`cacerts[${i}] is not a valid PEM certificate`);
  });
  return cacerts;
}
connect(addr, { tls: { cacerts: assertCaCerts([open('ca.pem'), __ENV.EXTRA_CA]) } });

Type guard

function isPemCert(v) {
  return typeof v === 'string' && /-----BEGIN CERTIFICATE-----/.test(v) && /-----END CERTIFICATE-----/.test(v);
}

Try / catch

try { client.connect(addr, params); } catch (e) { if (/failed to append ca certificate \[(\d+)\]/.test(e.message)) { /* fix that cacerts entry, reload from file, retry */ } throw e; }

Prevention

When it happens

Trigger: connect(addr, { tls: { cacerts: [open('ca.pem'), __ENV.EXTRA_CA] } }) where any entry is not valid PEM: a file path passed instead of contents, newlines stripped by env vars/CI secrets, cert+key concatenated into cacerts, or a mangled Base64 body.

Common situations: Loading CA material from environment variables or secret managers that collapse '\n'; pasting single-line PEMs from browsers; mixing up the cacerts and cert/key options.

Understand the failure class

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/1bbf1f3e2cc068ab. Report an issue: GitHub.