VPC 安全控制

深入学习 VPC 的两大安全机制:安全组(Security Groups)和网络 ACL(Network Access Control Lists)。

安全组(Security Groups)

安全组基础

什么是安全组:

安全组是虚拟防火墙:
├─ 控制实例级别的流量
├─ 有状态防火墙(Stateful)
├─ 只支持允许规则(无拒绝规则)
├─ 默认拒绝所有入站流量
├─ 默认允许所有出站流量
└─ 可以引用其他安全组

工作层级:
├─ 附加到 ENI(弹性网络接口)
├─ 一个实例可以有多个安全组(最多 5 个)
└─ 一个安全组可以附加到多个实例

有状态防火墙特性:

有状态(Stateful)含义:
├─ 允许入站流量会自动允许响应出站
├─ 允许出站流量会自动允许响应入站
├─ 无需为响应流量创建规则
└─ 简化配置

示例:
入站规则:允许 80 端口
效果:
├─ 外部可以访问实例的 80 端口
└─ 实例的响应自动允许返回(无需出站规则)

出站规则:允许 443 端口
效果:
├─ 实例可以发起到外部 443 端口的连接
└─ 外部的响应自动允许进入(无需入站规则)

安全组规则

规则组成:

每条规则包含:
├─ 类型(Type):协议类型
├─ 协议(Protocol):TCP、UDP、ICMP 等
├─ 端口范围(Port Range):单个或范围
├─ 源/目标(Source/Destination):
│   ├─ CIDR 块(IP 地址范围)
│   ├─ 安全组 ID
│   ├─ 前缀列表 ID
│   └─ IPv4 或 IPv6
└─ 描述(Description):规则说明

示例规则:
类型:HTTP
协议:TCP
端口:80
源:0.0.0.0/0
描述:Allow HTTP from anywhere

常见规则配置:

# 创建 Web 服务器安全组
WEB_SG=$(aws ec2 create-security-group \
  --group-name web-servers-sg \
  --description "Security group for web servers" \
  --vpc-id $VPC_ID \
  --query 'GroupId' \
  --output text)

# 允许 HTTP 流量
aws ec2 authorize-security-group-ingress \
  --group-id $WEB_SG \
  --protocol tcp \
  --port 80 \
  --cidr 0.0.0.0/0 \
  --group-rule-description "Allow HTTP from internet"

# 允许 HTTPS 流量
aws ec2 authorize-security-group-ingress \
  --group-id $WEB_SG \
  --protocol tcp \
  --port 443 \
  --cidr 0.0.0.0/0 \
  --group-rule-description "Allow HTTPS from internet"

# 允许 SSH(仅来自跳板机)
aws ec2 authorize-security-group-ingress \
  --group-id $WEB_SG \
  --protocol tcp \
  --port 22 \
  --source-group $BASTION_SG \
  --group-rule-description "Allow SSH from bastion"

引用安全组

安全组引用的优势:

使用安全组 ID 而不是 IP 地址:

优势:
├─ 动态更新:实例变化时无需修改规则
├─ 简化管理:不需要追踪 IP 地址
├─ 自动扩展:Auto Scaling 自动适配
└─ 清晰的层次关系

示例:
Web 层 → App 层 → 数据库层

三层架构示例:

