You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无法从本地SSH访问AWS私有子网内的EC2实例(Terraform部署)

问题:无法从本地直接SSH连接AWS私有子网内的EC2实例

我刚开始学习Terraform与AWS,实操中搭建了VPC、分别部署在公有/私有子网的EC2、NAT网关及Internet Gateway。目前可SSH访问公有子网EC2,且能从该实例ping通私有子网EC2,但无法从本地直接SSH连接私有子网EC2,求解决办法。

配置文件

create-vpc-main.tf

terraform{
    backend "s3" {
      bucket = "norman-personal-s3-bucket"
      key = "lab3-create_vpc"
      region = "ap-southeast-1"
    }
}


provider "aws" {
    region = "us-east-1"
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  enable_dns_support = true
  tags = {
    Name = "Lab 3 - Project VPC"
  }
}

resource "aws_security_group" "http_server_sg" {
  name = "http_server_sg"
  //vpc_id = "vpc-c49ff1be"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = -1
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    name = "http_server_sg"
  }
}

resource "aws_subnet" "public_subnets" {
 count      = length(var.public_subnet_cidrs)
 vpc_id     = aws_vpc.main.id
 cidr_block = element(var.public_subnet_cidrs, count.index)
 availability_zone = element(var.azs, count.index)
 
 tags = {
   Name = "Lab 3 - Public Subnet"
 }
}
 
resource "aws_subnet" "private_subnets" {
 count      = length(var.private_subnet_cidrs)
 vpc_id     = aws_vpc.main.id
 cidr_block = element(var.private_subnet_cidrs, count.index)
 availability_zone = element(var.azs, count.index)
 
 tags = {
   Name = "Lab 3 - Private Subnet" 
 }
}

resource "aws_internet_gateway" "gw" {
 vpc_id = aws_vpc.main.id
 
 tags = {
   Name = "Lab 3 - Project VPC IG"
 }
}

resource "aws_route_table" "second_rt" {
 vpc_id = aws_vpc.main.id
 
 route {
   cidr_block = "0.0.0.0/0"
   gateway_id = aws_internet_gateway.gw.id
 }
 
 tags = {
   Name = "Lab 3 - 2nd Route Table"
 }
}

resource "aws_route_table_association" "public_subnet_asso" {
 count = length(var.public_subnet_cidrs)
 subnet_id      = element(aws_subnet.public_subnets[*].id, count.index)
 route_table_id = aws_route_table.second_rt.id
}

resource "aws_eip" "nat_gateway" {
  vpc = true
}

resource "aws_nat_gateway" "nat_gateway" {
  allocation_id = aws_eip.nat_gateway.id
  subnet_id = "${element(aws_subnet.public_subnets.*.id, 0)}"
  tags = {
    "Name" = "Lab 3 - NG"
  }
}

resource "aws_route_table" "third_rt" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gateway.id
  }
}

resource "aws_route_table_association" "private_subnet_asso" {

  count = length(var.public_subnet_cidrs)

  subnet_id = "${element(aws_subnet.private_subnets.*.id, count.index)}"
  route_table_id = aws_route_table.third_rt.id
}

create-ec2-main.tf

data "aws_vpc" "vpc" {
  filter {
    name = "tag:Name"
    values = ["Lab 3 - Project VPC"]
  }
}

data "aws_subnets" "private_subnets" {

  filter{
    name ="vpc-id"
    values = [data.aws_vpc.vpc.id]
  }

  tags = {
    Name = "Lab 3 - Private Subnet"
  }
}

data "aws_subnet" "private_subnet" {
  for_each = toset(data.aws_subnets.private_subnets.ids)
  id       = each.value
}

data "aws_subnets" "public_subnets" {

  filter{
    name ="vpc-id"
    values = [data.aws_vpc.vpc.id]
  }

  tags = {
    Name = "Lab 3 - Public Subnet"
  }
}

data "aws_subnet" "public_subnet" {
  for_each = toset(data.aws_subnets.public_subnets.ids)
  id       = each.value
}

data "aws_security_groups" "sg" {

  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.vpc.id]
  }
}

data "aws_ami" "aws_linux_2_latest" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*"]
  }
}

data "aws_ami_ids" "aws_linux_2_latest_ids" {
  owners = ["amazon"]
}

terraform{
    backend "s3" {
      bucket = "norman-personal-s3-bucket"
      key = "lab3-create_ec2"
      region = "ap-southeast-1"
    }
}


provider "aws" {
    region = "us-east-1"
}



