Systems Manager Session

Introduction

AWS Systems Manager Session Manager is a fully managed AWS service that allows you to securely connect to your EC2 instances (Linux or Windows) without needing to open inbound ports, manage SSH keys, or use a bastion host. In this post, I will demonstrate how an EC2 sitting in a private subnet, without any access to the Internet, can be connected to using SSM through the AWS Console and remotely.

Key Features

  • Uses SSM agent which runs on your instance
  • No need for SSH server or RDP listener
  • All traffic is encrypted with TLS
  • Logging can be enabled for session activity
  • Works with Linux, MacOS, and Windows EC2 instances
  • Can be initiated through AWS Console, CLI, or SDK

Logical

Players

  • IAM user with the console access, CLI credentials, and policy enabled for SSM
  • EC2 with SSM role and security group for SSM traffic outbound
  • VPC Endpoints with security group for SSM traffic outbound and EC2 traffic inbound
  • VPC with two private subnets without any Internet traffic inbound or outbound

VPC with Two Private Subnets

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

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "my-vpc"
  }
}

resource "aws_subnet" "private_a" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1a"
  map_public_ip_on_launch = false
  tags = {
    Name = "private-subnet-a"
  }
}

resource "aws_subnet" "private_b" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1b"
  map_public_ip_on_launch = false
  tags = {
    Name = "private-subnet-b"
  }
}

IAM User with Policy

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

resource "aws_iam_user" "mgnuser" {
  name = "mgnuser"
  tags = {
    Purpose = "MGN Session Access"
  }
}

resource "random_password" "mgnuser" {
  length  = 16
  special = true
}

resource "aws_iam_user_login_profile" "mgnuser" {
  user                    = aws_iam_user.mgnuser.name
  password_reset_required = true
  password                = random_password.mgnuser.result
}

resource "aws_iam_access_key" "mgnuser" {
  user = aws_iam_user.mgnuser.name
}

resource "aws_iam_user_policy" "mgnuser_inline_policy" {
  name = "mgnuser-ssm-inline-policy"
  user = aws_iam_user.mgnuser.name

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "ssm:StartSession",
          "ssm:SendCommand"
        ],
        Resource = [
          "arn:aws:ec2:us-east-1:<your-account-id>:instance/<instance-id>",
          "arn:aws:ssm:*::document/AWS-StartPortForwardingSession"
        ]
      },
      {
        Effect = "Allow",
        Action = [
          "ssm:DescribeSessions",
          "ssm:GetConnectionStatus",
          "ssm:DescribeInstanceInformation",
          "ssm:DescribeInstanceProperties",
          "ec2:DescribeInstances"
        ],
        Resource = "*"
      },
      {
        Effect = "Allow",
        Action = [
          "ssm:TerminateSession",
          "ssm:ResumeSession"
        ],
        Resource = [
          "arn:aws:ssm:*:*:session/\${aws:username}-*"
        ]
      }
    ]
  })
}

output "mgnuser_console_password" {
  value     = random_password.mgnuser.result
  sensitive = true
}

output "mgnuser_access_key_id" {
  value     = aws_iam_access_key.mgnuser.id
  sensitive = true
}

output "mgnuser_secret_access_key" {
  value     = aws_iam_access_key.mgnuser.secret
  sensitive = true
}

EC2 Role with Policy

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

# IAM Role for EC2
resource "aws_iam_role" "ec2_ssm_role" {
  name = "ec2-ssm-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "ec2.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
  tags = {
    Purpose = "SSM Messaging Access"
  }
}

# Inline Policy
resource "aws_iam_role_policy" "ec2_ssm_inline_policy" {
  name = "ssm-messaging-inline"
  role = aws_iam_role.ec2_ssm_role.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "ssm:UpdateInstanceInformation",
          "ssmmessages:CreateControlChannel",
          "ssmmessages:CreateDataChannel",
          "ssmmessages:OpenControlChannel",
          "ssmmessages:OpenDataChannel"
        ],
        Resource = "*"
      }
    ]
  })
}

# IAM Instance Profile (to attach to EC2)
resource "aws_iam_instance_profile" "ec2_ssm_instance_profile" {
  name = "ec2-ssm-instance-profile"
  role = aws_iam_role.ec2_ssm_role.name
}

Security Group for EC2

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

resource "aws_security_group" "self_referencing_sg" {
  name        = "self-referencing-sg"
  description = "Allow all outbound traffic to itself only"
  vpc_id      = aws_vpc.main.id

  tags = {
    Name = "self-referencing-sg"
  }
}

