Session Manager gives you an interactive shell on an EC2 instance with no SSH port open, no key pairs, and no bastion host. The connection is outbound from the instance to AWS, and you connect through the AWS API.
That removes a lot at once: no port 22 in any security group, no keys to distribute, rotate or revoke when someone leaves, no bastion to patch and pay for, and no public subnet needed for access. Authorisation is IAM, and every session is recorded in CloudTrail against a real identity rather than a shared key.
aws ssm start-session --target i-0123456789abcdef0Starting session with SessionId: alice@example.com-08fe3b1c2d4e5f6a7
sh-5.2$
The CLI needs the Session Manager plugin installed locally, which is a separate download from the AWS CLI itself, SessionManagerPlugin is not found is the error when it is missing.
Prerequisites are the same three as Systems Manager generally: the agent running, AmazonSSMManagedInstanceCore on the instance profile, and outbound access to the ssm, ssmmessages and ec2messages endpoints. ssmmessages is the one that matters here. An instance that shows up in Fleet Manager but fails to start a session is almost always missing that endpoint.
aws ssm describe-instance-information \
--filters Key=InstanceIds,Values=i-0123456789abcdef0 \
--query 'InstanceInformationList[0].PingStatus'Online means a session should work. Anything else is a prerequisite problem.
The benefit is not realised until you actually remove SSH access:
aws ec2 revoke-security-group-ingress --group-id sg-0123456789abcdef0 \
--protocol tcp --port 22 --cidr 0.0.0.0/0An instance reachable through Session Manager and SSH still has the bastion, the keys and the open port to manage. Removing 22 entirely is the point, and it lets instances live in private subnets with no inbound rules at all.
Without this you have replaced auditable SSH with unauditable shell access, which is a step backwards:
aws ssm create-document --name SSM-SessionManagerRunShell \
--document-type Session --document-format JSON --content '{
"schemaVersion": "1.0",
"description": "Session preferences",
"sessionType": "Standard_Stream",
"inputs": {
"s3BucketName": "my-session-logs",
"s3EncryptionEnabled": true,
"cloudWatchLogGroupName": "/aws/ssm/sessions",
"cloudWatchEncryptionEnabled": true,
"cloudWatchStreamingEnabled": true,
"kmsKeyId": "alias/session-manager",
"idleSessionTimeout": "20",
"maxSessionDuration": "120",
"runAsEnabled": true,
"runAsDefaultUser": "ssm-user",
"shellProfile": { "linux": "cd /home/ssm-user && bash" }
}
}'That records every keystroke and every byte of output to S3 and CloudWatch Logs. cloudWatchStreamingEnabled sends output continuously rather than only at session end, so a session that is killed still leaves a record, which is precisely the session you would want to read.
kmsKeyId also enables end-to-end encryption of the session itself, beyond the TLS that is always present.
idleSessionTimeout and maxSessionDuration close forgotten sessions, which is both a security improvement and a way to stop a dangling session holding things open.
Session Manager can tunnel a port, which gives you private database access with no bastion and no VPN:
# Forward a port on an instance
aws ssm start-session --target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["8080"],"localPortNumber":["9090"]}'# Forward to a host the instance can reach. An RDS database, for example
aws ssm start-session --target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{
"host":["orders.abc123.eu-west-1.rds.amazonaws.com"],
"portNumber":["5432"],
"localPortNumber":["15432"]
}'psql -h localhost -p 15432 -U app ordersThe second form is the one worth knowing. The database stays in a private subnet with no public access, the EC2 instance is only a relay, and your local tooling, psql, a GUI client, a migration script, connects to localhost. No bastion, no VPN client, and the whole thing is authorised by IAM and logged.
Existing SSH-based tooling can keep working while port 22 stays closed. Add to ~/.ssh/config:
Host i-* mi-*
ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
ssh ec2-user@i-0123456789abcdef0
scp file.txt ec2-user@i-0123456789abcdef0:/tmp/This still uses SSH keys on the instance, so it is a transitional arrangement rather than the end state, but it means Ansible, scp and rsync work without an open port while you migrate.
An IAM policy scoped by tag is the usual approach:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": { "ssm:resourceTag/Environment": "development" }
}
},
{
"Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession"],
"Resource": "arn:aws:ssm:*:*:session/${aws:username}-*"
}
]
}The second statement is easy to miss: without it a user can start a session and not end their own. The ${aws:username}-* pattern confines them to their own sessions.
You can also restrict which documents may be used, which is how you allow shell access while forbidding port forwarding, or the reverse.
aws ssm describe-sessions --state Active \
--query 'Sessions[].[SessionId,Owner,Target,StartDate]' --output tableaws ssm terminate-session --session-id "alice@example.com-08fe3b1c2d4e5f6a7"Worth an alarm: a session started against a production instance outside change hours is something you want to know about while it is happening.
Sessions run as ssm-user, created automatically by the agent, with passwordless sudo on Linux by default.
That default deserves attention. Anyone who can start a session has root, so the access control is entirely in IAM, which is fine when IAM is scoped properly and not fine when everyone has ssm:StartSession on *. To remove it:
sudo rm /etc/sudoers.d/ssm-agent-usersOr set runAsDefaultUser in the session document to connect as a specific less-privileged account instead.
An error occurred (TargetNotConnected) when calling the StartSession operation:
i-0123456789abcdef0 is not connected.
The agent is not reporting. Check the three prerequisites, and specifically the ssmmessages endpoint.
SessionManagerPlugin is not found. Please refer to SessionManager Documentation
Install the plugin locally; it is separate from the AWS CLI.
An error occurred (AccessDeniedException) when calling the StartSession operation
Missing ssm:StartSession, or a tag condition on the policy that this instance does not satisfy.
Session starts and immediately closes. Usually a broken shellProfile in the session document, or a missing shell on a minimal AMI.
| Task | Command |
|---|---|
| Connect | aws ssm start-session --target <id> |
| Is it reachable? | aws ssm describe-instance-information --filters ⦠|
| Forward a local port | --document-name AWS-StartPortForwardingSession |
| Tunnel to a database | --document-name AWS-StartPortForwardingSessionToRemoteHost |
| SSH through SSM | ProxyCommand with AWS-StartSSHSession |
| Who is connected | aws ssm describe-sessions --state Active |
| End a session | aws ssm terminate-session --session-id <id> |
| Configure logging | aws ssm create-document --name SSM-SessionManagerRunShell |
ssmmessages is the endpoint Session Manager needs: its absence is the usual failure.AWS-StartSSHSession keeps SSH-based tooling working while the port stays closed.ssm:StartSession by tag, and grant TerminateSession for the user's own sessions.ssm-user has passwordless sudo by default: IAM is the only thing standing in front of root.<MCQExercise exercise={{ id: 'aws-ssm-sm-quiz1', title: 'Visible in Fleet Manager, sessions fail', description: 'An instance in a private subnet appears in Systems Manager with PingStatus Online, but start-session fails with TargetNotConnected. VPC endpoints exist for ssm and ec2messages. What is missing?', options: [ { id: 'a', text: 'The ssmmessages VPC endpoint, which carries Session Manager traffic specifically' }, { id: 'b', text: 'An inbound security group rule allowing port 22 from the SSM service' }, { id: 'c', text: 'A public IP address on the instance for the session data channel' }, { id: 'd', text: 'The AmazonSSMFullAccess policy, which start-session requires beyond the core policy' }, ], correctId: 'a', explanation: 'The three SSM endpoints carry different traffic. ssm handles the control API and registration, ec2messages carries Run Command, and ssmmessages carries the Session Manager data channel. With the first two present the instance registers and reports Online, so everything looks healthy right up until you try to open a session, which is what makes this particular gap confusing. Create the com.amazonaws.<region>.ssmmessages interface endpoint, with private DNS enabled and a security group allowing 443 from the instances.', wrongExplanations: { b: 'Session Manager needs no inbound rules at all; that is the main reason to use it.', c: 'A private instance reaches SSM through VPC endpoints or NAT. A public IP is not part of the design.', d: 'AmazonSSMManagedInstanceCore covers session establishment. The problem is network path, not permissions.', }, }} />
<MCQExercise exercise={{ id: 'aws-ssm-sm-quiz2', title: 'Reaching a private database from a laptop', description: 'A developer needs to run psql against an RDS instance in a private subnet with no public access. There is no VPN and the team wants no bastion. What works?', options: [ { id: 'a', text: 'Port forwarding with AWS-StartPortForwardingSessionToRemoteHost through an EC2 instance that can reach the database, connecting psql to localhost' }, { id: 'b', text: 'Making the RDS instance publicly accessible and restricting it by security group' }, { id: 'c', text: 'Running psql inside a normal Session Manager shell on the instance' }, { id: 'd', text: 'A VPC peering connection between the developer's machine and the VPC' }, ], correctId: 'a', explanation: 'The remote-host port forwarding document tunnels a local port through an SSM session to any host the target instance can reach, so the database stays entirely private, the EC2 instance acts only as a relay, and local tooling connects to localhost on a chosen port. Authorisation is IAM and the session is logged in CloudTrail, which is a better audit position than a bastion with shared keys. It is the single most useful Session Manager feature and the least known.', wrongExplanations: { b: 'Exposing a production database publicly to avoid setting up a tunnel is a poor trade, even with security group restrictions.', c: 'That gives a shell-based psql on the instance rather than letting local GUI clients, migration tools or scripts connect.', d: 'VPC peering connects two VPCs, not a laptop to a VPC.', }, }} />
The agent is not reporting to SSM. Check the three prerequisites: the agent is running, the instance profile has AmazonSSMManagedInstanceCore, and there is network access to the SSM endpoints. When the instance appears in Fleet Manager as Online but sessions specifically fail, it is almost always the ssmmessages VPC endpoint, that one carries session traffic, while ssm handles registration, so a partial endpoint set produces exactly this.
Create or edit the SSM-SessionManagerRunShell session document with s3BucketName and cloudWatchLogGroupName, and set cloudWatchStreamingEnabled to true so output is written continuously rather than only when the session ends. Otherwise a session that is killed leaves no record, which is the one you would most want. Add a kmsKeyId for end-to-end session encryption, and set idleSessionTimeout and maxSessionDuration to close forgotten sessions.
Yes, and it is the feature most worth knowing. Use AWS-StartPortForwardingSessionToRemoteHost with the database hostname and port, plus a local port. The session tunnels through an EC2 instance that can reach the database, and your local psql, GUI client or migration tool connects to localhost. The database needs no public access and there is no bastion or VPN involved, while authorisation stays in IAM and the session is logged.
Yes. Add a ProxyCommand entry to ~/.ssh/config using the AWS-StartSSHSession document, and ssh, scp, rsync and Ansible work against instance IDs with port 22 closed to the network. It does still rely on SSH keys on the instance, so treat it as a transition rather than the destination, but it means existing tooling keeps working immediately rather than blocking the removal of your bastion.
By default, effectively yes. Sessions run as ssm-user, which the agent creates with passwordless sudo on Linux. That makes IAM the entire access control: anyone with ssm:StartSession on an instance has root on it. Scope the permission by tag so people can only reach the environments they should, and either remove /etc/sudoers.d/ssm-agent-users or set runAsDefaultUser in the session document to connect as a less-privileged account where full root is not warranted.
Store configuration and secrets in SSM Parameter Store with SecureString encryption, path hierarchies, versions, labels, standard vs advanced tiers and cost.
Set up multi-account AWS with Organizations. Learn why separate accounts beat separate VPCs, how to design OUs, create accounts and use consolidated billing.
Write service control policies that restrict without breaking things: deny lists over allow lists, why an SCP grants nothing, break-glass roles and testing.