# 1. Web 层安全组
WEB_SG=$(aws ec2 create-security-group \
  --group-name web-tier-sg \
  --description "Web tier security group" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# 允许来自 Internet 的 HTTP/HTTPS
aws ec2 authorize-security-group-ingress \
  --group-id $WEB_SG \
  --ip-permissions \
    IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges='[{CidrIp=0.0.0.0/0}]' \
    IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges='[{CidrIp=0.0.0.0/0}]'

# 2. 应用层安全组
APP_SG=$(aws ec2 create-security-group \
  --group-name app-tier-sg \
  --description "Application tier security group" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# 只允许来自 Web 层的流量(引用安全组)
aws ec2 authorize-security-group-ingress \
  --group-id $APP_SG \
  --protocol tcp \
  --port 8080 \
  --source-group $WEB_SG \
  --group-rule-description "Allow from web tier"

# 3. 数据库层安全组
DB_SG=$(aws ec2 create-security-group \
  --group-name db-tier-sg \
  --description "Database tier security group" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# 只允许来自应用层的数据库连接
aws ec2 authorize-security-group-ingress \
  --group-id $DB_SG \
  --protocol tcp \
  --port 3306 \
  --source-group $APP_SG \
  --group-rule-description "Allow MySQL from app tier"

架构图示:

Internet
    │ (HTTP/HTTPS)
    ▼
┌─────────────────┐
│   Web Tier SG   │
│ Allow: 80, 443  │
│ From: 0.0.0.0/0 │
└────────┬────────┘
         │ (8080)
         ▼
┌─────────────────┐
│   App Tier SG   │
│ Allow: 8080     │
│ From: WEB_SG    │
└────────┬────────┘
         │ (3306)
         ▼
┌─────────────────┐
│    DB Tier SG   │
│ Allow: 3306     │
│ From: APP_SG    │
└─────────────────┘

优势:
├─ 清晰的安全边界
├─ 最小权限原则
├─ 易于管理和审计
└─ 自动适应扩缩容

高级安全组场景

跳板机(Bastion Host)访问:

# 跳板机安全组
BASTION_SG=$(aws ec2 create-security-group \
  --group-name bastion-sg \
  --description "Bastion host security group" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# 只允许来自公司 IP 的 SSH
aws ec2 authorize-security-group-ingress \
  --group-id $BASTION_SG \
  --protocol tcp \
  --port 22 \
  --cidr 203.0.113.0/24 \
  --group-rule-description "Allow SSH from office"

# 私有实例安全组 - 允许来自 Bastion 的 SSH
aws ec2 authorize-security-group-ingress \
  --group-id $PRIVATE_SG \
  --protocol tcp \
  --port 22 \
  --source-group $BASTION_SG \
  --group-rule-description "Allow SSH from bastion"

负载均衡器配置:

# ALB 安全组
ALB_SG=$(aws ec2 create-security-group \
  --group-name alb-sg \
  --description "Application Load Balancer SG" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# 允许来自 Internet 的流量
aws ec2 authorize-security-group-ingress \
  --group-id $ALB_SG \
  --ip-permissions \
    IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges='[{CidrIp=0.0.0.0/0}]' \
    IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges='[{CidrIp=0.0.0.0/0}]'

# 后端实例安全组 - 只允许来自 ALB
aws ec2 authorize-security-group-ingress \
  --group-id $WEB_SG \
  --protocol tcp \
  --port 80 \
  --source-group $ALB_SG \
  --group-rule-description "Allow from ALB"

自引用规则(集群通信):

# 创建集群安全组
CLUSTER_SG=$(aws ec2 create-security-group \
  --group-name cluster-sg \
  --description "Cluster internal communication" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# 允许集群内所有实例互相通信
aws ec2 authorize-security-group-ingress \
  --group-id $CLUSTER_SG \
  --protocol -1 \
  --source-group $CLUSTER_SG \
  --group-rule-description "Allow all traffic within cluster"

# 解释:
# 集群内任何附加此安全组的实例都可以:
# ├─ 接收来自其他集群成员的所有流量
# └─ 发送流量到其他集群成员

安全组最佳实践

设计原则:

1. 最小权限原则
   ├─ 仅开放必需的端口
   ├─ 限制源 IP 范围
   ├─ 定期审查规则
   └─ 删除未使用的规则

2. 命名规范
   ├─ 使用描述性名称
   ├─ 包含环境信息(prod/dev)
   ├─ 包含用途(web/app/db)
   └─ 示例:prod-web-tier-sg

3. 规则描述
   ├─ 每条规则添加描述
   ├─ 说明规则的用途
   ├─ 便于审计和排查
   └─ 示例:"Allow HTTPS from CloudFront"

4. 使用标签
   ├─ Environment: Production
   ├─ Application: MyApp
   ├─ ManagedBy: Terraform
   └─ CostCenter: Engineering

常见错误避免:

❌ 不要做:
├─ 开放 0.0.0.0/0 到管理端口(SSH/RDP)
├─ 使用过于宽泛的规则(如 0-65535 端口)
├─ 忘记删除测试规则
├─ 不添加规则描述
└─ 混用不同用途的安全组

✅ 应该做:
├─ SSH/RDP 仅允许跳板机或 VPN
├─ 精确指定需要的端口
├─ 定期审查和清理
├─ 使用引用而非 IP 地址
└─ 按层次和用途划分安全组

网络 ACL (NACLs)

NACL 基础

什么是 NACL:

网络访问控制列表:
├─ 子网级别的防火墙
├─ 无状态防火墙(Stateless)
├─ 支持允许和拒绝规则
├─ 按规则编号顺序评估
├─ 默认 NACL 允许所有流量
└─ 自定义 NACL 默认拒绝所有流量

工作层级:
├─ 附加到子网
├─ 一个子网只能有一个 NACL
├─ 一个 NACL 可以附加到多个子网
└─ 先于安全组评估

无状态防火墙特性:

无状态(Stateless)含义:
├─ 入站和出站规则独立
├─ 必须显式配置双向规则
├─ 不追踪连接状态
└─ 更复杂但更灵活

示例:
允许 HTTP 访问需要:
├─ 入站规则:允许 TCP 80 从 0.0.0.0/0
└─ 出站规则:允许 TCP 1024-65535 到 0.0.0.0/0
   (临时端口用于响应)

对比安全组(有状态):
└─ 仅需入站规则:允许 TCP 80
   响应自动允许

NACL vs 安全组

详细对比:

┌──────────────────┬──────────────────┬──────────────────┐
│      特性        │    安全组        │      NACL        │
├──────────────────┼──────────────────┼──────────────────┤
│ 工作层级         │ 实例级别         │ 子网级别         │
│ 状态             │ 有状态           │ 无状态           │
│ 规则类型         │ 仅允许           │ 允许 + 拒绝      │
│ 规则评估         │ 所有规则         │ 按编号顺序       │
│ 应用范围         │ 指定实例         │ 子网内所有实例   │
│ 响应流量         │ 自动允许         │ 需要显式规则     │
│ 规则数量         │ 60 入站 + 60出站 │ 20 入站 + 20出站 │
│ 引用其他规则     │ 支持(安全组ID)│ 不支持           │
│ 默认行为         │ 拒绝所有入站     │ 自定义NACL拒绝全部│
└──────────────────┴──────────────────┴──────────────────┘

使用场景对比:

安全组:
├─ 实例级别的精细控制
├─ 应用层访问控制
├─ 引用其他安全组
└─ 大多数场景的首选

NACL:
├─ 子网级别的额外防护
├─ 阻止特定 IP 地址
├─ 合规要求的网络层控制
└─ 纵深防御的一部分

NACL 规则配置

创建自定义 NACL:

# 创建 NACL
NACL_ID=$(aws ec2 create-network-acl \
  --vpc-id $VPC_ID \
  --tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=Web-Subnet-NACL}]' \
  --query 'NetworkAcl.NetworkAclId' \
  --output text)

# 入站规则 100:允许 HTTP
aws ec2 create-network-acl-entry \
  --network-acl-id $NACL_ID \
  --rule-number 100 \
  --protocol tcp \
  --port-range From=80,To=80 \
  --cidr-block 0.0.0.0/0 \
  --egress false \
  --rule-action allow

# 入站规则 110:允许 HTTPS
aws ec2 create-network-acl-entry \
  --network-acl-id $NACL_ID \
  --rule-number 110 \
  --protocol tcp \
  --port-range From=443,To=443 \
  --cidr-block 0.0.0.0/0 \
  --egress false \
  --rule-action allow

# 入站规则 120:允许 SSH(仅来自办公室)
aws ec2 create-network-acl-entry \
  --network-acl-id $NACL_ID \
  --rule-number 120 \
  --protocol tcp \
  --port-range From=22,To=22 \
  --cidr-block 203.0.113.0/24 \
  --egress false \
  --rule-action allow

# 入站规则 130:允许临时端口(用于响应)
aws ec2 create-network-acl-entry \
  --network-acl-id $NACL_ID \
  --rule-number 130 \
  --protocol tcp \
  --port-range From=1024,To=65535 \
  --cidr-block 0.0.0.0/0 \
  --egress false \
  --rule-action allow

# 出站规则 100:允许 HTTP
aws ec2 create-network-acl-entry \
  --network-acl-id $NACL_ID \
  --rule-number 100 \
  --protocol tcp \
  --port-range From=80,To=80 \
  --cidr-block 0.0.0.0/0 \
  --egress true \
  --rule-action allow

# 出站规则 110:允许 HTTPS
aws ec2 create-network-acl-entry \
  --network-acl-id $NACL_ID \
  --rule-number 110 \
  --protocol tcp \
  --port-range From=443,To=443 \
  --cidr-block 0.0.0.0/0 \
  --egress true \
  --rule-action allow

# 出站规则 120:允许临时端口(用于响应)
aws ec2 create-network-acl-entry \
  --network-acl-id $NACL_ID \
  --rule-number 120 \
  --protocol tcp \
  --port-range From=1024,To=65535 \
  --cidr-block 0.0.0.0/0 \
  --egress true \
  --rule-action allow

# 关联到子网
aws ec2 replace-network-acl-association \
  --association-id $ASSOC_ID \
  --network-acl-id $NACL_ID

规则编号策略:

规则编号建议:
├─ 使用 100 的倍数(100, 200, 300...)
├─ 预留空间插入新规则
├─ 低编号 = 高优先级
└─ 最后规则:* (隐式拒绝)

示例:
100 - 允许 HTTP
110 - 允许 HTTPS
200 - 允许 SSH
300 - 拒绝特定 IP
...
32766 - 最后一条自定义规则
32767 - 默认拒绝(AWS 自动添加)

NACL 实战场景

场景 1:阻止恶意 IP:

# 在最前面添加拒绝规则
aws ec2 create-network-acl-entry \
  --network-acl-id $NACL_ID \
  --rule-number 10 \
  --protocol -1 \
  --cidr-block 198.51.100.0/24 \
  --egress false \
  --rule-action deny \
  --group-rule-description "Block malicious IP range"

# 规则 10 会在规则 100 之前评估
# 因此来自 198.51.100.0/24 的流量会被拒绝
# 即使后续有允许规则

场景 2:私有子网 NACL:

# 创建私有子网 NACL
PRIVATE_NACL=$(aws ec2 create-network-acl \
  --vpc-id $VPC_ID \
  --tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=Private-Subnet-NACL}]' \
  --query 'NetworkAcl.NetworkAclId' \
  --output text)

# 入站:允许来自 VPC 内部的流量
aws ec2 create-network-acl-entry \
  --network-acl-id $PRIVATE_NACL \
  --rule-number 100 \
  --protocol -1 \
  --cidr-block 10.0.0.0/16 \
  --egress false \
  --rule-action allow

# 入站:允许临时端口(用于 NAT 响应)
aws ec2 create-network-acl-entry \
  --network-acl-id $PRIVATE_NACL \
  --rule-number 110 \
  --protocol tcp \
  --port-range From=1024,To=65535 \
  --cidr-block 0.0.0.0/0 \
  --egress false \
  --rule-action allow

# 出站:允许所有流量
aws ec2 create-network-acl-entry \
  --network-acl-id $PRIVATE_NACL \
  --rule-number 100 \
  --protocol -1 \
  --cidr-block 0.0.0.0/0 \
  --egress true \
  --rule-action allow

纵深防御架构

多层安全架构:

Internet
    │
    ▼
┌────────────────────────────────┐
│     Internet Gateway           │
└───────────┬────────────────────┘
            │
    ┌───────▼────────┐
    │   NACL (公有)  │ ◄─── 第 1 层:子网级防护
    │ ├─ 允许 80    │
    │ ├─ 允许 443   │
    │ └─ 拒绝特定IP │
    └───────┬────────┘
            │
    ┌───────▼────────┐
    │  Security Group│ ◄─── 第 2 层:实例级防护
    │ ├─ 允许 80    │
    │ └─ 允许 443   │
    └───────┬────────┘
            │
    ┌───────▼────────┐
    │   EC2 Instance │ ◄─── 第 3 层:主机防火墙
    │ (iptables/nft)│
    └────────────────┘

优势:
├─ 多层防护降低风险
├─ 不同层级的控制粒度
├─ 符合安全最佳实践
└─ 满足合规要求

VPC Flow Logs

Flow Logs 配置

什么是 Flow Logs:

VPC Flow Logs 记录:
├─ IP 流量信息
├─ 可应用于 VPC、子网或 ENI
├─ 发送到 CloudWatch Logs 或 S3
├─ 用于安全审计和故障排查
└─ 不影响网络性能

启用 Flow Logs:

# 1. 创建 CloudWatch 日志组
aws logs create-log-group \
  --log-group-name /aws/vpc/flowlogs

# 2. 创建 IAM 角色
cat > trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "vpc-flow-logs.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}
EOF

ROLE_ARN=$(aws iam create-role \
  --role-name VPCFlowLogsRole \
  --assume-role-policy-document file://trust-policy.json \
  --query 'Role.Arn' \
  --output text)

# 3. 附加策略
cat > flow-logs-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
      "logs:DescribeLogGroups",
      "logs:DescribeLogStreams"
    ],
    "Resource": "*"
  }]
}
EOF