resource "aws_security_group_rule" "egress_self" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1" # all protocols
  security_group_id = aws_security_group.self_referencing_sg.id
  source_security_group_id = aws_security_group.self_referencing_sg.id
  description       = "Allow all outbound traffic to itself"
}

Security Group for VPC Endpoints

resource "aws_security_group" "https_from_sg" {
  name        = "https-from-self-referencing-sg"
  description = "Allow inbound 443 from self-referencing SG"
  vpc_id      = aws_vpc.main.id

  tags = {
    Name = "https-from-self-referencing-sg"
  }
}

resource "aws_security_group_rule" "ingress_https_from_self_sg" {
  type                     = "ingress"
  from_port                = 443
  to_port                  = 443
  protocol                 = "tcp"
  security_group_id        = aws_security_group.https_from_sg.id
  source_security_group_id = aws_security_group.self_referencing_sg.id
  description              = "Allow HTTPS from self-referencing SG"
}

resource "aws_security_group_rule" "egress_all" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1" # all protocols
  cidr_blocks       = ["0.0.0.0/0"]
  ipv6_cidr_blocks  = ["::/0"]
  security_group_id = aws_security_group.https_from_sg.id
  description       = "Allow all outbound traffic"
}

VPC Endpoint 1 – SSM

resource "aws_vpc_endpoint" "ssm_endpoint" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-east-1.ssm"
  vpc_endpoint_type = "Interface"

  subnet_ids = [
    aws_subnet.private_a.id,
    aws_subnet.private_b.id
  ]

  security_group_ids = [
    aws_security_group.https_from_sg.id
  ]

  private_dns_enabled = true

  tags = {
    Name = "ssm-endpoint"
  }
}

VPC Endpoint 2 – SSM Messages

resource "aws_vpc_endpoint" "ssm_endpoint" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.us-east-1.ssmmessages"
  vpc_endpoint_type = "Interface"

  subnet_ids = [
    aws_subnet.private_a.id,
    aws_subnet.private_b.id
  ]

  security_group_ids = [
    aws_security_group.https_from_sg.id
  ]

  private_dns_enabled = true

  tags = {
    Name = "ssm-endpoint"
  }
}

EC2 with Role and Security Group

resource "aws_instance" "ssm_ec2" {
  ami                         = "ami-02e3d076cbd5c28fa"
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.private_a.id
  vpc_security_group_ids      = [aws_security_group.self_referencing_sg.id]
  iam_instance_profile        = aws_iam_instance_profile.ec2_ssm_instance_profile.name
  associate_public_ip_address = false
  key_name                    = null 

  tags = {
    Name = "ssm-managed-ec2"
  }
}

Connecting to the EC2 – AWS Console

Once you run the terraform script to create all the players, you will be able to login as the user that was created, go to EC2 console, and Connect to PowerShell of the EC2 through SSM Session Manager, all without a jumpbox/bastion host.

Connecting to the EC2 – Outside of AWS Console

From outside of AWS, you can connect to the same private EC2 without the need for a jumbox/bastion host provided you install the Session Manager plugin located here: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html

Once installed, you can obtain the EC2 Instance ID and run the command to connect to the EC2

aws ssm start-session --target i-06547163d9f33d195

Conclusion

AWS Systems Manager Session Manager is a powerful tool that provides secure, auditable, and convenient access to your EC2 instances without the need for SSH or RDP. By eliminating the need to open inbound ports or manage SSH keys, it significantly reduces your attack surface and simplifies operations.

Whether you’re managing a large fleet of Linux or Windows instances, Session Manager ensures compliance and security by integrating seamlessly with IAM for access control and CloudWatch or S3 for session logging.

Incorporating Session Manager into your infrastructure is a best practice for modern cloud environments, especially for organizations focused on security, automation, and operational efficiency.

Bonus – Port Forwarding

If you rather not connect to a Powershell to the EC2, you can set up port forwarding where you can use remote desktop protocol (for Windows) to connect to the EC2.

aws ssm start-session --target i-06547163d9f33d195 --document-name AWS-StartPortForwardingSession --parameters portNumber="3389", localPortNumber="49225"

The above:

  • uses the Port Forwarding document from AWS to initiate the port forwarding
  • uses your machine’s local port of 49225 (make sure this is free)
  • uses the default RDP port of 3389 on the remote EC2 to marry the local port you defined above
  • allows you to use Remote Desktop Protocol (on port 49225) to connect to the remote EC2 to get you a full desktop of the EC2

Leave a Comment

Your email address will not be published. Required fields are marked *