Open-source tool · AWS
ssmx — a smarter AWS SSM CLI
Shell access, one-off commands and port forwarding over AWS Systems Manager — using names instead of instance IDs, endpoints and ports. You give it the RDS identifier; it looks up the endpoint and port, picks a sensible local port, and opens the tunnel. No bastion SSH keys, no open ports, no copy-pasting endpoints from the console.
The short version
- Install:
pipx install git+https://github.com/originalpages/ssmx.git— plus the AWS CLI v2 and the Session Manager plugin — Install. - Shell by name:
ssmx connect api-1— Open a shell. - Tunnel to a database by name:
ssmx forward bastion-1 --to prod-db— Port forward. - Same IAM, same APIs. A nicer interface over the AWS CLI, not a different security model — IAM permissions.
Why: three commands become one
Tunnelling to a private RDS database with the plain AWS CLI means looking up the endpoint, looking up the bastion’s instance ID, and then pasting both into a JSON parameter blob:
aws rds describe-db-instances --db-instance-identifier prod-db \
--query 'DBInstances[0].Endpoint' # 1. find host + port
aws ec2 describe-instances --filters Name=tag:Name,Values=bastion-1 \
--query 'Reservations[].Instances[].InstanceId' # 2. find instance ID
aws ssm start-session --target i-0abc1234def5678ab \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["prod-db.abc123.eu-west-1.rds.amazonaws.com"],
"portNumber":["5432"],"localPortNumber":["5432"]}' # 3. open tunnelssmx forward bastion-1 --to prod-db
# prod-db (rds:postgres) -> prod-db.abc123.eu-west-1.rds.amazonaws.com:5432
# forwarding localhost:5432 via bastion-1 (i-0abc1234def5678ab) - Ctrl+C to stop| Task | AWS CLI | ssmx |
|---|---|---|
| Shell on an instance | aws ssm start-session --target i-0abc... | ssmx connect web-1 |
| Run a command | aws ssm send-command ... + poll for output | ssmx run web-1 -- uptime |
| Tunnel to RDS / Aurora / Redis | three commands, above | ssmx forward web-1 --to prod-db |
| Instance by Name tag | manual describe-instances | built in |
| Read a SecureString | aws ssm get-parameter --with-decryption ... | ssmx param get /app/DB_PASS |
How it works
ssmx is a single Python file on top of boto3. It does the lookups itself, then hands the finished session to the aws CLI and the Session Manager plugin — the same path you would take by hand, with the same profile and region.
localhost through the bastion to the database.Because the connection runs through Systems Manager, the bastion needs no inbound security-group rules and nobody needs SSH keys. What the bastion does need is a network path to the database: its security group must be allowed into the database’s security group on the database port.
Install
Prerequisites
- Python 3.9+
- AWS CLI v2
- Session Manager plugin — needed for
connectandforward - Target instances must be SSM-managed: SSM Agent running, and an instance profile with
AmazonSSMManagedInstanceCore
Recommended: pipx
An isolated install that puts ssmx on your PATH.
pipx install git+https://github.com/originalpages/ssmx.gitOr pip
pip install git+https://github.com/originalpages/ssmx.gitOr just the script
It is a single file with one dependency.
pip install boto3
curl -L https://raw.githubusercontent.com/originalpages/ssmx/main/ssmx.py -o ~/.local/bin/ssmx
chmod +x ~/.local/bin/ssmxCheck it works
ssmx --version
ssmx listUsage, command by command
Find instances
ssmx list # all running, SSM-managed instances
ssmx ls -f api # filter by Name tag or ID substring
ssmx list -o json | jq # machine-readable$ ssmx list
NAME INSTANCE ID TYPE PRIVATE IP PING
api-1 i-0abc1234def5678ab t3.medium 10.0.1.12 Online
bastion-1 i-0fed9876cba54321f t3.micro 10.0.0.5 OnlineOpen a shell
ssmx connect api-1 # by Name tag
ssmx c i-0abc1234def5678ab # or by instance IDRun a one-off command
ssmx run api-1 -- systemctl status nginx
ssmx run api-1 -- tail -n 100 /var/log/app.log
ssmx run api-1 --timeout 600 -- ./long-job.shstdout and stderr come back to your terminal, and the remote exit code becomes ssmx’s exit code — so it works in scripts and && chains.
Port forward — the smart part
--to takes a name and works out what it is. It tries these in order and uses the first match:
- host:portUsed as-is — anything TCP: OpenSearch, Kafka, an internal ALB…
- RDS instanceInstance endpoint and port
- Aurora / RDS clusterWriter endpoint
- ElastiCache replication groupPrimary endpoint, or configuration endpoint in cluster mode
- ElastiCache cache clusterRedis, Valkey or Memcached node endpoint
ssmx forward bastion-1 --to prod-db # RDS: finds host + port
ssmx fwd bastion-1 --to orders-aurora # Aurora cluster
ssmx fwd bastion-1 --to sessions-redis # ElastiCache
ssmx fwd bastion-1 --to prod-db --local 15432 # pick your own local port
ssmx fwd bastion-1 --to search.internal:9200 # explicit host:portThe local port defaults to the remote port. Then, in another terminal:
$ psql -h localhost -p 5432 -U admin mydb
$ redis-cli -p 6379Already running the same database locally? Use --local, e.g. --local 15432 next to a local Postgres on 5432. Otherwise you get Address already in use.
Check what a name resolves to
Useful before connecting, or in scripts.
ssmx resolve prod-db
ssmx resolve api-1 --ec2
ssmx resolve prod-db -o json$ ssmx resolve prod-db
prod-db (rds:postgres) -> prod-db.abc123.eu-west-1.rds.amazonaws.com:5432
$ ssmx resolve api-1 --ec2
api-1 -> i-0abc1234def5678abParameter Store
ssmx param get /myapp/prod/DB_PASSWORD # decrypts SecureString
export DB_PASS=$(ssmx param get /myapp/prod/DB_PASSWORD)
ssmx param list /myapp/prod/
ssmx param put /myapp/prod/API_URL https://api.example.com --type String
ssmx param put /myapp/prod/DB_PASSWORD - --overwrite < secret.txt # '-' = read stdin
ssmx param delete /myapp/prod/OLD_KEY # asks for confirmation; -y to skipparam put defaults to SecureString. Prefer the - (stdin) form for secrets so the value does not end up in your shell history.
Profiles, regions and SSO
ssmx uses the standard AWS credential chain via boto3, and passes the same profile and region to the aws CLI when it opens a session. Global options (--profile, --region, --no-cache, --cache-ttl) go before the command.
ssmx --profile prod connect api-1
ssmx --region us-east-1 list
AWS_PROFILE=staging ssmx fwd bastion-1 --to staging-db
# IAM Identity Center / SSO
aws sso login --profile prod
ssmx --profile prod listThe lookup cache
Resolving names costs one or more AWS API calls, so ssmx caches successful lookups and repeat commands start instantly.
- What is cached: Name tag → instance ID, and resource name → host/port. Never credentials, parameter values or command output.
- Where:
~/.cache/ssmx/cache.json(respectsXDG_CACHE_HOME), file mode600. - Scope: per profile + region, so prod and dev never mix.
- Lifetime: 5 minutes by default.
ssmx listalso warms the cache.
ssmx --no-cache connect api-1 # bypass once
ssmx --cache-ttl 3600 fwd b-1 --to db # longer TTL for this call
export SSMX_CACHE_TTL=0 # disable entirely
ssmx cache clear # wipe it
ssmx cache path # show locationIf an instance was replaced and a cached ID is stale, the session fails fast; run ssmx cache clear (or wait for the TTL) and retry.
IAM permissions
Same APIs, same permissions as the AWS CLI. Minimum actions for the caller, per command:
| Command | Actions |
|---|---|
list | ec2:DescribeInstances, ssm:DescribeInstanceInformation |
connect | ec2:DescribeInstances, ssm:StartSession, ssm:TerminateSession |
run | ec2:DescribeInstances, ssm:SendCommand, ssm:GetCommandInvocation |
forward | connect permissions + rds:DescribeDBInstances, rds:DescribeDBClusters, elasticache:DescribeReplicationGroups, elasticache:DescribeCacheClusters |
param get | ssm:GetParameter (+ kms:Decrypt for a SecureString with a customer key) |
param put | ssm:PutParameter (+ kms:Encrypt) |
param list | ssm:DescribeParameters |
param delete | ssm:DeleteParameter |
forward also needs ssm:StartSession on the document AWS-StartPortForwardingSessionToRemoteHost. A starting-point policy covering every command — tighten the Resource entries for production:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Discovery",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ssm:DescribeInstanceInformation",
"rds:DescribeDBInstances",
"rds:DescribeDBClusters",
"elasticache:DescribeReplicationGroups",
"elasticache:DescribeCacheClusters"
],
"Resource": "*"
},
{
"Sid": "Sessions",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ssm:*::document/AWS-StartPortForwardingSessionToRemoteHost",
"arn:aws:ssm:*:*:document/SSM-SessionManagerRunShell"
]
},
{
"Sid": "EndOwnSessions",
"Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession"],
"Resource": "arn:aws:ssm:*:*:session/${aws:userid}-*"
},
{
"Sid": "RunCommand",
"Effect": "Allow",
"Action": "ssm:SendCommand",
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ssm:*::document/AWS-RunShellScript"
]
},
{
"Sid": "RunCommandOutput",
"Effect": "Allow",
"Action": "ssm:GetCommandInvocation",
"Resource": "*"
},
{
"Sid": "ParameterStore",
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:DescribeParameters"
],
"Resource": "*"
}
]
}Tag conditions and path-scoped Parameter Store examples are in docs/iam.md.
Troubleshooting
| Symptom | Fix |
|---|---|
no AWS region configured | Pass --region, set AWS_REGION, or add region to your profile |
| Token expired / SSO errors | aws sso login --profile <name> |
SessionManagerPlugin is not found | Install the Session Manager plugin (see Prerequisites) |
Instance not in ssmx list | Not SSM-managed: check the SSM Agent and the instance profile |
matches several running instances | Two instances share a Name tag — use the instance ID |
| Tunnel opens but the DB times out | The DB’s security group does not allow the tunnel instance |
Address already in use | Something already listens on that port — use --local <other port> |
| Connects to an old or terminated instance | ssmx cache clear |
Source, issues and pull requests: github.com/originalpages/ssmx.