hashicorp/terraform · error

Failed to parse ssh private key

Error message

Failed to parse ssh private key: %s

What it means

In readPrivateKey, the private key is first PEM-decoded manually. If PEM decoding succeeds (block is non-nil) and the key is not password-protected (no Proc-Type 4,ENCRYPTED header), ssh.ParsePrivateKey is called. This error fires when ParsePrivateKey still fails despite valid PEM — typically due to an unsupported algorithm, corrupted key body, or a key format the linked x/crypto version cannot parse. Note this path does NOT catch password-protected keys of newer formats (OpenSSH new format) where the Proc-Type header is absent.

Solutions

  1. Regenerate the SSH key with ssh-keygen using a standard format: ssh-keygen -t rsa -b 2048 -m PEM -f keyfile.
  2. If the key is in OpenSSH new format, convert to PEM: ssh-keygen -p -m PEM -f keyfile.
  3. Update golang.org/x/crypto to a version that supports your key's algorithm and format.
  4. Verify the key body is intact — compare against the original file with diff or a checksum.
  5. Ensure no trailing characters, BOM, or encoding issues are present in the key material.

Example fix

# before — key in a format ParsePrivateKey can't handle
connection {
  private_key = var.pkcs8_or_new_format_key
}

# after — convert to standard PEM format
ssh-keygen -p -m PEM -f ~/.ssh/id_rsa
connection {
  private_key = file("~/.ssh/id_rsa")
}
Defensive patterns

Strategy: validation

Validate before calling

// Pre-validate the private key using the same logic as readPrivateKey
func validateReadPrivateKey(pk string) error {
    block, _ := pem.Decode([]byte(pk))
    if block == nil {
        return errors.New("no PEM block found in private key")
    }
    if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
        return errors.New("password-protected keys are not supported; decrypt the key first")
    }
    _, err := ssh.ParsePrivateKey([]byte(pk))
    return err
}

Type guard

func isUnencryptedPEMKey(pk string) bool {
    block, _ := pem.Decode([]byte(pk))
    if block == nil {
        return false
    }
    return block.Headers["Proc-Type"] != "4,ENCRYPTED"
}

Try / catch

signer, err := ssh.ParsePrivateKey([]byte(pk))
if err != nil {
    return nil, fmt.Errorf("cannot parse SSH private key (check format/algorithm): %w", err)
}

Prevention

When it happens

Trigger: Providing a private_key that is valid PEM (decodes successfully) but whose algorithm or internal structure ssh.ParsePrivateKey cannot handle. Triggered by: PKCS#8-wrapped keys unsupported by older x/crypto, keys with unexpected headers, or subtly corrupted key bodies that decode as PEM but fail cryptographic parsing.

Common situations: User provides a key generated by a newer ssh-keygen or by OpenSSL in PKCS#8 format that the x/crypto version doesn't support. Key was re-encoded or transformed by a secrets manager. Key uses a cipher or KDF that ParsePrivateKey doesn't recognize. Key is a valid PEM but for a non-SSH purpose (e.g., TLS certificate key) that happens to decode.

Understand the failure class

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/6eca17b61b67e272. Report an issue: GitHub.

Appendix: source

Thrown at internal/communicator/ssh/provisioner.go:436

	return ssh.PublicKeys(ucertSigner), nil
}

func readPrivateKey(pk string) (ssh.AuthMethod, error) {
	// We parse the private key on our own first so that we can
	// show a nicer error if the private key has a password.
	block, _ := pem.Decode([]byte(pk))
	if block == nil {
		return nil, errors.New("Failed to read ssh private key: no key found")
	}
	if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
		return nil, errors.New(
			"Failed to read ssh private key: password protected keys are\n" +
				"not supported. Please decrypt the key prior to use.")
	}

	signer, err := ssh.ParsePrivateKey([]byte(pk))
	if err != nil {
		return nil, fmt.Errorf("Failed to parse ssh private key: %s", err)
	}

	return ssh.PublicKeys(signer), nil
}

func connectToAgent(connInfo *connectionInfo) (*sshAgent, error) {
	if !connInfo.Agent {
		// No agent configured
		return nil, nil
	}

	agent, conn, err := sshagent.New()
	if err != nil {
		return nil, err
	}

	// connection close is handled over in Communicator
	return &sshAgent{

View on GitHub (pinned to d32a084675)