How to Use AWS Systems Manager Session Manager Instead of SSH

Port 22 sitting open on a production EC2 instance is the kind of thing that keeps security engineers up at night — and for good reason. AWS Systems Manager Session Manager gives you a fully audited, browser-based shell to any managed instance without opening a single inbound port, without distributing SSH keys, and without a bastion host eating money in the corner of your VPC.

TL;DR: Session Manager vs SSH at a Glance

ConcernSSHSession Manager
Inbound port requiredPort 22 openNone — outbound HTTPS only
Key managementPEM files distributed to engineersIAM identity, no keys
Session audit trailNone by defaultCloudTrail + optional S3/CloudWatch logging
Access controlOS-level authorized_keysIAM policies + optional tag-based conditions
Bastion hostOften requiredNot needed
Works in private subnetRequires VPN or bastionYes, via SSM endpoints or NAT

How AWS Systems Manager Session Manager Works

Session Manager does not open a reverse shell in the traditional sense. The SSM Agent running on your EC2 instance maintains a persistent outbound HTTPS connection to the Systems Manager service endpoint (port 443). When you initiate a session, the control plane signals the agent through that existing channel — the instance never accepts an inbound connection. Your shell traffic flows over WebSocket tunneled through the SSM service, encrypted in transit with TLS.

sequenceDiagram participant Agent as SSM Agent
(EC2 Instance) participant SSM as SSM Service
(AWS Control Plane) participant User as Engineer
(Console or CLI) Agent->>SSM: Outbound HTTPS poll (port 443) Note over Agent,SSM: No inbound port needed on instance User->>SSM: StartSession API call (IAM authenticated) SSM-->>User: IAM authorization check SSM->>Agent: Signal via existing channel Agent-->>SSM: WebSocket session channel opened SSM-->>User: Session terminal connected User->>SSM: Shell input (encrypted) SSM->>Agent: Forwarded over WebSocket Agent-->>SSM: Shell output (encrypted) SSM-->>User: Output displayed in terminal Note over SSM: Optional: stream to S3 or CloudWatch Logs
  1. SSM Agent polls outbound — the agent on the instance opens a long-lived HTTPS connection to the regional SSM endpoint. No inbound rule is needed.
  2. IAM authentication — when you start a session via console or CLI, IAM validates your identity and permissions before the SSM control plane accepts the request.
  3. Session channel established — SSM creates an encrypted WebSocket channel between your client and the agent, proxied through the service. Traffic never traverses the public internet directly to the instance.
  4. Optional logging — session input/output can be streamed to CloudWatch Logs or S3 for audit purposes, controlled by the SSM document attached to the session preference.

The agent needs outbound connectivity to reach the SSM endpoints. For instances in a private subnet with no NAT Gateway, you can use VPC Interface Endpoints for Systems Manager — specifically com.amazonaws.region.ssm, com.amazonaws.region.ssmmessages, and com.amazonaws.region.ec2messages.

Prerequisites Before Your First Session Manager Connection

Three things must be in place. Miss any one of them and the instance simply won't appear in the Session Manager console — no error, just silence.

  • SSM Agent installed and running — Amazon Linux 2, Amazon Linux 2023, and recent Windows AMIs ship with it pre-installed. Ubuntu and other distributions may require manual installation.
  • Instance profile with SSM permissions — the EC2 instance needs an IAM role with at minimum the AmazonSSMManagedInstanceCore managed policy attached.
  • Outbound HTTPS reachability — either a NAT Gateway, Internet Gateway (for public subnets), or VPC Interface Endpoints for the three SSM service namespaces listed above.

Step 1: Create an IAM Instance Profile for Session Manager

The instance profile is the most commonly skipped step. Without it, the SSM Agent has no credentials to register with the Systems Manager service, and the instance never shows up as managed.

Create the trust policy document and role, then attach the managed policy:

# Create the trust policy file
cat > ec2-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# Create the IAM role
aws iam create-role \
  --role-name EC2SSMRole \
  --assume-role-policy-document file://ec2-trust-policy.json

# Attach the managed policy
aws iam attach-role-policy \
  --role-name EC2SSMRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

# Create the instance profile and add the role
aws iam create-instance-profile \
  --instance-profile-name EC2SSMProfile

aws iam add-role-to-instance-profile \
  --instance-profile-name EC2SSMProfile \
  --role-name EC2SSMRole

If your instance is already running without a profile, you can associate one without stopping it:

aws ec2 associate-iam-instance-profile \
  --instance-id i-0abc123def456789 \
  --iam-instance-profile Name=EC2SSMProfile

After association, the SSM Agent picks up the new credentials and registers within a few minutes. You can verify registration with:

