hashicorp/terraform · error

Failed to parse ssh private key: %s

Error message

Failed to parse ssh private key: %s

What it means

Raised by the SSH communicator's readPrivateKey when ssh.ParsePrivateKey cannot interpret the private key string supplied via the connection block's private_key. The function already rejects PEM-encrypted/`Proc-Type: 4,ENCRYPTED` keys earlier (lines 428-431) and no-key input at line 426, so reaching line 436 means a PEM block decoded but x/crypto/ssh refused it: unsupported key type, malformed/corrupted body, wrong OpenSSH/PEM format, or non-key data. It surfaces during Terraform remote-exec provisioner / connection setup when authenticating to the target over SSH.

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 c9def3e214)

Solutions

  1. Regenerate/convert the key to an unencrypted PEM or OpenSSH format: `ssh-keygen -p -f id_rsa` to strip the passphrase, or `ssh-keygen -t rsa -m PEM -f id_rsa` for classic PEM.
  2. Verify the key is valid for the same Go crypto Terraform uses: `ssh-keygen -l -f id_rsa` and `file id_rsa` should report an OpenSSH/RSA private key, not ASCII text.
  3. Pass the key from a clean source: reference `file("~/.ssh/id_rsa")` rather than an inline heredoc, ensuring no surrounding quotes, whitespace, or escaped newlines survive interpolation.
  4. If you need a passphrase-protected key, decrypt it to a temp file for Terraform or switch to `agent = true` and use ssh-agent (`ssh-add`).

Example fix

// before
connection {
  type        = "ssh"
  private_key = "-----BEGIN RSA PRIVATE KEY-----\n...truncated/garbled...\n"
}

// after
connection {
  type        = "ssh"
  private_key = file("~/.ssh/id_rsa_nopass")  // unencrypted PEM key
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate an SSH private key before handing it to Terraform's SSH communicator.
import (
    "encoding/pem"
    "crypto/x509"
    "golang.org/x/crypto/ssh"
)
func validPrivateKeyMaterial(s string) error {
    block, _ := pem.Decode([]byte(s))
    if block == nil { return fmt.Errorf("no PEM block in private key") }
    if block.Headers["Proc-Type"] == "4,ENCRYPTED" { return fmt.Errorf("key is passphrase-protected") }
    if _, err := ssh.ParsePrivateKey([]byte(s)); err != nil {
        return fmt.Errorf("key not parseable by x/crypto/ssh: %w", err)
    }
    _ = x509.ParsePKCS1PrivateKey // accepted formats also covered by ssh.ParsePrivateKey
    return nil
}

Type guard

// Guard against empty/garbage key values when generating config.
func isNonEmptyPEM(v string) bool {
    return strings.Contains(v, "-----BEGIN ") && strings.Contains(v, "-----END ")
}

Prevention

When it happens

Trigger: A `connection { type="ssh" private_key = ... }` whose value is not a parseable unencrypted OpenSSH/PEM private key: trailing whitespace or literal quotes wrapped around the key, a key in the new OpenSSH format unsupported by the bundled golang.org/x/crypto, an ECDSA/Ed25519/DSS key the build rejects, or the public-key half pasted by mistake. Also when file("...") returns an error string because the path is wrong.

Common situations: Pasting a key via an inline heredoc that mangles newlines, loading a key whose file path is wrong so file() yields an error message, encrypted keys that bypass the Proc-Type check (e.g. `-----BEGIN OPENSSH PRIVATE KEY-----` with a passphrase), or running a Terraform version linked against an older x/crypto predating a key type.

Understand the failure class

Related errors


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