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
- 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.
- Verify the key decodes: 'openssl pkey -in client.key -noout && echo ok'.
- If the key is not encrypted, simply omit tls.password - k6 then uses the PEM as-is via tls.X509KeyPair.
- 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
- Always load key material with open(), never inline paths.
- Verify PEMs in CI: openssl pkey -in key.pem -noout.
- Skip tls.password entirely for unencrypted keys.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- encrypted pkcs8 formatted key is not supported
- failed to append root certificate to the pool
- no gRPC connection, you must call connect first
- canceled by client (k6)
- gRPC exporter endpoint is required
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)