grafana/k6 · error

failed to decode PEM key

Error message

failed to decode PEM key

What it means

When client.connect() is given TLS certificates that include a password, k6 decrypts the private key via decryptPrivateKey (lifted from lib/options.go). pem.Decode must find a PEM block; a key that is not PEM-encoded (binary DER, wrong file, truncated copy-paste, empty content) yields a nil block and this error.

Source

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

	defer func() { _ = fdsetFile.Close() }()
	fdsetBytes, err := io.ReadAll(fdsetFile)
	if err != nil {
		return nil, fmt.Errorf("couldn't read protoset: %w", err)
	}

	fdset := &descriptorpb.FileDescriptorSet{}
	if err = proto.Unmarshal(fdsetBytes, fdset); err != nil {
		return nil, fmt.Errorf("couldn't unmarshal protoset file %s: %w", protosetPath, err)
	}

	return c.convertToMethodInfo(fdset)
}

// Note: this function was lifted from `lib/options.go`
func decryptPrivateKey(key, password []byte) ([]byte, error) {
	block, _ := pem.Decode(key)
	if block == nil {
		return nil, errors.New("failed to decode PEM key")
	}

	blockType := block.Type
	if blockType == "ENCRYPTED PRIVATE KEY" {
		return nil, errors.New("encrypted pkcs8 formatted key is not supported")
	}
	/*
	   Even though `DecryptPEMBlock` has been deprecated since 1.16.x it is still
	   being used here because it is deprecated due to it not supporting *good* cryptography
	   ultimately though we want to support something so we will be using it for now.
	*/
	decryptedKey, err := x509.DecryptPEMBlock(block, password) //nolint:staticcheck
	if err != nil {
		return nil, err
	}
	key = pem.EncodeToMemory(&pem.Block{
		Type:  blockType,
		Bytes: decryptedKey,

View on GitHub (pinned to 93accf6570)

Solutions

  1. Verify the key content starts with -----BEGIN RSA PRIVATE KEY----- (or EC/PRIVATE KEY) and ends with -----END ...-----
  2. Convert DER to PEM: openssl rsa -inform DER -in key.der -out key.pem
  3. Check the argument order in the certs triple [cert, key, password] and that the key file was read completely

Example fix

// before: keyFile is DER/binary
client.connect(addr, { tls: { certs: [[certPem, keyDer, 'pass']] } });

// after: convert once, then
// openssl rsa -inform DER -in key.der -out key.pem
client.connect(addr, { tls: { certs: [[certPem, keyPem, 'pass']] } });
Defensive patterns

Strategy: validation

Validate before calling

function isPemKey(s) {
  return typeof s === 'string' && /^-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(s.trim());
}

if (!isPemKey(keyPem)) throw new Error('tls key is not PEM-encoded (missing BEGIN PRIVATE KEY fence)');
client.connect(addr, { tls: { certs: [[certPem, keyPem, password]] } });

Prevention

When it happens

Trigger: client.connect(addr, { tls: { certs: [[certPem, keyPem, password]] } }) where keyPem is DER/binary, is actually the certificate or CA file, or has lost its '-----BEGIN ... PRIVATE KEY-----' header/footer.

Common situations: Pointing the key slot at a .der or .crt file; PEM fences broken by templating or YAML/JSON escaping; key variable left empty so another value is passed by mistake.

Understand the failure class

Related errors


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