n8n-io/n8n · error · Error

The Private Key field must contain a PEM private key (-----B

Error message

The Private Key field must contain a PEM private key (-----BEGIN PRIVATE KEY-----).

What it means

Thrown by buildClientAssertion when createPrivateKey(formatPemBlock(privateKey)) raises. The wrapper message states the Private Key field must be a PEM private key; the original crypto error is attached as `cause`. The key is used to RS256-sign the client-assertion JWT.

Source

Thrown at packages/@n8n/utils/src/client-assertion.ts:61

export function buildClientAssertion(options: BuildClientAssertionOptions): string {
	const now = Math.floor(Date.now() / 1000);
	const header = { alg: 'RS256', typ: 'JWT', x5t: certificateThumbprint(options.certificate) };
	const payload = {
		aud: options.accessTokenUri,
		iss: options.clientId,
		sub: options.clientId,
		jti: randomUUID(),
		iat: now,
		nbf: now,
		exp: now + ASSERTION_TTL_SECONDS,
	};

	let privateKey: KeyObject;
	try {
		privateKey = createPrivateKey(formatPemBlock(options.privateKey));
	} catch (error) {
		throw new Error(
			'The Private Key field must contain a PEM private key (-----BEGIN PRIVATE KEY-----).',
			{ cause: error },
		);
	}

	// `createSign('RSA-SHA256')` also signs EC/Ed25519 keys, producing a signature
	// that contradicts the pinned `alg: RS256` header. Reject non-RSA keys up front.
	if (privateKey.asymmetricKeyType !== 'rsa') {
		throw new Error('Certificate authentication requires an RSA private key');
	}

	const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;
	const signature = createSign('RSA-SHA256').update(signingInput).sign(privateKey);
	return `${signingInput}.${base64url(signature)}`;
}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Confirm the PEM starts with '-----BEGIN PRIVATE KEY-----' (or 'BEGIN RSA PRIVATE KEY') and has the matching END.
  2. Make sure you pasted the private key, not the certificate.
  3. If the key is encrypted, decrypt it or supply the passphrase and use the appropriate loader.
  4. Inspect err.cause for the specific crypto error (e.g. unsupported key type).

Example fix

// before
buildClientAssertion({ ..., privateKey: certPem }); // swapped
// after
buildClientAssertion({ ..., privateKey: privateKeyPem }); // -----BEGIN PRIVATE KEY-----...
Defensive patterns

Strategy: validation

Validate before calling

import { createPrivateKey } from 'node:crypto';

function assertValidPrivateKeyPem(key: string): void {
  if (!key || !/-----BEGIN (RSA |EC |ENCRYPTED |)PRIVATE KEY-----/.test(key)) {
    throw new Error('privateKey must be a PEM string bounded by BEGIN/END PRIVATE KEY markers');
  }
  createPrivateKey(key); // throws on malformed input
}

Type guard

function looksLikePemPrivateKey(v: unknown): v is string {
  return typeof v === 'string'
    && /-----BEGIN (RSA |EC |ENCRYPTED |OPENSSH |)PRIVATE KEY-----[\s\S]*-----END/.test(v);
}

Try / catch

try {
  return buildClientAssertion(opts);
} catch (e) {
  if (/must contain a PEM private key/i.test(e?.message ?? '')) {
    throw new ConfigError('private key field is not a valid PEM private key', { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing an empty key, a certificate PEM instead of a private key PEM, an encrypted PKCS#8 key without a passphrase, a malformed/truncated PEM, or a key in a format Node cannot parse (e.g. OpenSSH new-format).

Common situations: Swapping the certificate and private key fields in config; copy-paste losing the END marker; exporting a public key by mistake; encrypted keys where the runtime has no passphrase; line-ending corruption (CRLF) breaking PEM parsing.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/2e4133e7f1229290. Report an issue: GitHub.