The SSH handshake failed error occurs when Terraform's remote-exec or file provisioner cannot establish a successful SSH connection to a remote host. This is typically due to authentication configuration issues, incorrect credentials, key type mismatches, or timing problems during instance initialization.
The "ssh: handshake failed" error indicates that while Terraform was able to initiate a TCP connection to the remote host on port 22, the SSH protocol handshake and authentication phase failed. This error commonly appears in several contexts: - **Terraform remote-exec provisioner**: When running commands on newly created instances via SSH - **Terraform file provisioner**: When uploading or downloading files via SCP (which uses SSH) - **SSH connection block**: When configuring how Terraform should communicate with a resource The handshake failure typically has one of two root causes: 1. **Authentication failed**: The provided credentials (key or password) don't match what the remote host expects 2. **Connection configuration mismatch**: The SSH client and server negotiated incompatible settings during the handshake phase The error message may include additional details like "ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain" which tells you what authentication methods were attempted.
Before troubleshooting Terraform, confirm you can SSH to the instance manually:
ssh -i /path/to/private/key.pem ubuntu@instance-ip-addressIf this works, the instance and firewall are properly configured. The issue is in your Terraform connection block configuration.
If this fails with the same handshake error, the problem is with your SSH setup, not Terraform specifically.
Terraform requires the private_key argument (not the deprecated key_file). Load it using the file() function:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
key_name = aws_key_pair.deployer.key_name
connection {
type = "ssh"
user = "ubuntu" # or ec2-user, centos, etc - depends on AMI
private_key = file("~/.ssh/my-key.pem") # CORRECT
# NOT: key_file = "~/.ssh/my-key.pem" # WRONG - deprecated
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"echo 'Connected!'"
]
}
}The file() function reads the key from disk. You can also use variables:
variable "private_key" {
type = string
sensitive = true
}
connection {
type = "ssh"
user = "ubuntu"
private_key = var.private_key
host = self.public_ip
}Different cloud images have different default users. Using the wrong user causes handshake failures:
- Ubuntu (AWS): ubuntu
- Amazon Linux 2: ec2-user
- CentOS: centos
- RHEL (AWS): ec2-user
- Debian: admin
- Fedora: ec2-user
- Windows Server: N/A - use WinRM, not SSH
Example for different images:
# Ubuntu AMI
connection {
type = "ssh"
user = "ubuntu" # Ubuntu's default user
private_key = file("~/.ssh/key.pem")
host = self.public_ip
}
# Amazon Linux 2
connection {
type = "ssh"
user = "ec2-user" # Amazon Linux's default user
private_key = file("~/.ssh/key.pem")
host = self.public_ip
}To find the correct user for an AMI, check the AMI description in the AWS console or AWS documentation.
SSH is very strict about key file permissions. Keys with overly permissive permissions will be rejected:
# Check current permissions
ls -la ~/.ssh/my-key.pem
# Set correct permissions (read/write for owner only)
chmod 600 ~/.ssh/my-key.pem
# Or for older systems
chmod 400 ~/.ssh/my-key.pemThe private key file should be owned by your user and readable only by the owner. If you see permissions like -rw-rw-r-- or -rw-r--r--, SSH will refuse to use it.
Newly created instances need time to fully boot and start the SSH daemon. Add a timeout and optional delay:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
key_name = aws_key_pair.deployer.key_name
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/key.pem")
host = self.public_ip
# Allow up to 2 minutes for the instance to be ready
timeout = "2m"
}
# Optional: wait for instance status checks to pass
provisioner "remote-exec" {
inline = [
"echo 'Instance is ready'"
]
}
depends_on = [aws_instance.example]
}
# Or add an explicit wait
resource "time_sleep" "wait_for_instance" {
depends_on = [aws_instance.example]
create_duration = "30s"
}
resource "aws_instance" "example" {
# ... configuration ...
provisioner "remote-exec" {
inline = ["echo 'Instance ready'"]
}
depends_on = [time_sleep.wait_for_instance]
}Ensure the security group attached to the instance allows inbound SSH traffic:
resource "aws_security_group" "allow_ssh" {
name = "allow_ssh"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["YOUR_IP/32"] # Restrict to your IP for security
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "example" {
# ... other configuration ...
vpc_security_group_ids = [aws_security_group.allow_ssh.id]
}Warning: Only use 0.0.0.0/0 for testing. In production, restrict SSH to specific IPs.
Sometimes SSH agent conflicts or host key verification issues cause handshake failures. Try disabling both:
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/key.pem")
host = self.public_ip
# Disable SSH agent to avoid conflicts
agent = false
# Skip host key verification (less secure, but useful for testing)
# This adds the host to known_hosts automatically
host_key = "ignore" # Terraform 1.2+
}Note: Disabling host key verification reduces security. Only use for development/testing. In production, add the host to your known_hosts file properly.
Ensure the public key on the remote instance matches your private key:
# Extract the public key from your private key
ssh-keygen -y -f ~/.ssh/my-key.pem
# On the remote instance, check authorized_keys
ssh -i ~/.ssh/my-key.pem ubuntu@instance-ip
cat ~/.ssh/authorized_keysThe public key output from ssh-keygen -y must appear in the instance's ~/.ssh/authorized_keys file.
When using Terraform key pairs, ensure the correct key is specified:
resource "aws_key_pair" "deployer" {
key_name = "deployer-key"
public_key = file("~/.ssh/my-key.pub")
}
resource "aws_instance" "example" {
key_name = aws_key_pair.deployer.key_name
}Some systems have issues with certain SSH key types. Try testing with different key types:
# Generate an RSA key (most compatible)
ssh-keygen -t rsa -b 4096 -f ~/.ssh/terraform-rsa.pem -N ""
# Or ed25519 (more modern, smaller)
ssh-keygen -t ed25519 -f ~/.ssh/terraform-ed25519.pem -N ""
# Or ECDSA
ssh-keygen -t ecdsa -b 521 -f ~/.ssh/terraform-ecdsa.pem -N ""Update your Terraform to use the specific key:
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/terraform-rsa.pem") # Try different keys
host = self.public_ip
}If one key type works but others don't, the issue is key type compatibility between your SSH client and the server.
Use Terraform debug logging to see detailed connection information:
# Enable debug logging
export TF_LOG=DEBUG
# Run your Terraform command
terraform apply
# Save to a file for easier analysis
export TF_LOG_PATH=./terraform-debug.log
terraform apply
# Disable when done
unset TF_LOG
unset TF_LOG_PATHThe debug logs will show:
- Which connection parameters are being used
- What SSH methods are being attempted
- Whether the connection succeeded or where it failed
Look for lines containing "handshake" or "ssh:" in the output.
Understanding SSH authentication methods:
SSH supports several authentication methods. The error message "attempted methods [none publickey]" means Terraform tried none and publickey. If publickey fails, the handshake is aborted. Other methods like password or keyboard-interactive aren't even attempted.
Key type compatibility issues:
Different versions of SSH implementations have varying support for key types:
- RSA (ssh-rsa): Widely supported but older
- ed25519: Modern, secure, but not supported on very old systems
- ECDSA: Good balance, supported on most modern systems
The issue "key type ssh-rsa not in PubkeyAcceptedKeyTypes" means the server has disabled RSA keys for security reasons (common in recent OpenSSH versions). Switch to ed25519 or ECDSA.
SSH agent conflicts:
When agent = true (default), Terraform uses your SSH agent. If the agent:
- Isn't running
- Has different keys than expected
- Has keys with passphrase protection
...it can cause authentication failures. Setting agent = false forces Terraform to use only the specified private_key.
Host key verification:
By default, SSH verifies that the remote host's key hasn't changed (protection against MITM attacks). This requires:
1. The host key in your ~/.ssh/known_hosts file, OR
2. Host key verification disabled (less secure)
When connecting to new instances that aren't in known_hosts, you might see different errors depending on your SSH configuration. Terraform usually handles this by accepting the key automatically on first connection.
Provisioner best practices:
While provisioners work, the Terraform team recommends alternatives:
- Use user data scripts instead of remote-exec for most cases
- Use configuration management tools (Ansible, Chef, Puppet) for complex setup
- Consider cloud-init or user data for initial configuration
Provisioners should be a last resort because they make Terraform harder to reason about (imperative vs declarative), hide infrastructure setup, and are harder to troubleshoot.
For Windows instances:
SSH handshake errors on Windows instances are often due to Windows requiring WinRM instead of SSH. Use the winrm connection type instead. Windows usually requires Sysprep or a custom AMI with WinRM enabled, and firewall rules allowing port 5985/5986.
Error: Error rendering template: template not found
How to fix "template not found" error in Terraform
Error: Error generating private key
How to fix 'Error generating private key' in Terraform
Error creating Kubernetes Service: field is immutable
How to fix "field is immutable" errors in Terraform
Error: Error creating local file: open: permission denied
How to fix "Error creating local file: permission denied" in Terraform
Error: line endings have changed from CRLF to LF
Line endings have changed from CRLF to LF in Terraform