aws ssm describe-instance-information \
  --filters Key=InstanceIds,Values=i-0abc123def456789 \
  --query 'InstanceInformationList[*].{ID:InstanceId,PingStatus:PingStatus,AgentVersion:AgentVersion}' \
  --output table

A PingStatus of Online means the agent is registered and reachable. If you see nothing returned, the instance hasn't registered yet — check outbound connectivity and the agent status on the instance itself.

Step 2: Verify SSM Agent Status on the Instance

If the instance doesn't appear in describe-instance-information after a few minutes, the agent is the first thing to check. This step catches the gap that the IAM check can't — the role might be correct, but the agent might be stopped or on an outdated version that doesn't support Session Manager.

For Amazon Linux 2 / Amazon Linux 2023:

sudo systemctl status amazon-ssm-agent

If it's stopped:

sudo systemctl enable amazon-ssm-agent
sudo systemctl start amazon-ssm-agent

To check the installed version remotely using SSM Run Command (once the agent is at least partially functional):

aws ssm send-command \
  --instance-ids i-0abc123def456789 \
  --document-name AWS-RunShellScript \
  --parameters commands='amazon-ssm-agent --version' \
  --output text

The minimum agent version supporting Session Manager is 2.3.68.0. Anything older and sessions will silently fail to establish.

Step 3: Configure Session Preferences for Logging (Optional but Recommended)

Raw shell access with no audit trail is only marginally better than SSH from a compliance standpoint. Session Manager supports logging session output to S3 or CloudWatch Logs, configured through an SSM document called SSM-SessionManagerRunShell in your account.

To update session preferences via CLI:

🔽 Click to expand: Session Manager preferences JSON
aws ssm update-document \
  --name SSM-SessionManagerRunShell \
  --content '{
    "schemaVersion": "1.0",
    "description": "Session Manager preferences",
    "sessionType": "Standard_Stream",
    "inputs": {
      "s3BucketName": "your-audit-bucket-name",
      "s3KeyPrefix": "session-logs/",
      "s3EncryptionEnabled": true,
      "cloudWatchLogGroupName": "/aws/ssm/sessions",
      "cloudWatchEncryptionEnabled": true,
      "cloudWatchStreamingEnabled": true,
      "idleSessionTimeout": "20",
      "runAsEnabled": false,
      "shellProfile": {
        "linux": "",
        "windows": ""
      }
    }
  }' \
  --document-version '\$LATEST'

The instance profile also needs permission to write to the S3 bucket and CloudWatch Logs group. Add an inline policy or attach an additional managed policy covering s3:PutObject on the target bucket and logs:CreateLogStream / logs:PutLogEvents on the log group.

Step 4: Grant Engineers IAM Permission to Start Sessions

The instance having the right role is only half the equation. The human (or CI system) initiating the session also needs IAM permissions. This is where you enforce who can shell into which instances — something SSH's authorized_keys file was never designed to do at scale.

Minimum policy for a user or role to start a session on a specific instance:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:StartSession"
      ],
      "Resource": [
        "arn:aws:ec2:us-east-1:123456789012:instance/i-0abc123def456789",
        "arn:aws:ssm:us-east-1::document/AWS-StartSSHSession"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "ssm:DescribeSessions",
        "ssm:GetConnectionStatus",
        "ssm:DescribeInstanceProperties",
        "ec2:DescribeInstances"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ssm:TerminateSession",
        "ssm:ResumeSession"
      ],
      "Resource": "arn:aws:ssm:us-east-1:123456789012:session/${aws:username}-*"
    }
  ]
}

The TerminateSession resource uses the ${aws:username} policy variable so engineers can only terminate their own sessions, not each other's. Adjust the instance ARN to a tag-based condition if you want to scope access by environment tag rather than instance ID.

Step 5: Start a Session

From the AWS Console, navigate to Systems Manager → Session Manager → Start session, select the instance, and click Start session. A browser-based terminal opens immediately.

From the CLI, you need the Session Manager plugin installed locally in addition to the AWS CLI:

# Start an interactive session
aws ssm start-session \
  --target i-0abc123def456789 \
  --region us-east-1

The plugin handles the WebSocket tunnel transparently. You land in a shell as ssm-user by default on Linux instances. This user is created by the SSM Agent and has sudo access on Amazon Linux AMIs — something worth locking down in hardened environments by configuring runAsEnabled and specifying a restricted OS user in the session preferences document.

Step 6: Set Up VPC Interface Endpoints for Private Subnets

Instances in private subnets with no NAT Gateway won't reach the SSM service endpoints over the public internet. The fix is three VPC Interface Endpoints — not one, not two. Missing any one of them and sessions either fail to establish or establish but immediately drop.

