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

  1. 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'.
  2. 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).
  3. 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

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


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)