aws iam put-role-policy \
  --role-name VPCFlowLogsRole \
  --policy-name FlowLogsPolicy \
  --policy-document file://flow-logs-policy.json

# 4. 创建 Flow Logs
aws ec2 create-flow-logs \
  --resource-type VPC \
  --resource-ids $VPC_ID \
  --traffic-type ALL \
  --log-destination-type cloud-watch-logs \
  --log-group-name /aws/vpc/flowlogs \
  --deliver-logs-permission-arn $ROLE_ARN \
  --tag-specifications 'ResourceType=vpc-flow-log,Tags=[{Key=Name,Value=VPC-Flow-Logs}]'

Flow Logs 字段:

日志格式:
version account-id interface-id srcaddr dstaddr srcport dstport protocol packets bytes start end action log-status

示例日志:
2 123456789012 eni-abc123 172.31.16.139 172.31.16.21 20641 22 6 20 4249 1418530010 1418530070 ACCEPT OK

字段说明:
├─ srcaddr: 源 IP 地址
├─ dstaddr: 目标 IP 地址
├─ srcport: 源端口
├─ dstport: 目标端口
├─ protocol: IANA 协议号(6=TCP, 17=UDP, 1=ICMP)
├─ packets: 数据包数量
├─ bytes: 字节数
├─ action: ACCEPT 或 REJECT
└─ log-status: OK, NODATA, SKIPDATA

