TerraformPilot

Troubleshooting

Fix Terraform Error - Remote-Exec Connection Refused

Fix the Terraform remote-exec connection refused error. Covers security groups, SSH keys, instance readiness, bastion hosts, and user_data alternatives.

LLuca Berton2 min read

Quick Answer

#

Terraform can't SSH into the instance. Check that the security group allows port 22 from your IP, the instance is fully booted, and the SSH key/user are correct. Consider using user_data instead of remote-exec provisioners.

The Error

#
Error: timeout - last error: dial tcp 10.0.1.50:22: connect: connection refused
Error: timeout - last error: dial tcp 52.1.2.3:22: i/o timeout
Error: timeout waiting for SSH to become available

What Causes This Error

#

1. Security Group Blocks SSH

#

Port 22 isn't open from your machine's IP address:

# Missing or wrong CIDR — your IP isn't allowed
ingress {
  from_port   = 22
  to_port     = 22
  protocol    = "tcp"
  cidr_blocks = ["10.0.0.0/8"]  # Doesn't include your public IP
}

2. Instance Not Ready Yet

#

The EC2 instance is running but SSH daemon hasn't started. This is common with large user_data scripts or slow-booting AMIs.

3. Wrong SSH User or Key

#

Each AMI has a different default SSH user:

AMIDefault User
Amazon Linux / AL2023ec2-user
Ubuntuubuntu
Debianadmin
RHELec2-user
CentOScentos
SUSEec2-user

4. Private Instance Without Bastion

#

The instance has no public IP and you're trying to SSH from outside the VPC.

5. Wrong Key Pair

#

The key pair specified doesn't match the private key in the connection block.

How to Fix It

#

Solution 1: Fix Security Group

#
resource "aws_security_group" "allow_ssh" {
  name   = "allow-ssh"
  vpc_id = aws_vpc.main.id
 
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.my_ip]  # e.g., "203.0.113.50/32"
  }
 
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Solution 2: Configure Connection Block Correctly

#
resource "aws_instance" "web" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"
  key_name               = aws_key_pair.deploy.key_name
  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  subnet_id              = aws_subnet.public.id
 
  # Ensure public IP for direct SSH
  associate_public_ip_address = true
 
  connection {
    type        = "ssh"
    user        = "ubuntu"                        # Match your AMI
    private_key = file("~/.ssh/deploy-key.pem")   # Match key_name
    host        = self.public_ip
    timeout     = "5m"                            # Increase timeout
  }
 
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx",
    ]
  }
}

Solution 3: Add Timeout and Retry

#
connection {
  type        = "ssh"
  user        = "ec2-user"
  private_key = file(var.private_key_path)
  host        = self.public_ip
  timeout     = "10m"  # Default is 5m; increase for slow boots
}

Solution 4: Use a Bastion Host for Private Instances

#
resource "aws_instance" "private_server" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.private.id
  key_name      = aws_key_pair.deploy.key_name
 
  connection {
    type         = "ssh"
    user         = "ec2-user"
    private_key  = file(var.private_key_path)
    host         = self.private_ip
    bastion_host = aws_instance.bastion.public_ip
    bastion_user = "ec2-user"
    bastion_private_key = file(var.private_key_path)
  }
 
  provisioner "remote-exec" {
    inline = ["echo 'Connected via bastion!'"]
  }
}
#

HashiCorp recommends avoiding provisioners. Use user_data or cloud-init instead:

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
 
  user_data = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    systemctl enable nginx
    systemctl start nginx
  EOF
 
  user_data_replace_on_change = true
}

Benefits over remote-exec:

  • No SSH access needed
  • Works with private instances
  • No security group changes
  • Runs during instance boot
  • Idempotent with cloud-init

Solution 6: Debug SSH Connectivity

#
# Test SSH manually
ssh -i key.pem -v ubuntu@52.1.2.3
 
# Check instance status
aws ec2 describe-instance-status --instance-ids i-1234567890abcdef0
 
# View system log for boot issues
aws ec2 get-console-output --instance-id i-1234567890abcdef0

Troubleshooting Checklist

#
  1. ✅ Does the security group allow SSH (port 22) from your IP?
  2. ✅ Does the instance have a public IP (or are you using a bastion)?
  3. ✅ Is the SSH user correct for your AMI?
  4. ✅ Does the private key match the key pair on the instance?
  5. ✅ Is the instance fully booted? (check system log)
  6. ✅ Have you increased the connection timeout?
  7. ✅ Can you SSH manually from your machine?

Prevention Tips

#
  • Prefer user_data over remote-exec — no SSH dependency
  • Use SSM Session Manager for interactive access without opening port 22
  • Set generous timeouts — 10 minutes for instances with large user_data scripts
  • Test SSH manually first before adding provisioners
  • Use associate_public_ip_address = true when SSH is needed from outside VPC
#

Conclusion

#

Connection refused means Terraform can't reach SSH on the target instance. Fix the security group, verify the SSH user and key match the AMI, increase timeouts for slow boots, and use a bastion for private instances. Better yet, replace remote-exec with user_data — it's simpler, more reliable, and doesn't require SSH access.

#provisioner#remote-exec#ssh

Share this article