grafana/k6 · error

failed to decode PEM key

Error message

failed to decode PEM key

What it means

When client.connect() receives TLS params containing a non-empty password (params.tls.password), k6 runs decryptPrivateKey on the key PEM before building the TLS config. pem.Decode returning nil means the key bytes are not a PEM block at all - no '-----BEGIN...-----' envelope - so decryption cannot even start. (Lifted from lib/options.go, same behavior as the --tls K6_OPTIONS handling of encrypted keys.)

Solutions

  1. Ensure tls.key contains the full PEM text including the BEGIN/END PRIVATE KEY lines - usually via open('./client.key') which k6 reads as a string.
  2. Verify the key decodes: 'openssl pkey -in client.key -noout && echo ok'.
  3. If the key is not encrypted, simply omit tls.password - k6 then uses the PEM as-is via tls.X509KeyPair.
  4. Check you did not swap cert and key values.

Example fix

// before: path string instead of PEM contents
client.connect('grpc.acme.com:443', {
  tls: { cert: './cert.pem', key: './key.pem', password: 'secret' }, // throws
});

// after: read the PEM contents
client.connect('grpc.acme.com:443', {
  tls: { cert: open('./cert.pem'), key: open('./key.pem'), password: 'secret' },
});
Defensive patterns

Strategy: validation

Validate before calling

function assertPemKey(pem, label = 'key') {
  if (!/-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(pem)) {
    throw new Error(`${label} is not a PEM private key (missing BEGIN/END envelope)`);
  }
  return pem;
}
// usage
client.connect(addr, { tls: { cert: open('c.pem'), key: assertPemKey(open('k.pem')), password: pw } });

Type guard

const looksLikePem = (s) => typeof s === 'string' && s.includes('-----BEGIN') && s.includes('-----END');

Try / catch

try {
  client.connect(addr, params);
} catch (e) {
  if (/failed to decode PEM key/.test(e.message)) {
    throw new Error('tls.key must contain PEM contents (use open()), not a path or DER bytes');
  }
  throw e;
}

Prevention

When it happens

Trigger: client.connect(addr, { tls: { cert, key, password } }) where key is a DER/binary key, a corrupted or truncated PEM, a file path instead of contents, or empty/whitespace bytes. The password being non-empty is what routes the key through decryptPrivateKey; an unencrypted key with no password never hits this path.

Common situations: Passing a file path in tls.key instead of the PEM contents (forgetting open()); double-encoding or line-wrap corruption when pasting the PEM into JSON; mixing up the cert and key fields so binary/mismatched data lands in key; trailing BOM/whitespace from copy-paste breaking pem.Decode.

Understand the failure class

Related errors


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

Appendix: source

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

	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 8d06114777)