Flow Logs 分析

使用 CloudWatch Insights 查询:

# 查找被拒绝的连接
fields @timestamp, srcaddr, dstaddr, srcport, dstport, action
| filter action = "REJECT"
| sort @timestamp desc
| limit 20

# 统计每个源 IP 的流量
fields srcaddr, sum(bytes) as total_bytes
| stats count(*) as connection_count, sum(total_bytes) as total_traffic by srcaddr
| sort total_traffic desc
| limit 10

# 查找异常大量连接的 IP
fields srcaddr, dstaddr
| stats count(*) as connection_count by srcaddr
| filter connection_count > 1000
| sort connection_count desc

安全威胁检测:

常见安全模式:

1. 端口扫描检测:
   ├─ 同一源 IP 访问大量不同端口
   ├─ 大量 REJECT 记录
   └─ 短时间内大量连接尝试

2. DDoS 攻击检测:
   ├─ 单一目标的大量连接
   ├─ 来自多个源的相似流量模式
   └─ 异常高的数据包速率

3. 数据渗出检测:
   ├─ 异常大的出站流量
   ├─ 非工作时间的大量数据传输
   └─ 到未知目标的连接

安全最佳实践总结

设计原则:

1. 纵深防御
   ├─ NACL + 安全组组合使用
   ├─ WAF 保护 Web 应用
   ├─ 主机防火墙作为最后一层
   └─ 定期安全审计

