grafana/k6 · error
encrypted pkcs8 formatted key is not supported
Error message
encrypted pkcs8 formatted key is not supported
What it means
With a non-empty tls.password, k6's decryptPrivateKey handles encrypted PEM keys using x509.DecryptPEMBlock, which only understands the legacy PEM encryption format (blocks carrying a DEK-Info header, i.e. 'BEGIN RSA PRIVATE KEY'/'BEGIN EC PRIVATE KEY' with traditional encryption). A block explicitly typed 'ENCRYPTED PRIVATE KEY' is PKCS#8 encryption, a different container that the deprecated API cannot decrypt, so k6 rejects it up front rather than producing garbage.
Solutions
- Convert to the traditional encrypted PEM format k6 supports: 'openssl rsa -aes256 -in enc8.key -out trad.key' (prompts for passwords), or for EC keys 'openssl ec -aes256 -in enc8.key -out trad.key'.
- Or decrypt the key entirely and drop the password from connect params: 'openssl pkcs8 -in enc8.key -out plain.key' then pass it without tls.password (unencrypted PKCS#8 is fine; only the ENCRYPTED variant is rejected).
- Prefer keeping keys unencrypted on disk with restricted permissions in CI, letting the pipeline inject them, to avoid the legacy-format dance altogether.
Example fix
# before: encrypted PKCS#8 (unsupported)
openssl pkcs8 -topk8 -in key.pem -out enc.key -passout pass:secret
# tls: { key: open('enc.key'), password: 'secret' } -> error
# after: traditional encrypted PEM
openssl rsa -aes256 -in key.pem -out trad.key -passout pass:secret
# tls: { key: open('trad.key'), password: 'secret' } -> ok Defensive patterns
Strategy: validation
Validate before calling
const isEncryptedPkcs8 = (pem) => pem.includes('-----BEGIN ENCRYPTED PRIVATE KEY-----');
const keyPem = open('./client.key');
if (isEncryptedPkcs8(keyPem)) {
throw new Error('k6 cannot use encrypted PKCS#8 keys; convert with: openssl rsa -aes256 -in client.key -out client.trad.key');
} Type guard
const isSupportedEncryptedPem = (pem) => /-----BEGIN (RSA|EC|DSA)? ?PRIVATE KEY-----/.test(pem) && !pem.includes('ENCRYPTED PRIVATE KEY'); Try / catch
try {
client.connect(addr, { tls: { cert, key, password } });
} catch (e) {
if (/encrypted pkcs8 formatted key is not supported/.test(e.message)) {
throw new Error('Convert the key: openssl rsa -aes256 -in enc8.key -out trad.key');
}
throw e;
} Prevention
- Avoid 'openssl pkcs8 -topk8' with -passout for keys used with k6.
- Prefer unencrypted keys with tight file permissions in CI, then omit the password.
- Check the PEM header line of every encrypted key before wiring it into tls params.
When it happens
Trigger: client.connect(addr, { tls: { key: <pem starting with '-----BEGIN ENCRYPTED PRIVATE KEY-----'>, password: '...' } }). Such keys come from 'openssl pkcs8 -topk8 -passout ...' or modern OpenSSL 3 defaults when writing encrypted keys.
Common situations: Security teams mandating PKCS#8 encrypted client keys; converting keys with 'openssl pkcs8 -topk8' and using the output unchanged in k6; same limitation exists in k6's TLS options (--tls) since the function was lifted from lib/options.go.
Related errors
- failed to decode PEM key
- canceled by client (k6)
- failed to append root certificate to the pool
- gRPC exporter endpoint is required
- grpc protocol does not support URL path
AI-assisted analysis of grafana/k6@8d06114777 (2026-08-18).
Data as JSON: /api/errors/d88664efcaa44fa4.
Report an issue: GitHub.
Appendix: source
Thrown at internal/js/modules/k6/grpc/client.go:136
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,
})
return key, nil
}
func buildTLSConfig(View on GitHub (pinned to 8d06114777)