AWS SSM Session Manager: Kill Your Bastion Hosts

Bits Lovers
Written by Bits Lovers on
AWS SSM Session Manager: Kill Your Bastion Hosts

Every bastion host in your architecture is a maintenance burden and an attack surface. You need to keep the AMI patched, manage SSH keys across the team, control security group rules, and make sure the instance is running when someone needs access at 2am. Worse, every SSH connection over the open internet is a potential credential leak. Session Manager eliminates all of it. No open port 22, no SSH keys, no bastion host to maintain — just IAM-based shell access over HTTPS, with a full audit trail in CloudTrail.

Session Manager is part of AWS Systems Manager and has been available since 2018. It works through the SSM Agent already installed on Amazon Linux 2, AL2023, Ubuntu 16.04+, and Windows Server 2008+. Here’s how to replace your bastion hosts with it, set up port forwarding, and lock down access to specific instance tags.

How It Works

The SSM Agent runs on your EC2 instance and maintains an outbound HTTPS connection to the SSM service endpoint. When you start a session, the connection goes through this existing outbound channel — no inbound ports needed. Your security groups don’t need to allow any inbound traffic. Port 22 can be closed completely.

Access is controlled entirely by IAM. The IAM principal (user, role, or SSO session) making the ssm:StartSession call needs permission to start a session. The EC2 instance’s IAM role needs the AmazonSSMManagedInstanceCore policy to allow the agent to communicate with SSM endpoints.

This is the minimal IAM setup:

# Attach the managed policy to your EC2 instance profile
aws iam attach-role-policy \
  --role-name MyEC2InstanceRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

# Verify the instance is visible in SSM
aws ssm describe-instance-information \
  --query 'InstanceInformationList[].{ID:InstanceId,Platform:PlatformName,Status:PingStatus}'

The PingStatus field shows Online when the agent is connected. ConnectionLost means the agent is installed but can’t reach the SSM endpoint — usually a missing VPC endpoint or NAT gateway.

Starting a Session

The quickest way to open a shell:

# Install the Session Manager plugin for AWS CLI
# On macOS:
brew install --cask session-manager-plugin

# On Linux (x86_64):
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
sudo dpkg -i session-manager-plugin.deb

# Start a session
aws ssm start-session --target i-0abc123def456789

That opens an interactive shell. The session runs as ssm-user by default (or SYSTEM on Windows). The default session timeout is 20 minutes of idle time, configurable up to 2,160 minutes (36 hours) in Session Manager preferences.

To use your regular SSH client through the SSM tunnel:

# Add this to ~/.ssh/config
Host i-* mi-*
    ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"

# Then SSH normally — no bastion, no open port 22
ssh ec2-user@i-0abc123def456789

This still uses SSH key authentication, but the transport goes through SSM. You get the familiar SSH experience plus SSM’s audit logging. The security group for the instance doesn’t need port 22 open — only the SSM endpoints need to be reachable.

Port Forwarding

Session Manager supports two port forwarding modes. The first tunnels a port from your local machine to a port on the EC2 instance:

# Forward local port 5432 to RDS port 5432 via the bastion EC2
aws ssm start-session \
  --target i-0abc123def456789 \
  --document-name AWS-StartPortForwardingSession \
  --parameters '{"portNumber":["5432"],"localPortNumber":["5432"]}'

# Now connect to localhost:5432 — it routes through to the RDS instance
psql -h localhost -p 5432 -U dbuser -d mydb

The second mode — added in 2022 — forwards directly to a remote host without an EC2 instance as intermediary. This lets you access RDS, ElastiCache, or any private endpoint through a lightweight EC2 instance acting as a jump host, without ever logging into that instance:

# Forward local port to RDS endpoint directly (no shell needed on the EC2)
aws ssm start-session \
  --target i-0abc123def456789 \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{
    "host":["mydb.cluster-xxxx.us-east-1.rds.amazonaws.com"],
    "portNumber":["5432"],
    "localPortNumber":["5432"]
  }'

This pattern is better than a persistent bastion host — the EC2 instance can be a t4g.nano running nothing but the SSM agent, no SSH daemon, no exposed ports, and the port forwarding session closes automatically when you disconnect.

Private Instances Without NAT Gateway

Instances in private subnets without NAT gateway access need VPC endpoints to reach SSM. Three endpoints are required:

# Create VPC endpoints for SSM (interface type)
for SERVICE in ssm ssmmessages ec2messages; do
  aws ec2 create-vpc-endpoint \
    --vpc-id vpc-0abc123 \
    --vpc-endpoint-type Interface \
    --service-name com.amazonaws.us-east-1.$SERVICE \
    --subnet-ids subnet-0abc123 subnet-0def456 \
    --security-group-ids sg-0abc123 \
    --private-dns-enabled
