pytun is a production-grade SSH reverse tunnel manager that allows on-premises services to be securely accessed from cloud instances without VPN. It provides automatic health monitoring, alerting, and self-healing capabilities.
Connect cloud applications to local network services (LDAP, databases, SMTP) that don't have public network access:
Cloud Application → SSH Server → pytun Connector → Local Service
(SaaS) (Bastion) (Your Network) (LDAP/DB/etc)
Example: Your InvGate cloud instance needs to query your on-premises Active Directory server without exposing AD to the internet.
- 🔄 Automatic Restart - Monitors tunnels every 30 seconds, auto-restarts on failure
- 🔐 SSH Security - Key-based authentication with host verification (no passwords)
- 📊 HTTP Monitoring - Built-in status API on port 9999
- 📧 Alerting - Email (SMTP) and HTTP POST alerts with rate limiting
- 🔍 Multi-Tunnel - Manage multiple tunnels from single connector instance
- 💾 Logging - Daily rotating logs with 30-day retention
- 🔒 Device Authorization - Optional MAC address + cryptographic signature
- 🪟 Windows Service - Runs as Windows service (via PyInstaller + Shawl)
- Python 3.6+ (recommended: 3.10+)
- SSH access to a bastion/jump host
- Passwordless SSH key pair
# 1. Clone repository
git clone https://github.com/InvGate/pytun.git
cd pytun
# 2. Install dependencies
pip install -r requirements.txt
# 3. Create connector.ini (see Configuration section)
# Edit connector.ini with your settings (no example file - create from scratch)
# 4. Create tunnel configuration
mkdir -p configs
# Create configs/my-tunnel.ini (see example below)
# 5. Run connector
python pytun.py[pytun]
# Required
tunnel_manager_id=my-connector-001 # Unique identifier for this connector
# Tunnel Configuration
tunnel_dirs=./configs # Directory containing tunnel .ini files
# Logging
log_level=DEBUG # DEBUG | INFO | WARNING | ERROR
log_to_console=True # Output to console
log_path=./logs # Log directory
# HTTP Inspection Server
inspection_port=9999 # Status API port
inspection_localhost_only=True # Security: localhost only (recommended)
# Optional: Email Alerts (SMTP)
smtp_hostname=smtp.example.com # SMTP server
smtp_port=587 # SMTP port
smtp_security=tls # none | tls | ssl
smtp_login[email protected] # SMTP username
smtp_password=your-password # SMTP password (plain text)
smtp_from[email protected] # From address
smtp_to[email protected] # Alert recipient
# Optional: HTTP POST Alerts
http_url=https://api.example.com/webhook
http_user=webhook-user
http_password=webhook-password[tunnel]
# Tunnel Name (optional, for logging/alerts)
tunnel_name=LDAP Service Tunnel
# SSH Server (Cloud Bastion/Jump Host)
server_host=bastion.example.com # Required: SSH server hostname/IP
server_port=22 # SSH port (default: 22)
server_key=~/.ssh/known_hosts # Server's public key file
# Port Forwarding
port=10389 # Port to listen on SSH server (bastion)
# Local Service (On-Premises)
remote_host=ldap.local.network # Required: Local service hostname/IP
remote_port=389 # Required: Local service port
# SSH Authentication
username=tunnel-user # Required: SSH username
keyfile=~/.ssh/pytun_key # Required: Path to private SSH key (passwordless!)
# Health Monitoring
keep_alive_time=30 # Seconds between keepalive checks
# Logging (optional, inherits from main config if not set)
log_level=DEBUG
log_to_console=False┌──────────────────────────────────────────────────────────────────┐
│ Example: Expose local LDAP (389) on cloud server port 10389 │
└──────────────────────────────────────────────────────────────────┘
Cloud App connects to: bastion.example.com:10389
↓
[SSH Server / Bastion]
↓ (reverse SSH tunnel)
[pytun Connector]
↓ (local network)
[LDAP Server: ldap.local.network:389]
Data flows bidirectionally through encrypted SSH tunnel
# Standard run (uses ./connector.ini)
python pytun.py
# Custom configuration file
python pytun.py --config_ini /path/to/connector.ini
# Show version
python pytun.py --version# Test service connectivity (checks if local services are reachable)
python pytun.py --test_connections
# Test SSH tunnel establishment (verifies SSH connection to bastion)
python pytun.py --test_tunnels
# Test SMTP email alerts
python pytun.py --test_smtp
# Test HTTP POST alerts
python pytun.py --test_http
# Run all tests (comprehensive diagnostic)
python pytun.py --test_all# View logs (real-time)
tail -f logs/main_connector.log # Main connector log
tail -f logs/my-tunnel-name.log # Per-tunnel logs
# HTTP Status API (while connector is running)
curl http://localhost:9999/ # Health check
curl http://localhost:9999/status # Tunnel status + service health
curl http://localhost:9999/configs -o configs.zip # Download configs
curl http://localhost:9999/logs -o logs.zip # Download logsStatus Response Example:
{
"created_at": 1612345678.123,
"mac_address": "00:11:22:33:44:55",
"status_data": {
"my-tunnel": {
"started_times": 2,
"last_start": 1612345678.123
}
},
"my-tunnel": {
"remote_host": "ldap.local.network",
"remote_port": 389,
"status": true
}
}Important: pytun requires passwordless SSH keys.
# Generate 4096-bit RSA key (no passphrase)
ssh-keygen -t rsa -b 4096 -f ~/.ssh/pytun_key -N ""
# Two files created:
# ~/.ssh/pytun_key (private key - keep secure!)
# ~/.ssh/pytun_key.pub (public key)# Copy public key to SSH server's authorized_keys
ssh-copy-id -i ~/.ssh/pytun_key.pub [email protected]
# Or manually:
cat ~/.ssh/pytun_key.pub | ssh [email protected] \
"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"# Verify passwordless authentication works
ssh -i ~/.ssh/pytun_key [email protected]
# If prompted for password, key setup failed# Add server to known_hosts
ssh-keyscan bastion.example.com >> ~/.ssh/known_hosts
# Use this file as server_key in tunnel config# Install PyInstaller (if not already installed)
pip install pyinstaller
# Build executable
pyinstaller pytun.spec
# Output: dist/pytun.exe# Using Shawl (Windows service wrapper)
# Download: https://github.com/mtkennerly/shawl
shawl add --name InvGateTunnel -- \
"C:\path\to\pytun.exe" --config_ini "C:\path\to\connector.ini"
# Start service
sc start InvGateTunnel
# Stop service
sc stop InvGateTunnel
# Remove service
shawl remove --name InvGateTunnelpytun.py (Main Process)
├─ Loads configurations
├─ Spawns TunnelProcess for each .ini file (multiprocessing)
├─ Monitors health every 30 seconds
├─ Auto-restarts failed processes
└─ HTTP inspection server (port 9999)
TunnelProcess (Per-Tunnel Child Process)
├─ Establishes SSH connection
├─ Requests reverse port forwarding
├─ Accepts incoming connections
└─ Spawns handler thread per connection (bidirectional relay)
Each tunnel performs three health checks every 30 seconds:
- SSH Keepalive -
send_ignore()at transport level - Transport Active - Verify SSH connection state
- Session Test - Open/close a test SSH session
If any check fails → tunnel marked as failed → process exits → parent restarts
- Email: SMTP with TLS/SSL, rate limited to 1 email per 10 minutes per tunnel
- HTTP POST: Webhook to external monitoring system, no rate limit
- Non-blocking: Alerts sent via
ThreadPoolExecutor, doesn't block tunnel operations
- SSH Key-Based Auth: No passwords, only SSH keys
- Host Key Verification: Rejects unknown SSH servers (prevents MITM)
- SSH Agent Disabled: No automatic key discovery
- Localhost-Only API: Inspection server only on 127.0.0.1 by default
-
Config Files Contain Credentials
- SMTP passwords stored in plain text
- Restrict file permissions:
chmod 600 connector.ini - Consider encrypting config files at rest
-
HTTP
/configsEndpoint Exposes SSH Keys- Returns ZIP with ALL config files including private keys
- Mitigation:
inspection_localhost_only=Trueby default - NEVER set
inspection_localhost_only=Falsein production - Use SSH tunnel for remote access:
ssh -L 9999:localhost:9999 user@connector-host
-
SSH Keys Must Be Passwordless
- Required for unattended operation
- Secure with file permissions:
chmod 400 ~/.ssh/pytun_key
-
Log Files May Contain Sensitive Data
- Set
log_level=INFOin production (not DEBUG) - Secure log directory permissions
- Set
Symptoms: Process exits immediately, no tunnel established
Common Causes:
# Check logs for specific error
tail -f logs/main_connector.log
# Common errors and solutions:
PasswordRequiredException → SSH key has password (must be passwordless)
BadHostKeyException → Server key mismatch (update known_hosts)
AuthenticationException → SSH key not authorized (check authorized_keys)
FileNotFoundError: keyfile → Path to SSH key incorrect
Missing keyfile argument → keyfile not set in tunnel configDebug Steps:
# 1. Test SSH connection manually
ssh -i /path/to/keyfile username@server_host -p server_port
# 2. Test tunnel establishment
python pytun.py --test_tunnels
# 3. Test service connectivity
python pytun.py --test_connectionsSymptoms: Logs show constant restart loop
Check:
- Local service is running and reachable
remote_hostandremote_portare correct- Firewall allows connection to local service
# Test service connectivity from connector host
nc -zv remote_host remote_port
# Or use telnet
telnet remote_host remote_port
# Check connector logs for pattern
tail -f logs/tunnel-name.log | grep -E "error|exception|down"SMTP Alerts:
# Test SMTP configuration
python pytun.py --test_smtp
# Check rate limiting (10-minute window)
# If alert sent recently, may be suppressed
# Verify SMTP settings in connector.iniHTTP Alerts:
# Test HTTP POST configuration
python pytun.py --test_http
# Test webhook endpoint manually
curl -X POST -u http_user:http_password \
-H "Content-Type: application/json" \
-d '{"test": "alert"}' \
http_urlLinux:
# Fix config file permissions
chmod 600 connector.ini
chmod 600 configs/*.ini
chmod 400 ~/.ssh/pytun_key
chmod 755 logs/
# Check ownership
ls -la connector.iniWindows: Run as Administrator (required by uac_admin=True in PyInstaller spec)
- CLAUDE.md - Technical documentation for developers/AI assistants (root overview)
- tunnel_infra/CLAUDE.md - SSH tunnel infrastructure details
- alerts/CLAUDE.md - Alert system details
- observation/CLAUDE.md - HTTP inspection server details
- TMT (Tunnel Manager Tool) - Flask API for managing connectors and generating installers
- Tunnel Windows Installer - NSIS script to build Windows installer packages
coloredlogs==14.0 # Colored logging output
cryptography # RSA-PSS device authorization (transitive via paramiko)
deckar01-ratelimit==3.0.2 # Rate limiting for alerts
email-validator==1.1.1 # Email address validation
paramiko==3.4.0 # SSH protocol implementation
psutil==5.7.2 # System and process utilities
requests==2.32.4 # HTTP requests for alerts
- Primary: Windows (via PyInstaller executable)
- Supported: Linux (run from source)
- Tested On: Windows, Windows Server, Ubuntu
- SSH Implementation: Paramiko (Python SSH library)
- Process Isolation:
multiprocessing.Process(one process per tunnel) - Concurrency: Daemon threads for connection handling
- I/O Multiplexing:
select.select()for bidirectional relay - Logging:
TimedRotatingFileHandler(daily rotation, 30-day retention)
# Simplified flow (see tunnel_infra/Tunnel.py for details)
1. SSH client connects to bastion server
2. Request reverse port forward: server.port → client
3. Server accepts connections on specified port
4. For each connection:
- SSH server forwards to pytun via SSH tunnel
- pytun spawns handler thread
- Handler connects to local service
- select() relays data bidirectionally
5. Connection closes, thread exits
6. Keepalive checks every 30 secondsVersion: 1.1.17 Maintainer: InvGate - Internal Tools Team Repository: Private (Internal Use Only) Last Updated: 2026