grafana/k6 · error

failed to append certificate from PEM: %w

Error message

failed to append certificate from PEM: %w

What it means

grpc.connect() with mutual TLS calls tls.X509KeyPair(tls.cert, tls.key); any failure to build the key pair - invalid PEM in either argument, a certificate that does not match the key, or an unsupported key encoding - is wrapped as 'failed to append certificate from PEM' (internal/js/modules/k6/grpc/client.go:176). Note the exact tls map keys: cert, key, password, cacerts (validated earlier in parseConnectTLSParam).

Source

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

			}
		}
	}

	// 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 {
			return nil, fmt.Errorf("failed to append certificate from PEM: %w", err)
		}
		tlsCfg.Certificates = []tls.Certificate{cert}
	}
	return tlsCfg, nil
}

func buildTLSConfigFromMap(parentConfig *tls.Config, tlsConfigMap map[string]any) (*tls.Config, error) {
	var cert, key, pass []byte
	var ca [][]byte
	var err error
	if certstr, ok := tlsConfigMap["cert"].(string); ok {
		cert = []byte(certstr)
	}
	if keystr, ok := tlsConfigMap["key"].(string); ok {
		key = []byte(keystr)
	}
	if passwordStr, ok := tlsConfigMap["password"].(string); ok {
		pass = []byte(passwordStr)

View on GitHub (pinned to 93accf6570)

Solutions

  1. Verify the pair matches: compare `openssl x509 -noout -modulus` output with `openssl rsa -noout -modulus` (moduli must be identical)
  2. Re-export clean PEM files and reference them with open()
  3. For encrypted keys pass tls.password (PKCS#8-encrypted is unsupported - convert with `openssl rsa -in enc.key -out plain.key`)

Example fix

# before: mismatched pair (rotated cert, old key)
connect(addr, { tls: { cert: open('client_v2.crt'), key: open('client_v1.key') } })

# after: matching pair
connect(addr, { tls: { cert: open('client_v2.crt'), key: open('client_v2.key') } })
Defensive patterns

Strategy: validation

Validate before calling

const isPem = (s) => typeof s === 'string' && /-----BEGIN [A-Z0-9 ]+-----/.test(s);
function assertClientTLS(tls) {
  if ((tls.cert && !isPem(tls.cert)) || (tls.key && !isPem(tls.key))) {
    throw new Error('tls.cert and tls.key must be PEM strings (file contents, not paths)');
  }
  return tls;
}
client.connect(addr, { tls: assertClientTLS({ cert: open('c.crt'), key: open('c.key') }) });

Try / catch

try { client.connect(addr, { tls }); } catch (e) { if (/failed to append certificate from PEM/.test(e.message)) { /* verify cert/key pair with openssl, fix and retry */ } throw e; }

Prevention

When it happens

Trigger: connect(addr, { tls: { cert: open('client.crt'), key: open('client.key') } }) where the pair mismatches (cert rotated, stale key), either PEM is malformed, or the key is encrypted in an unsupported encoding (encrypted PKCS#8 fails earlier in decryptPrivateKey with its own message).

Common situations: Cert rotation where the new certificate ships but the old key remains; passing the CA bundle as cert; encrypted keys supplied without tls.password; PEMs mangled through env vars.

Understand the failure class

Related errors


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