2. 最小权限
   ├─ 仅开放必需的端口
   ├─ 限制源 IP 范围
   ├─ 使用安全组引用而非 0.0.0.0/0
   └─ 定期审查和清理规则

3. 监控和审计
   ├─ 启用 VPC Flow Logs
   ├─ 设置 CloudWatch 告警
   ├─ 定期分析安全日志
   └─ 使用 GuardDuty 威胁检测

4. 网络隔离
   ├─ 公私子网分离
   ├─ 多层架构
   ├─ 使用 PrivateLink 访问 AWS 服务
   └─ 避免将数据库暴露到公网

检查清单:

✅ 安全组配置:
   ├─ 无 0.0.0.0/0 到管理端口
   ├─ 使用描述性规则名称
   ├─ 定期审查未使用的规则
   └─ 使用引用而非硬编码 IP

✅ NACL 配置:
   ├─ 阻止已知恶意 IP
   ├─ 正确配置临时端口
   ├─ 规则编号有序且有间隔
   └─ 文档化规则用途

✅ 监控配置:
   ├─ 启用 VPC Flow Logs
   ├─ 配置 GuardDuty
   ├─ 设置异常流量告警
   └─ 定期审查安全事件

✅ 访问控制:
   ├─ 跳板机用于管理访问
   ├─ VPN 用于远程访问
   ├─ SSM Session Manager 替代 SSH
   └─ 定期轮换凭证

正确配置安全组和 NACL 是保护 VPC 资源的关键!