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
- 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.
- 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.
- 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.
- 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
- Store keys as files and load with file(); avoid inline heredocs that mangle newlines.
- Never commit passphrase-protected keys; strip passphrases (ssh-keygen -p) or use agent auth.
- Pin a Terraform version whose golang.org/x/crypto supports your key type (Ed25519, new OpenSSH format).
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- SSH authentication failed (%s@%s): %w
- ssh client is not connected
- SCP failed to start. This usually means that SCP is not prop
- connection type '%s' not supported
- Error creating new client connection via proxy: %s
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/6eca17b61b67e272.
Report an issue: GitHub.