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 configured

View on GitHub (pinned to c9def3e214)

Solutions

  1. Wrap the path with file(): private_key = file("~/.ssh/id_rsa") so the actual PEM bytes are loaded.
  2. Confirm the variable is populated: output the first 10 chars and check it starts with '-----BEGIN'.
  3. If sourcing from a variable, ensure no tool (terraform fmt, YAML, env injection) stripped the embedded newlines.
  4. 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

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


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