This file provides guidance to AI assistants when working with code in this repository.
ssm-ssh-connect is a Go-based SSH proxy command that enables keyless SSH access to EC2 instances through AWS Session Manager. It bridges the gap between AWS Session Manager's WebSocket connections and traditional SSH tooling by acting as a ProxyCommand in SSH configurations.
The tool automatically:
- Resolves EC2 instance IDs from instance names (with 24h caching)
- Pushes SSH public keys to instances via EC2 Instance Connect (60s TTL)
- Establishes SSM sessions through the
session-manager-plugin
All code lives in main.go (370 lines). The application is intentionally monolithic to simplify distribution as a standalone binary.
-
Caching System (lines 150-203)
- Instance metadata cached in
~/.ssm-ssh-connect/<profile>-<instance>-<user>.json - 24-hour TTL for cache entries
- Reduces AWS API calls for repeated connections
- Instance metadata cached in
-
Lock File Mechanism (lines 103-139)
- Prevents duplicate SSH key pushes during concurrent connections
- Lock files:
~/.ssm-ssh-connect/<profile>-<instance>-<user>.lock - Auto-cleanup after 50 seconds (SSH keys valid for 60s)
- Critical for handling multiple SSH sessions to same instance
-
AWS Integration Flow
- EC2: Queries instances by Name tag (main:221-248)
- EC2 Instance Connect: Sends SSH public keys (main:250-271)
- SSM: Starts SSH session via
AWS-StartSSHSessiondocument (main:285-369)
-
Session Manager Plugin Invocation (main:328-356)
- Locates
session-manager-pluginbinary in common paths - Argument order is critical (matches AWS SDK expectations)
- Passes session metadata as JSON structures
- Locates
-
Parent Process Monitoring (main:365-406, added in v0.2.0)
- Detects orphaned sessions when SSH client dies unexpectedly
- Checks every 1 hour if parent process (SSH) is still alive
- Kills
session-manager-pluginif parent PID changes or becomes 1 - Prevents accumulation of zombie processes
DO NOT attempt to detect SSH disconnect by reading from os.Stdin. This was attempted and breaks SSH sessions.
Why it fails:
// BROKEN APPROACH - DO NOT USE
go func() {
io.Copy(io.Discard, os.Stdin) // Consumes stdin data!
// Detect close...
}()When cmd.Stdin = os.Stdin is set (line 360), the session-manager-plugin process expects to read SSH data from stdin. If another goroutine also reads from os.Stdin, both compete for the data stream, causing:
- SSH session hangs immediately after connection
- Commands typed by user are lost/discarded
- Session becomes unresponsive
Why cmd.Run() must stay in a goroutine:
- Original
cmd.Run()blocks until plugin exits - Running in goroutine allows concurrent parent process monitoring
- Select statement handles both normal exit and parent death
- Preserves stdin/stdout/stderr passthrough to plugin
go build -o ssm-ssh-connectCross-compile for macOS (Intel):
GOOS=darwin GOARCH=amd64 go build -o ssm-ssh-connectCross-compile for macOS (ARM):
GOOS=darwin GOARCH=arm64 go build -o ssm-ssh-connectManual testing requires:
# Set debug logging
export SSM_SSH_CONNECT_DEBUG=1
# Test connection (invoked via SSH)
ssh -v <instance-name>
# View logs
tail -f ~/.ssm-ssh-connect/ssm-ssh-connect.logNo unit tests exist. Testing is done through SSH connections.
Integration tests:
# Test 1: Successful command execution
ssh -v ssm-test true && echo "ok"
# Test 2: Failed command execution (expect non-zero exit)
ssh -v ssm-test false || echo "ok"
# Test 3: Long-running command
ssh -v ssm-test "sleep 5" && echo "ok"Releases are automated via GitHub Actions (.github/workflows/release.yml):
git tag v1.x.x
git push origin v1.x.x- Default log level:
ERROR(to minimize noise in SSH sessions) - Enable debug:
export SSM_SSH_CONNECT_DEBUG=1 - Log rotation: Files >1MB are auto-deleted on next run
- All logs in
~/.ssm-ssh-connect/ssm-ssh-connect.log
SIGHUPis ignored (SSH may send during connection setup)SIGINT/SIGTERMtrigger graceful shutdown- Important for proper cleanup of sessions
- Reads from
~/.ssh/id_rsa.pubonly (hardcoded) - Key pushed via EC2 Instance Connect API
- Valid for 60 seconds on target instance
- Lock file prevents race conditions
- AWS SDK errors logged via slog but don't always exit
- Critical errors (instance not found, plugin missing) cause exit(1)
- Designed to fail gracefully for SSH's benefit
- AWS SDK v2 for Go (EC2, SSM, EC2 Instance Connect)
- Requires
session-manager-pluginbinary installed separately - Reads AWS profiles from standard AWS config files
Expected to be invoked via SSH ProxyCommand:
Host prd-* qa-* dev-*
User ubuntu
ProxyCommand /path/to/ssm-ssh-connect <aws-profile> %h %r
Where %h = hostname (instance name), %r = remote user