在很多企业的技术实现中,由于数据安全和合规性要求,大部分的应用服务都部署在私有云环境或专用网络中。为了满足开发人员和运维团队从本地数据中心安全访问云上资源的需求,采用堡垒机作为一种有效的解决方案变得尤为重要。
堡垒机的核心实现原理基于SSH(Secure Shell)协议,这是一种业界广泛认可的加密通信协议。SSH不仅为数据传输提供了加密保护,还确保了身份验证的安全性,从而构建了一个可靠的远程访问通道。
然而,传统的自建堡垒机在其管理和运维方面面临着多种挑战:
与传统方式不同,基于 Amazon Systems Manager Session Manager 的堡垒机通过建立双向加密通道,连接客户端和远程节点,并采用 TLS 1.2 进行流量加密和 Sigv4 签名请求。这一方式具备显著优势:
基于 Session Manager 的堡垒机为访问管理提供了高效、安全、高可用的解决方案,成为提升操作效率和保障安全性的理想选择。堡垒机作为一种安全措施,允许用户通过中间主机来访问位于私有子网中的实例或资源。它作为一座桥梁,帮助用户在安全的环境下连接到内部资源,同时减少对直接连接的需求,从而降低被攻击的风险。对于企业的安全管理员来说,堡垒机可以增强安全性、简化网络配置、降低网络资源暴露风险、提供审计跟踪,对于开发运维人员来说,他们可以在本地环境中使用熟悉的开发、运维工具,如数据库客户端、浏览器、RDP 等去访问云上私有资源,大大提高生产效率。
本文主要阐述的是基于 Amazon Systems Manager 的 Session Manager 特性和 Amazon EC2 堡垒机的设计和实现。主要内容包括堡垒机的架构设计、安全设计、日志监控设计、高可用弹性设计以及自动化部署设计等,同时针对堡垒机在不同场景中的使用方式和脚本进行举例说明。
在进行云上堡垒机设计时,安全是第一个需要考虑的要求。Session Manager 是一种无需通过公网 IP 或 SSH 密钥来连接到 EC2 实例的服务,它通过 Systems Manager 的控制台或 CLI 来创建和管理安全的交互式会话。其安全设计主要考虑以下四点:
堡垒机的高可用设计可以确保在发生故障或负载增加时,堡垒机仍然能够保持可用,并继续提供安全的访问控制。
Amazon CloudWatch 指标监控: CloudWatch 可以监控 EC2 实例和其他亚马逊云资源的性能指标。可监控实例的 CPU 使用率、内存利用率、磁盘等关键指标。并通过 Amazon SNS 服务设置报警,以便在性能异常时及时收到通知。
考虑到堡垒机的高可用设计,在其中一台机器宕机的情况下,可以自动、快速的拉起一台新的堡垒机以供使用,我们采取 EC2 launch template 的方式进行堡垒机的构建,以下是 IaC 样例代码:
## 创建Bastion的launch template
resource "aws_launch_template" "bastion_template" {
name = "bastion_template"
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = 8
encrypted = true
}
}
iam_instance_profile {
name = aws_iam_instance_profile.bastion_ec2_profile.id
}
image_id = data.aws_ami.linux_2023_image.id
instance_initiated_shutdown_behavior = "terminate"
instance_type = var.bastion_instance_type
monitoring {
enabled = true
}
network_interfaces {
associate_public_ip_address = false
device_index = 0
security_groups = [aws_security_group.bastion.id]
subnet_id = element(module.vpc.private_subnets, 0)
delete_on_termination = true
}
user_data = base64encode(
templatefile(
"${path.module}/templates/user_data.sh.tftpl",
{
##传入值到user data中所需要的变量中
}
)
)
}
## 创建Bastion的弹性伸缩组
resource "aws_autoscaling_group" "bastion_asg" {
launch_template {
id = aws_launch_template.bastion_template.id
version = "$Latest"
}
availability_zones = ["${var.region}a","${var.region}b"]
name = "bastion_daily"
max_size = 1
min_size = 1
health_check_grace_period = 300
health_check_type = "EC2"
force_delete = true
termination_policies = ["OldestInstance"]
tag {
key = "Name"
value = "bastion"
propagate_at_launch = true
}
}
## 创建Bastion的弹性伸缩规则-样例中使用定时规则
resource "aws_autoscaling_schedule" "bastion_scale_down" {
scheduled_action_name = "bastion_scale_down"
min_size = 0
max_size = 0
desired_capacity = 0
recurrence = "0 20 * * *" # 20:00 UTC +8 时间
time_zone = "Asia/Chongqing"
autoscaling_group_name = aws_autoscaling_group.bastion_asg.name
}
resource "aws_autoscaling_schedule" "bastion_scale_up" {
scheduled_action_name = "bastion_scale_up"
min_size = 1
max_size = 1
desired_capacity = 1
recurrence = "0 8 * * *" # 8:00 UTC +8 时间
time_zone = "Asia/Chongqing"
autoscaling_group_name = aws_autoscaling_group.bastion_asg.name
}
## 或者基于CPU利用率的弹性伸缩规则(样例)
resource "aws_autoscaling_group" "bation_cpu" {
name = "bastion-cpu-asg"
launch_configuration = aws_launch_configuration.bastion_template.name
min_size = 2
max_size = 4
desired_capacity = 2
metric_trigger {
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
statistic = "Average"
unit = "Percent"
threshold = 70 # CPU利用率超过70%时扩容
comparison_operator = "GreaterThanOrEqualToThreshold"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.bastion_cpu.name
}
}
scaling_policy {
name = "scale-up-policy"
adjustment_type = "ChangeInCapacity"
scaling_adjustment = 1
cooldown = 300 # 冷却时间300s
}
在创建堡垒机时,不仅仅是单独的创建一台 EC2 的实例,还需要针对用于该 EC2 的实例进行安全加固、安装相应的工具、设置对于堡垒机的监控等功能,以下是 IaC 代码中,用于 EC2 实例初始化的用户数据(User Data)样例。
## 安全加固
yum -y update --security
## 仅允许ec2-user访问该目录
mkdir /var/log/bastion
chown ec2-user:ec2-user /var/log/bastion
chmod -R 770 /var/log/bastion
setfacl -Rdm other:0 /var/log/bastion
## Make OpenSSH execute a custom script on logins
echo -e "\\nForceCommand /usr/bin/bastion/shell" >> /etc/ssh/sshd_config
echo "AllowTcpForwarding yes" >> /etc/ssh/sshd_config
awk '!/X11Forwarding/' /etc/ssh/sshd_config > temp && mv temp /etc/ssh/sshd_config
echo "X11Forwarding no" >> /etc/ssh/sshd_config
sed -i "s/002/022/g" /etc/profile
umask 022
## SSH命令记录脚本
mkdir /usr/bin/bastion
cat > /usr/bin/bastion/shell << 'EOF'
# The format of log files is /var/log/bastion/YYYY-MM-DD_HH-MM-SS_user
LOG_FILE="`date --date="today" "+%Y-%m-%d_%H-%M-%S"`_`whoami`"
LOG_DIR="/var/log/bastion/"
echo ""
echo "NOTE: This SSH session will be recorded"
echo "AUDIT KEY: $LOG_FILE"
echo ""
SUFFIX=`mktemp -u _XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`
script -qf --timing=$LOG_DIR$LOG_FILE$SUFFIX.time $LOG_DIR$LOG_FILE$SUFFIX.data --command=/bin/bash
EOF
chmod a+x /usr/bin/bastion/shell
chown root:ec2-user /usr/bin/script
chmod g+s /usr/bin/script
##关闭根用户登录
systemctl stop sshd
echo "PermitRootLogin no" >> /etc/ssh/sshd_config
echo "PermitEmptyPasswords no" >> /etc/ssh/sshd_config
systemctl start sshd
## 安装CloudWatch Agent,进行系统日志收集及性能指标收集
sudo yum -y install amazon-cloudwatch-agent
sudo yum install -y collectd
cat > /opt/aws/amazon-cloudwatch-agent/etc/config.json << _EOF_
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "root"
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/messages",
"log_group_name": "/aws/ec2/bastion/messages",
"log_stream_name": "{instance_id}",
"retention_in_days": 30
},
{
"file_path": "/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log",
"log_group_name": "/aws/ec2/bastion/amazon-cloudwatch-agent",
"log_stream_name": "{instance_id}",
"retention_in_days": 30
}
]
}
}
},
"metrics": {
"aggregation_dimensions": [
[
"InstanceId"
]
],
"append_dimensions": {
"AutoScalingGroupName": "$${aws:AutoScalingGroupName}",
"InstanceId": "$${aws:InstanceId}"
},
"metrics_collected": {
"collectd": {
"metrics_aggregation_interval": 60
},
"disk": {
"measurement": [
{
"name":"used_percent",
"rename":"disk_used_percent"
}
],
"metrics_collection_interval": 60,
"resources": [
"*"
]
},
"mem": {
"measurement": [
{
"name":"used_percent",
"rename":"mem_used_percent"
}
],
"metrics_collection_interval": 60
},
"statsd": {
"metrics_aggregation_interval": 60,
"metrics_collection_interval": 10,
"service_address": ":8125"
}
}
}
}
_EOF_
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/config.json -s
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a start
根据 Session Manager 安全组的最佳实践,本方案中堡垒机的安全组设计如下:
入站规则:
源 | 协议 | 端口 | 备注 |
---|---|---|---|
NA | NA | NA | 无入站规则 |
出站规则:
目标 | 协议 | 端口 | 备注 |
---|---|---|---|
VPC CIDR | TCP | 0-65535 | 允许访问 VPC 内部资源 |
VPC endpoints Security group ID | HTTPS | 443 | 允许访问 VPC Interface endpoints |
VPC endpoints S3 prefix list | HTTPS | 443 | 允许访问 S3 Gateway endpoint |
IaC 代码设计样例如下:
# 创建堡垒机安全组并配置规则
resource "aws_security_group" "bastion" {
description = "Ingress should be only from Systems Session Manager"
name = "bastion-sg"
vpc_id = module.vpc.vpc_id
tags = merge(
{
Name = "bastion-sg"
}
)
}
resource "aws_security_group_rule" "basion_egress_1" {
security_group_id = aws_security_group.bastion.id
description = "bastion_to_local_VPC_CIDRs"
type = "egress"
from_port = "0"
to_port = "65535"
protocol = "TCP"
cidr_blocks = [local.vpc_cidr]
}
resource "aws_security_group_rule" "basion_egress_2" {
security_group_id = aws_security_group.bastion.id
description = "bastion_egress_to_inteface_endpoints"
type = "egress"
from_port = "443"
to_port = "443"
protocol = "TCP"
source_security_group_id = aws_security_group.vpc_endpoints.id
}
resource "aws_security_group_rule" "bastion_linux_3" {
security_group_id = aws_security_group.bastion.id
description = "bastion_linux_egress_to_s3_endpoint"
type = "egress"
from_port = "443"
to_port = "443"
protocol = "TCP"
prefix_list_ids = [data.aws_vpc_endpoint.s3.prefix_list_id]
}
在启用 Session Manager 时,需要对 Session Manager 进行相关的配置,如是否开启 Session 加密,是否将日志存储到 CloudWatch Logs 或者 S3 上,是否对日志进行加密,最大的 Session 时长等。我们通过创建 SSM Documents 的方式来进行配置,如以下 IaC 样例代码所示:
## ssm run shell json
{
"schemaVersion": "1.0",
"description" : "Document for SSM log configuration",
"sessionType" : "Standard_Stream",
"inputs": {
"s3BucketName" : "${bucket_name}",
"s3KeyPrefix" : "ssm",
"s3EncryptionEnabled" : true,
"cloudWatchLogGroupName" : "${log_group_name}",
"cloudWatchEncryptionEnabled" : true,
"cloudWatchStreamingEnabled": true,
"kmsKeyId" : "${kms_id}",
"runAsEnabled": true,
"idleSessionTimeout": "20",
"maxSessionDuration":"60",
}
}
## IaC code 创建SessionManagerRunShell documents
resource "aws_ssm_document" "ssm_shell" {
name = "SSM-SessionManagerRunShell"
document_format = "JSON"
document_type = "Session"
content = templatefile("${path.module}/templates/ssm_runshell.json", {
bucket_name = join("-", [local.bucket_name, data.aws_caller_identity.current.account_id])
log_group_name = "/aws/sessionmanager"
kms_id = aws_kms_key.this.arn
}
)
}
在本设计中,堡垒机的 IAM 角色主要包含以下权限:
如需自定义更多 Session Manager IAM 角色的详细可访问:自定义 Session Manager IAM 角色
以下是 IaC 代码样例:
## 关联托管的IAM Policy
resource "aws_iam_role_policy_attachment" "bastion_managed" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.bastion_role.name
}
resource "aws_iam_role_policy_attachment" "bastion_managed_cloudwatch" {
policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
role = aws_iam_role.bastion_role.name
}
## 创建IAM 角色及关联的Policy Documents
data "aws_iam_policy_document" "bastion_assume_policy_document" {
statement {
actions = [
"sts:AssumeRole"
]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "bastion_role" {
name = "bastion_ec2_role"
path = "/"
assume_role_policy = data.aws_iam_policy_document.bastion_assume_policy_document.json
tags = merge(
{
Name = "bastion_linux_ec2_role"
},
)
}
本章将介绍如何通过基础设施即代码(Infrastructure as code)的方式一键式构建云上堡垒机,通过 Terraform 进行相应的 IaC 代码开发。
根据云上堡垒机的设计,我们在堡垒机的部署中,将使用到亚马逊云上的多种服务:如Amazon CloudWatch、Key Management Service(KMS) 、 Identity and Access Management(IAM)、Systems Manager、Amazon Simply Notification Service(SNS)、Amazon Simple Storage Service(S3) 、Amazon PrivateLink、Amazon EC2 以及Amazon EC2 Auto Scaling等,以下为部署架构图:
以下对部署中所使用到的主要亚马逊云相关服务的用途进行简单介绍:
需要创建的 Endpoints 列表:
序号 | 名称 | 类型 | 用途 |
---|---|---|---|
1 | com.amazonaws..ssm | Interface | 与 Session Manager 进行安全交互 |
2 | com.amazonaws..ssmmessages | Interface | 用于 SSM Agent 到 Session Manager 的 API 操作 |
3 | com.amazonaws..ec2messages | Interface | 用于 SSM Agent 到 Session Manager 的 API 操作 |
4 | com.amazonaws..logs | Interface | 与 CloudWatch 服务进行安全交互 |
5 | com.amazonaws..monitoring | Interface | 与监控告警服务进行安全交互 |
6 | com.amazonaws..kms | Interface | 与 KSM 服务进行安全交互 |
7 | com.amazonaws..s3 | Gateway | 与 S3 服务进行安全交互 |
8 | eice-*.ec2-instance-connect-endpoint..amazonaws.com | EC2 Instance Connect Endpoint | 发送 Public Key 到远端 EC2(中国区不支持该功能) |
当 IaC 代码开发完成后,可以将该代码集成在其他资源的 CICD pipeline 中,也可以通过 Terraform 命令,直接进行部署。
# Terraform plan 检查要部署的Bastion资源
$ terraform plan -var-file=common.tfvars ## common.tfvars包含堡垒机所需的相关参数
# Terraform apply 执行部署
$ terraform apply -var-file=common.tfvars
如果要在本地机器上使用堡垒机,需要满足以下条件:
假设客户在亚马逊云上的私有子网中,部署了一个 Web 应用,包含 Web 前端和服务后端以及 RDS 数据库(以 SQL Server 为例)。该客户对于安全性的要求非常高,云上 VPC 环境中没有 Internet 访问,也没有在本地网络与云上环境之间搭建专线直连(Direct Connect)和 VPN。但客户的开发人员和运维人员,要求提供一个安全、高效、可靠的方式从本地的客户端、应用程序、浏览器去访问云上资源,以提高开发、运维效率。下图为堡垒机实操环境的部署示意图:
在这个场景中,开发人员或者运维人员通过公司 AD 集成的账号拿到了相应的角色权限,直接登录到亚马逊云的管理控制台,然后通过 Session Manager 直接连接到堡垒机上进行操作。操作步骤如下:
## 通过CLI获取Bastion的Instance ID
$ InstanceID=$(aws ec2 describe-instances --no-cli-pager \
--filters "Name=tag:Name,Values=bastion_linux" \
--query 'Reservations[0].Instances[0].InstanceId' --output text)
## 启动本地会话
$ aws ssm start-session --target $InstanceID
通过控制台或者本地 CLI 进入到堡垒机后,所有的操作都会被记录到 Amazon CloudWatch Log Group “/aws/sessionmanager”,日志保留时间为 3 个月,并且还会上传到 S3 桶(S3 桶名:bastion-audit-log-bucket-,路径为:ssm/)中做持久化存储。
在该场景中,开发人员或者运维人员通过公司 AD 集成的账号拿到了相应的角色权限,然后在本地启动 Session Manager 插件 来进行远程主机的端口转发,通过这种方式,开发人员或运维人员可以方便的使用本地客户端进行操作云上资源,如访问云上数据库和访问 WebServer 等。以下命令是启动如何在本地机器上实现远程主机端口转发:
## 通过CLI获取Bastion的Instance ID
$ InstanceID=$(aws ec2 describe-instances --no-cli-pager \
--filters "Name=tag:Name,Values=bastion_linux" \
--query 'Reservations[0].Instances[0].InstanceId' --output text)
## 启动远程主机端口转发,请替换掉命令中的<>为实际的远程端口、本地端口以及远程主机IP或者DNS
$ aws ssm start-session --target $InstanceID \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"portNumber":[""],"localPortNumber":[""],"host":[""]}'
(1) 远端数据库端口转发
以在本地访问云上 SQLServer 数据库为例子,端口号为 1433,本地端口号为 12345,远程主机 DNS 为:“bastion-test-db.xxxxx..rds.amazonaws.com”,启动后,如下图所示:
打开本地 SQLServer 客户端,输入 RDS Server 端地址和端口:“127.0.0.1,12345”,并输入登录数据库的账号和密码,如下图所示:
点击“Connect”,即可访问云上 RDS 数据库并进行操作:
(2) 远端服务器端口转发
以本地通过浏览器访问 Web Server 为例,端口号为 80,本地端口号为 8080,远程主机 IP 为:“10.99.3.132”,启动后,如下图所示:
通过本地浏览器访问 127.0.0.1:8080,可看到能够成功访问到位于私有网络中的 WebServer:
在开发人员或者运维人员在实际工作中,偶尔会需要通过 SSH 连接到堡垒机或者远端的主机上进行调试或者运维,并上传相应的文件到堡垒机上。一般情况下,运维人员需要拿到 EC2 的密钥并确保安全组中包含有 22 端口及客户端的 IP 地址的入站规则,然后才能连接到云上服务器上,这种情况不仅增加了密钥管理的难度,同时对于只存在私有网络中的服务器且本地数据中心和云上环境没有互联的场景是一个很大的挑战。通过 ec2-instance-connect 服务特性并结合 Session Manager 可以方便的满足客户通过 SSH 访问云上 EC2 的情景。
## Step 1: 通过 CLI 获取 Bastion 的 Instance ID
$ InstanceID=$(aws ec2 describe-instances --no-cli-pager \
--filters "Name=tag:Name,Values=bastion_linux" \
--query 'Reservations[0].Instances[0].InstanceId' --output text)
## Step 2: 生成临时 ssh key
$ echo -ne "Generating SSH key pair................\r"
$ echo -e 'y\n' | ssh-keygen -t rsa -f temp-key -N '' > /dev/null 2>&1
## Step 3: 发送生成 SSH public key 到堡垒机中,根据实际情况替换掉命令行中的变量
$ echo -ne "Pushing public key to instance.........\r"
$ aws ec2-instance-connect send-ssh-public-key --region $AWS_DEFAULT_REGION \
--instance-id $instanceId --availability-zone <az> \
--instance-os-user <ssm_user> \
--ssh-public-key file://temp-key.pub > /dev/null 2>&1
## Step 4: 修改 ~/.ssh/config 配置(仅需要一次配置)
Host test-ssh-bastion
IdentityFile ~/test/temp-key
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
LogLevel QUIET
User ec2-user
ProxyCommand sh -c "aws ssm start-session --target $(aws ec2 describe-instances --no-cli-pager \
--filters 'Name=tag:Name,Values=bastion_linux' \
--query 'Reservations[0].Instances[0].InstanceId' --output text) \
--document-name AWS-StartSSHSession --parameters 'portNumber=%p' --region us-east-1"
## Step 5: 修改完成后,通过 ssh 就可以连接到堡垒机中,如下图所示
$ ssh test-ssh-bastion
将以上代码中的 Step1 到 Step3 编写成一个脚本,在每次执行 ssh 之前,先运行该脚本,确保新生成的 SSH Key 有效(通过 send-ssh-public-key 上传到 EC2 上的 Key 有效期只有 60s)。
注意: 如果使用动态生成 SSH Key 的方式,需要先创建“EC2 Instance Connect Endpoint”,同时确保本地电脑上使用的 IAM 角色包含有“ec2-instance-connect:SendSSHPublicKey”的权限。另外,亚马逊云科技中国区暂时不支持该 Endpoint 类型的创建。
通过本地 Session Manager 插件 SSH 进入到堡垒机后,所有的操作都会上传到 S3 桶(S3 桶名:bastion-audit-log-bucket-,路径为:logs/)中做持久化存储。
除通过 SSH 登录到堡垒机外,SSM 还可以支持 SSH Socks Proxy,如下图在浏览器中设置 Proxy(以 Firefox 为例):
使用以下代码启动 SSH Socks Proxy:
ssh -f -N -p 2200 -i temp -o "IdentitiesOnly=yes" -D 1080 ec2-user@localhost
启动后,就可以通过 Socks Proxy 访问内网的 WebServer。
本方案中堡垒机是根据最常用的功能,如端口转发、SSH 访问来设计的,但在一些特定的场景中,还需要设计更多复杂的堡垒机方案,如:Windows 类型的堡垒机支持、如何使用 IAM 角色对堡垒机权限进行更精细化的管理等,这些都是未来本方案演进和增强的一个方向。目前我们方案中,创建的堡垒机类型只有一种,任何拥有启动 Session Manager 的 IAM 用户/角色都可以去访问它并进行操作,但在一些场景下,客户需要对于不同的角色使用不同的堡垒机:如角色 A 仅可以访问堡垒机 A,并通过该堡垒机上赋予的 IAM 权限进行相应的操作;角色 B 仅可以访问堡垒机 B,通过该堡垒机上赋予的 IAM 权限进行相应的操作。不同的用户角色和客户自有的 AD 域控进行集成,可以灵活的进行更精细化的权限控制。
本文主要介绍了亚马逊云科技基于 Amazon Systems Manager Session Manager 的堡垒机的设计和实现,并通过 IaC 自动化方式构建和部署云上堡垒机,同时基于堡垒机的使用场景进行了举例,介绍了不同场景下堡垒机的使用方法和步骤。该堡垒机方案已经集成到 Cloud Foundations 快速启动包服务中,为企业用户提供更便捷的部署方式。