resource "aws_instance" "http_server" {

  count = length(data.aws_subnets.private_subnets.ids)
  #ami                   = "ami-062f7200baf2fa504"
  ami                    = data.aws_ami.aws_linux_2_latest.id
  key_name               = "default-ec2"
  instance_type          = "t2.micro"
  vpc_security_group_ids = data.aws_security_groups.sg.ids

  //subnet_id              = "subnet-3f7b2563"
  subnet_id = element(data.aws_subnets.private_subnets.ids,count.index)

  connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ec2-user"
    private_key = file(var.aws_key_pair)
  }

  tags = {
    Name = "Lab 3 - Private EC2 ${count.index + 1}"
  }

  # provisioner "remote-exec" {
  #   inline = [
  #     "sudo yum install httpd -y",
  #     "sudo service httpd start",
  #     "echo Welcome to in28minutes - Virtual Server is at ${self.public_dns} | sudo tee /var/www/html/index.html"
  #   ]
  # }
}

resource "aws_instance" "public_http_server" {

  count = length(data.aws_subnets.public_subnets.ids)
  #ami                   = "ami-062f7200baf2fa504"
  ami                    = data.aws_ami.aws_linux_2_latest.id
  key_name               = "default-ec2"
  instance_type          = "t2.micro"
  vpc_security_group_ids = data.aws_security_groups.sg.ids

  //subnet_id              = "subnet-3f7b2563"
  subnet_id = element(data.aws_subnets.public_subnets.ids,count.index)

  connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ec2-user"
    private_key = file(var.aws_key_pair)
  }

  tags = {
    Name = "Lab 3 - Public EC2 ${count.index + 1}"
  }

  # provisioner "remote-exec" {
  #   inline = [
  #     "sudo yum install httpd -y",
  #     "sudo service httpd start",
  #     "echo Welcome to in28minutes - Virtual Server is at ${self.public_dns} | sudo tee /var/www/html/index.html"
  #   ]
  # }
}

resource "aws_eip" "eip" {
  count = length(aws_instance.http_server)

  domain = "vpc"
  instance = aws_instance.http_server[count.index].id
}

resource "aws_eip_association" "eip_assoc" {

  count = length(aws_instance.http_server)

  instance_id   = aws_instance.http_server[count.index].id
  allocation_id = aws_eip.eip[count.index].id
}

resource "aws_eip" "public_eip" {
  count = length(aws_instance.public_http_server)

  domain = "vpc"
  instance = aws_instance.public_http_server[count.index].id
}

resource "aws_eip_association" "public_eip_assoc" {

  count = length(aws_instance.public_http_server)

  instance_id   = aws_instance.public_http_server[count.index].id
  allocation_id = aws_eip.public_eip[count.index].id
}

问题原因分析

  1. 私有子网路由逻辑错误:私有子网的默认路由(0.0.0.0/0)指向NAT网关,而NAT网关仅负责将私有子网的出站流量转换为自身公网IP,无法处理外部入站流量的反向路由。即使给私有EC2绑定了EIP,外部SSH请求到达实例后,响应流量会走NAT网关,源地址被替换为NAT网关的IP,本地机器收到的响应与请求目标IP不匹配,会直接丢弃,导致连接失败。

  2. 违背私有子网设计原则:私有子网的核心作用是隔离内部资源,不直接暴露到互联网。直接给私有EC2绑定EIP并尝试直连,不符合AWS网络架构的最佳实践。


解决方案

方案1:通过公有子网跳板机SSH跳转(推荐)

这是访问私有子网EC2的标准方式,无需修改私有子网路由,保持网络隔离性:

步骤1:确保安全组配置正确

当前安全组已经开放22端口给所有IP,若要更安全,可将SSH ingress限制为你本地机器的公网IP。

步骤2:使用SSH代理跳转

在本地机器执行以下命令,通过公有子网的EC2作为跳板机,连接私有子网EC2:

ssh -i "default-ec2.pem" -A ec2-user@<公有EC2公网IP>
# 登录跳板机后,再连接私有EC2:
ssh ec2-user@<私有EC2私有IP>

或者直接用一条命令完成跳转:

ssh -i "default-ec2.pem" -o ProxyCommand="ssh -W %h:%p -i default-ec2.pem ec2-user@<公有EC2公网IP>" ec2-user@<私有EC2私有IP>

方案2:修改私有子网路由实现直连(不推荐)

若一定要直连私有EC2,需调整私有子网的路由表,让绑定EIP的实例的出站流量走Internet Gateway而非NAT网关:

修正create-vpc-main.tf中的私有子网路由表

添加一条针对EC2私有IP的路由,指向Internet Gateway,替换原有默认路由的逻辑:

resource "aws_route_table" "third_rt" {
  vpc_id = aws_vpc.main.id

  # 给绑定EIP的私有EC2单独配置路由,出站走IGW
  route {
    cidr_block = "<私有EC2的私有IP>/32"
    gateway_id = aws_internet_gateway.gw.id
  }

  # 其余私有子网流量仍走NAT网关
  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gateway.id
  }
}

注意:这种方式会打破私有子网的隔离性,仅适合测试场景,生产环境禁止使用。


内容的提问来源于stack exchange,提问作者Norman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 00:42:04