slackhq/nebula · error

bytes did not contain a proper private key banner

Error message

bytes did not contain a proper private key banner

What it means

pem.Decode succeeded but the block's Type string does not match any recognized private key banner ('NEBULA X25519 PRIVATE KEY' or 'NEBULA ECDSA P256 PRIVATE KEY'). The input is valid PEM, but it is not a nebula private key of the supported kind — often it is a public key, a signing key, or a standard OpenSSL PEM type.

Source

Thrown at cert/pem.go:244

// UnmarshalPrivateKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
// consumed data or an error on failure
func UnmarshalPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
	k, r := pem.Decode(b)
	if k == nil {
		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
	}
	var expectedLen int
	var curve Curve
	switch k.Type {
	case X25519PrivateKeyBanner:
		expectedLen = 32
		curve = Curve_CURVE25519
	case P256PrivateKeyBanner:
		expectedLen = 32
		curve = Curve_P256
	default:
		return nil, r, 0, fmt.Errorf("bytes did not contain a proper private key banner")
	}
	if len(k.Bytes) != expectedLen {
		return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s private key", expectedLen, curve)
	}
	return k.Bytes, r, curve, nil
}

func UnmarshalSigningPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
	k, r := pem.Decode(b)
	if k == nil {
		return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
	}
	var curve Curve
	switch k.Type {
	case EncryptedEd25519PrivateKeyBanner:
		return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
	case EncryptedECDSAP256PrivateKeyBanner:
		return nil, nil, Curve_P256, ErrPrivateKeyEncrypted

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Use the correct file: the entry for tunl.private_key must contain an 'X25519 PRIVATE KEY' or 'ECDSA P256 PRIVATE KEY' nebula banner
  2. Regenerate the key with nebula-cert keygen so the banner matches what UnmarshalPrivateKeyFromPEM expects
  3. Check that you are not passing a signing private key (that belongs in signing_key_path and is parsed by UnmarshalSigningPrivateKeyFromPEM)

Example fix

// before (config)
firewall: ...
tunl: { private_key_path: ./signing.key } // wrong key
// after
tunl: { private_key_path: ./host.key } // X25519/ECDSA P256 private key banner
Defensive patterns

Strategy: validation

Validate before calling

blk, _ := pem.Decode(data)
if blk != nil && blk.Type != "NEBULA X25519 PRIVATE KEY" && blk.Type != "NEBULA ECDSA P256 PRIVATE KEY" {
    return fmt.Errorf("expected a nebula host private key, got PEM type %q", blk.Type)
}

Type guard

func isHostPrivateKey(b []byte) bool {
    blk, _ := pem.Decode(b)
    return blk != nil && (blk.Type == "NEBULA X25519 PRIVATE KEY" || blk.Type == "NEBULA ECDSA P256 PRIVATE KEY")
}

Try / catch

key, _, _, err := nebula.UnmarshalPrivateKeyFromPEM(raw)
if err != nil {
    return fmt.Errorf("key file has the wrong banner for a host private key: %w", err)
}

Prevention

When it happens

Trigger: Call UnmarshalPrivateKeyFromPEM with a PEM block whose k.Type is not X25519PrivateKeyBanner or P256PrivateKeyBanner, e.g. passing a signing (Ed25519/ECDSA) key PEM, a public key PEM, or a 'RSA PRIVATE KEY' / 'PRIVATE KEY' standard block.

Common situations: Swapping the tunl.private_key and tunl.signing_key_path config entries by mistake, pointing at the certificate/public key file instead of the key file, or using keys generated by openssl in standard PEM formats the library does not accept.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/10d7344eb6028d00. Report an issue: GitHub.