hashicorp/terraform · error
Failed to read ssh private key: no key found
Error message
Failed to read ssh private key: no key found
What it means
Thrown by readPrivateKey() in the SSH communicator when pem.Decode() returns a nil block for the configured private key string. This means the value supplied to the connection block's private_key is not a valid PEM-encoded block at all. Terraform cannot even begin to parse it as a key.
Source
Thrown at internal/communicator/ssh/provisioner.go:426
usigner, err := ssh.NewSignerFromKey(rawPk)
if err != nil {
return nil, fmt.Errorf("failed to create signer from raw private key %q: %s", rawPk, err)
}
ucertSigner, err := ssh.NewCertSigner(pcert.(*ssh.Certificate), usigner)
if err != nil {
return nil, fmt.Errorf("failed to create cert signer %q: %s", usigner, err)
}
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 configuredView on GitHub (pinned to c9def3e214)
Solutions
- Wrap the path with file(): private_key = file("~/.ssh/id_rsa") so the actual PEM bytes are loaded.
- Confirm the variable is populated: output the first 10 chars and check it starts with '-----BEGIN'.
- If sourcing from a variable, ensure no tool (terraform fmt, YAML, env injection) stripped the embedded newlines.
- Verify you are not accidentally passing a public key or a known_hosts entry.
Example fix
# before
connection {
private_key = var.ssh_key_path # passes the PATH string, not the key
}
# after
connection {
private_key = file(var.ssh_key_path) # loads PEM bytes from disk
} Defensive patterns
Strategy: validation
Validate before calling
# validate the key is real PEM before the connection block consumes it
variable "ssh_key_path" { type = string }
locals {
key_bytes = file(var.ssh_key_path)
is_pem = strcontains(local.key_bytes, "-----BEGIN")
}
check "key_valid" {
assert {
condition = local.is_pem
error_message = "ssh_key_path does not point to a PEM private key"
}
}
connection { private_key = local.key_bytes } Type guard
# HCL guard: only treat the value as a key if it looks like PEM
locals {
safe_key = strcontains(coalesce(var.ssh_key, ""), "-----BEGIN") ? var.ssh_key : file(var.ssh_key_path)
} Try / catch
# use try() to fall back to file() if a raw value isn't usable
connection {
private_key = try(var.ssh_key, file(var.ssh_key_path))
} Prevention
- Always load keys via file() from a path, never inline a raw value.
- Lint connection blocks in CI so private_key is always file() or a known-good variable.
- Keep keys in a secrets manager and inject them as complete PEM strings, preserving newlines.
When it happens
Trigger: Calling the SSH communicator (provisioner or connection block) where private_key is empty, a literal file PATH instead of file contents, a single-line string with stripped newlines, or arbitrary garbage.
Common situations: Using `private_key = var.key_path` (the path string) instead of `private_key = file(var.key_path)`; a CI/CD variable that collapsed the key to one line or left it empty; reading the key with a helper that trimmed whitespace; copy-paste that dropped the BEGIN/END markers.
Related errors
- ssh client is not connected
- Failed to read ssh private key: password protected keys are
- connection type '%s' not supported
- SSH authentication failed (%s@%s): %w
- failed to parse private key %q: %s
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/61ca292a8ae534e8.
Report an issue: GitHub.