done

The security group on the VPC endpoints needs to allow HTTPS (443) from the instance’s security group. Once the three endpoints exist, SSM Agent on private instances routes through them instead of going to the public internet. No NAT gateway, no internet gateway, no outbound internet access needed.

VPC endpoints cost $0.01/hour per endpoint per AZ. For three SSM endpoints across two AZs, that’s $0.06/hour — about $43/month. Compared to a bastion host ($0.023/hour for t3.micro plus EIP plus maintenance time), VPC endpoints are competitive and you eliminate the security risk.

IAM Policy Granularity

The most useful IAM feature for Session Manager is restricting access by instance tag. This lets you give developers access to dev instances but block production access without separate AWS accounts:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ssm:StartSession",
      "Resource": "arn:aws:ec2:us-east-1:123456789012:instance/*",
      "Condition": {
        "StringEquals": {
          "ssm:resourceTag/Environment": "dev"
        }
      }
    },
    {
      "Effect": "Deny",
      "Action": "ssm:StartSession",
      "Resource": "arn:aws:ec2:us-east-1:123456789012:instance/*",
      "Condition": {
        "StringEquals": {
          "ssm:resourceTag/Environment": "prod"
        }
      }
    }
  ]
}

Tag your EC2 instances with Environment: dev or Environment: prod, and this policy enforces the separation at the IAM layer — developers can reach dev instances, production access requires a different IAM role.

You can also restrict which SSM documents (session types) a user can run. Allowing only AWS-StartSSHSession but not AWS-StartInteractiveCommand gives SSH access without an interactive SSM shell:

{
  "Effect": "Allow",
  "Action": "ssm:StartSession",
  "Resource": [
    "arn:aws:ssm:us-east-1::document/AWS-StartSSHSession",
    "arn:aws:ssm:us-east-1::document/AWS-StartPortForwardingSession"
  ]
}

Audit Logging

Every session is logged in CloudTrail automatically — the StartSession, ResumeSession, and TerminateSession API calls appear with the IAM principal, the target instance, the timestamp, and the source IP. That’s the minimum audit trail.

For session content logging (what commands were run), configure a session preferences document:

aws ssm update-document \
  --name "SSM-SessionManagerRunShell" \
  --document-version "\$LATEST" \
  --content '{
    "schemaVersion": "1.0",
    "description": "Session Manager session preferences",
    "sessionType": "Standard_Stream",
    "inputs": {
      "s3BucketName": "my-ssm-session-logs",
      "s3KeyPrefix": "session-logs/",
      "s3EncryptionEnabled": true,
      "cloudWatchLogGroupName": "/aws/ssm/sessions",
      "cloudWatchEncryptionEnabled": true,
      "cloudWatchStreamingEnabled": true,
      "kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/xxx",
      "runAsEnabled": false,
      "shellProfile": {
        "linux": "exec bash\ncd ~\n"
      }
    }
  }'

With cloudWatchStreamingEnabled: true, session input and output stream to CloudWatch Logs in near real-time. Every command typed and every output line is logged. This satisfies most compliance requirements for privileged access auditing — CIS Benchmark controls, SOC 2 audit trails, and PCI DSS requirement 10.2 (user access to systems with cardholder data).

Session Manager vs EC2 Instance Connect

EC2 Instance Connect also provides keyless access to EC2 instances, but the mechanism is different. Instance Connect generates a temporary SSH key, pushes it to the instance’s metadata for 60 seconds, and you connect over SSH. Port 22 must be open. The connection goes over the internet (or through Instance Connect Endpoint for private instances).

Session Manager needs zero open ports, no SSH daemon, works on Windows without any SSH configuration, and provides richer audit logging. Instance Connect is simpler to set up for one-off access, but Session Manager is the better long-term architecture. Remove your bastion hosts, close port 22 on all instances, and deploy the SSM Agent.

For the IAM roles you assign to EC2 instances for Session Manager access, see the AWS IAM roles and policies guide for how to scope the instance profile beyond the managed policy. The IAM cross-account roles guide covers how to grant cross-account Session Manager access for teams that operate in a multi-account setup.

The operational cost of bastion hosts — patching, key rotation, security group hygiene, on-call alerts when they go down — adds up to more than the VPC endpoint cost. Session Manager is the right default for any new infrastructure.

Bits Lovers

Bits Lovers

Professional writer and blogger. Focus on Cloud Computing.

Comments

comments powered by Disqus