graph LR Instance["EC2 Instance
(private subnet)"] -->|HTTPS 443| EP1["VPC Endpoint
ssm"] Instance -->|HTTPS 443| EP2["VPC Endpoint
ssmmessages"] Instance -->|HTTPS 443| EP3["VPC Endpoint
ec2messages"] EP1 --> SSMSVC["SSM Service
(Registration + API)"] EP2 --> SESSSVC["SSM Service
(Session Traffic)"] EP3 --> EC2SVC["EC2 Messages
(Agent comms)"] User["Engineer
(Console or CLI)"] -->|StartSession| SESSSVC style Instance fill:#f0f4ff,stroke:#4a6fa5 style EP2 fill:#fff3cd,stroke:#856404 style User fill:#d4edda,stroke:#155724
  1. ssm endpoint — handles the core Systems Manager API calls and agent registration.
  2. ssmmessages endpoint — carries the actual session channel traffic. This is the one most commonly forgotten.
  3. ec2messages endpoint — used by the agent for Run Command and some agent-to-service communication paths.
# Create the SSM endpoint
aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0abc123def456789 \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.ssm \
  --subnet-ids subnet-0abc123def456789 \
  --security-group-ids sg-0abc123def456789 \
  --private-dns-enabled

# Create the SSM messages endpoint
aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0abc123def456789 \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.ssmmessages \
  --subnet-ids subnet-0abc123def456789 \
  --security-group-ids sg-0abc123def456789 \
  --private-dns-enabled

# Create the EC2 messages endpoint
aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0abc123def456789 \
  --vpc-endpoint-type Interface \
  --service-name com.amazonaws.us-east-1.ec2messages \
  --subnet-ids subnet-0abc123def456789 \
  --security-group-ids sg-0abc123def456789 \
  --private-dns-enabled

The security group attached to the endpoints needs to allow inbound HTTPS (port 443) from the instance security group. The instance itself needs no inbound rules at all.

The Misdiagnosis That Wastes an Hour

Here's a pattern that shows up repeatedly: engineer sets up Session Manager, instance shows Online in describe-instance-information, but clicking 'Start session' in the console returns a generic error after a 30-second timeout. The instinct is to blame IAM — check the policy, add more permissions, try again. Same result.

The actual cause is almost always the ssmmessages endpoint missing or its security group blocking port 443 from the instance. The agent registers fine using the ssm endpoint, which is why it shows Online. But session traffic flows through ssmmessages — a separate endpoint that registration doesn't test. The instance looks healthy, IAM looks correct, and the one broken piece is a network path that no console health check surfaces.

Verify the endpoint security group allows inbound 443 from the instance's security group:

aws ec2 describe-vpc-endpoints \
  --filters Name=service-name,Values=com.amazonaws.us-east-1.ssmmessages \
  --query 'VpcEndpoints[*].{ID:VpcEndpointId,State:State,Groups:Groups}' \
  --output table

If the endpoint exists but sessions still fail, check that --private-dns-enabled was set. Without it, the agent resolves the SSM hostname to a public IP and the private endpoint is bypassed entirely.

Wrap-Up and Next Steps for AWS Systems Manager Session Manager

Session Manager eliminates the operational surface area that SSH creates — no open ports, no key rotation, no bastion hosts, and every session tied to an IAM identity with a CloudTrail record. The setup overhead is a one-time cost; the security posture improvement is permanent.

From here, consider:

  • Port forwarding via Session Manageraws ssm start-session --target ... --document-name AWS-StartPortForwardingSession lets you tunnel to RDS, Redis, or internal services without a VPN.
  • SSH over Session Manager — configure your local SSH client to proxy through SSM so existing tooling (scp, rsync, VS Code Remote) works without port 22 exposure.
  • AWS Config rule for open port 22 — use the managed rule restricted-ssh to detect and alert on any security group that re-opens port 22.
  • Review the official Session Manager documentation for KMS encryption of session data and advanced session preferences.

Glossary

TermDefinition
SSM AgentSoftware installed on EC2 instances that communicates with the Systems Manager service. Required for Session Manager to function.
Instance ProfileAn IAM container that attaches a role to an EC2 instance, giving the instance credentials to call AWS APIs.
VPC Interface EndpointA private network interface in your VPC that routes traffic to an AWS service without traversing the public internet.
ssmmessagesThe AWS service endpoint namespace that carries interactive session channel traffic for Session Manager.
AmazonSSMManagedInstanceCoreAWS managed IAM policy granting the minimum permissions an EC2 instance needs to be managed by Systems Manager.

Related Posts

Comments

Popular posts from this blog

AWS SNS Email Alerts Not Arriving? The Subscription Confirmation Trap Explained

S3 Public Access Denied: Why Your Public Object URL Still Returns 403

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table