A security-focused SSH connection monitoring tool for bastion/jump hosts. Tracks SSH connections and correlates users, authentication methods (password/key/cert), source IPs, and active connection counts.
Key components built:
proxy/server.go- SSH proxy that extracts authentication detailsmonitor/tracker.go- Connection tracking with concurrent-safe countersmonitor/logparser.go- Parses sshd auth.log for cert details (key ID, serial, CA)monitor/watcher.go- Watches log files or journaldmonitor/metrics.go- HTTP server with dashboard, Prometheus, and JSON endpoints
- Real-time monitoring of SSH connections through a bastion host
- Multi-auth support: Tracks password, public key, and certificate authentication
- Certificate awareness: Extracts key IDs and principals from SSH certificates
- Connection correlation: Links users to source IPs and tracks connection counts
- Multiple export formats:
- Web dashboard (HTML with auto-refresh)
- Prometheus metrics endpoint
- JSON API
- Low overhead: Minimal performance impact on SSH traffic
- Concurrent-safe: Proper locking for high-traffic environments
SSHadow operates as an SSH proxy that sits between clients and your actual SSH service:
Client → SSHadow (port 2222) → actual sshd (port 22)
↓
Metrics Server (port 9090)
The proxy performs the SSH handshake to capture authentication metadata, then forwards the connection to the real SSH service.
ssh-keygen -t ed25519 -f ssh_host_key -N ""go build -o SSHadow .
./SSHadow -hostkey ssh_host_key -listen :2222 -target localhost:22 -metrics :9090Open http://localhost:9090 in your browser to see active connections.
From another terminal:
ssh -p 2222 username@localhostgit clone <repo>
cd SSHadow
go mod download
go build -o SSHadow .-listen string
Address to listen on for SSH connections (default ":2222")
-target string
Address of the target SSH server (default "localhost:22")
-hostkey string
Path to SSH host private key (required)
-metrics string
Address for metrics HTTP server (default ":9090")
-mode string
Operating mode: "proxy" or "metrics-only" (default "proxy")
systemd service example:
[Unit]
Description=SSH Connection Monitor
After=network.target
[Service]
Type=simple
User=SSHadow
ExecStart=/usr/local/bin/SSHadow -hostkey /etc/SSHadow/ssh_host_key -listen :2222 -target localhost:22 -metrics :9090
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.targetSecurity considerations:
- Run as non-root user (use systemd socket activation for privileged ports)
- Restrict file permissions on host key:
chmod 600 ssh_host_key - Use firewall rules to limit access to metrics endpoint
- Consider TLS termination for metrics endpoint in production
Human-readable HTML dashboard with auto-refresh:
- Active connection count
- DoS Detection Views: Aggregated by IP, user, and key
- Per-user statistics
- Auth method breakdown
- Visual alerts when thresholds exceeded
DoS Detection Metrics (pre-aggregated for easy alerting):
# Single-host attack detection - connections per source IP
ssh_connections_by_ip{source_ip="10.0.0.99"} 8
# Distributed attack detection - connections per user
ssh_connections_by_user{username="deploy-bot"} 6
# Compromised key detection - connections per key
ssh_connections_by_key{key_id="SHA256:STOLEN-KEY-xyz789"} 6
Detailed Metrics:
ssh_active_connections 15
ssh_user_active_connections{username="alice",source_ip="192.168.1.100",auth_type="cert",key_id="alice-laptop"} 2
ssh_user_total_connections{username="alice",source_ip="192.168.1.100",auth_type="cert",key_id="alice-laptop"} 47
{
"alice:prod-key-1": {
"SourceIP": "192.168.1.100",
"Username": "alice",
"AuthType": "cert",
"KeyID": "prod-key-1",
"Principals": ["alice", "developers"],
"ActiveCount": 2,
"TotalCount": 15,
"FirstSeen": "2026-01-30T10:00:00Z",
"LastSeen": "2026-01-30T14:30:00Z"
}
}Move your existing sshd to a different port and run SSHadow on port 22:
# In /etc/ssh/sshd_config
Port 2222
# Restart sshd
systemctl restart sshd
# Run SSHadow on port 22
./SSHadow -hostkey /etc/SSHadow/host_key -listen :22 -target localhost:2222Pros: No client configuration changes Cons: Requires modifying sshd configuration
Run SSHadow on a different port:
./SSHadow -hostkey host_key -listen :2222 -target localhost:22Clients connect to port 2222 instead of 22.
Pros: No changes to existing sshd Cons: Clients need to specify port
If you want to monitor connections through other means (e.g., parsing auth logs), you can run just the metrics server:
./SSHadow -mode metrics-only -metrics :9090Then feed connection data to the tracker via your own integration.
Tracked by username + source IP
Tracked by username + key fingerprint (SHA256)
Tracked by username + certificate key ID
- Extracts principals from certificate
- Shows cert key ID in dashboard
- Ideal for tracking which cert authorized the connection
Run unit tests:
go test ./...Run with coverage:
go test -cover ./...Run integration tests with verbose output to see sample metrics for all authentication methods:
# See all auth methods with sample output
go test -v -run TestIntegrationAllAuthMethods ./monitor/
# See password auth output only
go test -v -run TestIntegrationPasswordAuth ./monitor/
# See public key auth output only
go test -v -run TestIntegrationPublicKeyAuth ./monitor/
# See certificate auth output only
go test -v -run TestIntegrationCertAuth ./monitor/
# See mixed auth with connect/disconnect simulation
go test -v -run TestIntegrationMixedAuthWithDisconnects ./monitor/
# See full flow from log parsing to output
go test -v -run TestIntegrationLogParsingToOutput ./monitor/Run an interactive demo that starts a real metrics server and opens your browser:
# Basic demo with pre-populated DoS scenario data
go test -v -tags=livedemo -run TestLiveDemo ./monitor/
# Attack simulation - watch the dashboard update in real-time
go test -v -tags=livedemo -run TestLiveDemoSimulateAttack ./monitor/The live demo will:
- Start a metrics server on http://localhost:9099
- Open your browser automatically
- Show the dashboard with DoS detection views
- The attack simulation adds connections in real-time so you can watch alerts appear
Press Ctrl+C to stop the demo.
JSON Output:
{
"charlie:172.16.0.1": {
"SourceIP": "172.16.0.1",
"Username": "charlie",
"AuthType": "password",
"KeyType": "",
"KeyID": "",
"Fingerprint": "",
"Principals": null,
"CAFingerprint": "",
"ActiveCount": 1,
"TotalCount": 1,
"FirstSeen": "2026-01-30T10:17:30Z",
"LastSeen": "2026-01-30T10:17:30Z"
}
}Prometheus Output:
ssh_user_active_connections{username="charlie",source_ip="172.16.0.1",auth_type="password",key_id=""} 1
ssh_user_total_connections{username="charlie",source_ip="172.16.0.1",auth_type="password",key_id=""} 1
JSON Output:
{
"bob:SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8": {
"SourceIP": "10.0.0.50",
"Username": "bob",
"AuthType": "publickey",
"KeyType": "RSA",
"KeyID": "SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8",
"Fingerprint": "SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8",
"Principals": null,
"CAFingerprint": "",
"ActiveCount": 1,
"TotalCount": 1,
"FirstSeen": "2026-01-30T10:16:00Z",
"LastSeen": "2026-01-30T10:16:00Z"
}
}Prometheus Output:
ssh_user_active_connections{username="bob",source_ip="10.0.0.50",auth_type="publickey",key_id="SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8"} 1
ssh_user_total_connections{username="bob",source_ip="10.0.0.50",auth_type="publickey",key_id="SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8"} 1
JSON Output:
{
"alice:alice-laptop": {
"SourceIP": "192.168.1.100",
"Username": "alice",
"AuthType": "cert",
"KeyType": "ED25519",
"KeyID": "alice-laptop",
"Fingerprint": "SHA256:abc123def456ghi789jkl012mno345pqr678stu901vwx",
"Principals": null,
"CAFingerprint": "SHA256:CORPORATE-CA-2024-abc123def456ghi789jkl012",
"ActiveCount": 2,
"TotalCount": 2,
"FirstSeen": "2026-01-30T10:15:23Z",
"LastSeen": "2026-01-30T10:45:00Z"
}
}Prometheus Output:
ssh_user_active_connections{username="alice",source_ip="192.168.1.100",auth_type="cert",key_id="alice-laptop"} 2
ssh_user_total_connections{username="alice",source_ip="192.168.1.100",auth_type="cert",key_id="alice-laptop"} 2
╔══════════════════════════════════════════════════════════════════════════════════╗
║ SSH CONNECTION MONITOR ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ Active Connections: 7 ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ USER │ SOURCE IP │ AUTH │ KEY ID/FINGERPRINT │ ACT │ TOT ║
╠════════════╪════════════════╪═══════════╪═══════════════════════════╪═════╪══════╣
║ alice │ 192.168.1.100 │ cert │ alice-laptop │ 2 │ 2 ║
║ bob │ 10.0.0.50 │ publickey │ SHA256:nThbg6kXUpJWGl... │ 1 │ 1 ║
║ eve │ 192.168.50.10 │ publickey │ SHA256:uH7kzJxNShdLrq... │ 1 │ 1 ║
║ charlie │ 172.16.0.1 │ password │ - │ 1 │ 1 ║
║ david │ 10.0.0.25 │ password │ - │ 1 │ 1 ║
║ frank │ 10.100.200.5 │ cert │ prod-deploy-key │ 1 │ 1 ║
╚══════════════════════════════════════════════════════════════════════════════════╝
Add to prometheus.yml:
scrape_configs:
- job_name: 'SSHadow'
static_configs:
- targets: ['bastion.example.com:9090']DoS Detection Alerts (using pre-aggregated metrics):
groups:
- name: ssh_dos_detection
rules:
# Scenario 1: Single IP opening too many connections
- alert: SSHSingleHostDoS
expr: ssh_connections_by_ip > 10
for: 1m
labels:
severity: critical
annotations:
summary: "Potential DoS: {{ $labels.source_ip }} has {{ $value }} connections"
description: "Single host opening excessive SSH connections - possible DoS attack"
# Scenario 2: Same user from many IPs (distributed attack)
- alert: SSHDistributedAttackByUser
expr: ssh_connections_by_user > 10
for: 2m
labels:
severity: critical
annotations:
summary: "Potential distributed attack: user '{{ $labels.username }}' has {{ $value }} connections"
description: "Same username connecting from multiple sources - possible credential compromise"
# Scenario 3: Same key used from many IPs (compromised key)
- alert: SSHCompromisedKey
expr: ssh_connections_by_key > 5
for: 1m
labels:
severity: critical
annotations:
summary: "Potential compromised key: {{ $labels.key_id }} used {{ $value }} times"
description: "Same SSH key connecting from multiple sources - possible key theft"
# General high connection warning
- alert: HighSSHConnections
expr: ssh_active_connections > 50
for: 5m
labels:
severity: warning
annotations:
summary: "High total SSH connections: {{ $value }}"Export JSON and feed to your SIEM:
curl http://localhost:9090/metrics/json | jq '.' | logger -t SSHadow- Password forwarding: The proxy captures passwords to forward them. In high-security environments, consider key/cert-only auth.
- Host key verification: The proxy presents its own host key. Clients will need to accept this key.
- Connection multiplexing: SSH ControlMaster creates multiple logical sessions over one TCP connection. The tracker counts TCP connections.
- Performance: Adds minimal latency (typically <1ms) but all SSH traffic flows through the proxy.
- High availability: For HA setups, run multiple instances behind a load balancer. Connection stats won't be aggregated across instances.
- Session recording integration
- Geo-IP lookup for source addresses
- Connection time tracking and idle detection
- Configurable alerting thresholds
- Database backend for historical data
- Integration with PAM/LDAP for user enrichment
- TLS for metrics endpoint
This tool is designed for security monitoring but has some important considerations:
- Credential exposure: The proxy sees passwords during authentication. Ensure the host running SSHadow is properly secured.
- Certificate validation: Currently uses
InsecureIgnoreHostKey()for target connections. In production, implement proper host key validation. - Metrics endpoint: Contains sensitive information (usernames, IPs). Restrict access appropriately.
MIT
Contributions welcome! Please:
- Add unit tests for new features
- Follow Go best practices
- Update documentation
- Keep it simple and focused
To release the software please adhere to the Semver versioning scheme which is denoted in the example below. New releases should include the binary in the release.
make release # builds with git tag as version
VERSION=v1.0.0 make release # builds with explicit version 