SSH Tunnel Manager is a lightweight, secure, and fully configurable CLI tool for managing SSH port‑forwarding tunnels (local forwarding) across multiple environments. It uses SSH ControlMaster sockets for lifecycle management — no kill or taskkill hacks, no hard‑coded configuration.
Perfect for:
- Developers who need persistent tunnels to staging/dev databases, caches, or message queues.
- DevOps engineers integrating tunnel setup into CI/CD pipelines.
- Teams sharing a common jump‑host without clashing credentials.
- No hard‑coded parameters – all configuration is injected via environment variables.
- Socket‑based lifecycle – each tunnel is controlled via its own
ControlPathsocket (ssh -O check/exit). - Environment‑aware – define as many environments as you want (
dev,stage,prod, …) with a single variable per environment. - Zero process‑killing – graceful shutdown via SSH control commands; no risk of interfering with other SSH sessions.
- Cross‑platform – works on Linux, macOS, Windows (Git Bash, WSL, Cygwin).
- Minimal dependencies – requires only
bash(4.3+) and OpenSSH client. - CI/CD friendly – can be sourced or run non‑interactively with
ssh-agentsupport.
git clone https://github.com/rayyee/ssh-tunnel-manager.git
cd ssh-tunnel-manager
chmod +x tunnel-manager.sh# Connection settings (use either alias OR host+user)
export BASTION_ALIAS="jump-prod" # ~/.ssh/config Host
# OR
export BASTION_HOST="jump.example.com"
export BASTION_USER="admin"
export BASTION_PORT="22" # optional, default 22
# Optional private key
export SSH_KEY="$HOME/.ssh/id_rsa"
# Tunnel rules for each environment (comma‑separated)
export BASTION_PROFILE_dev="3306:10.0.2.10:3306,6379:10.0.2.20:6379"
export BASTION_PROFILE_stage="8848:10.0.3.10:8848,9092:10.0.3.20:9092"./tunnel-manager.sh start dev
./tunnel-manager.sh status stage
./tunnel-manager.sh stop dev
./tunnel-manager.sh restart stageAll configuration is passed through environment variables – no editing of the script.
| Variable | Required | Description |
|---|---|---|
BASTION_ALIAS |
Conditional¹ | Host alias defined in ~/.ssh/config. If set, BASTION_HOST/USER are ignored. |
BASTION_HOST |
Conditional¹ | Jump‑host IP or domain. |
BASTION_USER |
Conditional¹ | SSH username for the jump‑host. |
BASTION_PORT |
No | SSH port (default: 22). |
SSH_KEY |
No | Path to private key. If not set, defaults to ~/.ssh/id_rsa or ssh-agent provided keys. |
BASTION_PROFILE_<ENV> |
Yes | Comma‑separated list of tunnel rules: local_port:remote_host:remote_port. Example: "3306:db.internal:3306,6379:cache.internal:6379" |
¹ Either BASTION_ALIAS or both BASTION_HOST and BASTION_USER must be set.
./tunnel-manager.sh {start|stop|status|restart} <environment>
| Command | Action |
|---|---|
start <env> |
Start all tunnels defined in BASTION_PROFILE_<env>. |
stop <env> |
Gracefully shut down all tunnels in that environment. |
status <env> |
Show the running status of each tunnel (based on socket health). |
restart <env> |
Stop, then start the environment (with a 1‑second pause). |
# Start the 'dev' environment
export BASTION_PROFILE_dev="3306:10.0.2.10:3306,6379:10.0.2.20:6379"
./tunnel-manager.sh start dev
# Check status
./tunnel-manager.sh status dev
# Output:
# ========== 环境 [dev] 状态 ==========
# ● 3306 -> 10.0.2.10:3306 (活跃, socket: /home/user/.ssh/sock.d/jump-prod_3306.sock)
# ● 6379 -> 10.0.2.20:6379 (活跃, socket: /home/user/.ssh/sock.d/jump-prod_6379.sock)
# =======================================
# Stop them
./tunnel-manager.sh stop dev- Socket per tunnel – Each tunnel gets its own
ControlPathsocket under~/.ssh/sock.d/(named<target>_<local_port>.sock). - Start – The script runs
ssh -f -N -L ...withControlMaster=autoandControlPersist=yes, creating a socket. - Stop – It calls
ssh -S <socket> -O exitto gracefully shut down that specific tunnel. - Status – It uses
ssh -S <socket> -O checkto verify the socket is alive.
No process scanning, no kill, no taskkill – pure SSH control.
- No hard‑coded credentials – all secrets are externalised.
- SSH agent integration – if
SSH_KEYis specified, the script will attempt to add it to the agent (with a 1‑hour timeout). - Socket directory –
~/.ssh/sock.d/is created with0700permissions (inherited frommkdir). - Avoids port conflicts – checks socket existence before starting a new tunnel.
The script is designed to run on:
- Linux (any distribution)
- macOS
- Windows via Git Bash, WSL, or Cygwin (requires OpenSSH client in PATH).
The ssh -S option is fully supported by OpenSSH for Windows (since version 8.1).
You can source the script and use it in your pipelines. For example, in a GitHub Actions workflow:
- name: Set up tunnels
run: |
export BASTION_ALIAS="ci-jump"
export BASTION_PROFILE_test="5432:postgres.internal:5432"
./tunnel-manager.sh start test
# run your tests here
./tunnel-manager.sh stop testContributions are welcome! Please follow these steps:
- Fork the repository.
- Create a feature branch (
git checkout -b feature/amazing-feature). - Commit your changes (
git commit -m 'Add some amazing feature'). - Push to the branch (
git push origin feature/amazing-feature). - Open a Pull Request.
Please ensure your code is bash‑compatible (4.3+) and passes basic testing on Linux/macOS/Windows (Git Bash). Add comments for clarity.
Distributed under the MIT License. See LICENSE for more information.
Q: Can I use this with multiple jump‑hosts?
A: Yes – each environment can point to a different jump‑host by setting BASTION_ALIAS/BASTION_HOST accordingly before running the command. You can even wrap the script in a shell function that sets different variables per call.
Q: What if I need to use -J ProxyJump?
A: The script uses plain -L forwarding. If you need a proxy chain, you can set BASTION_ALIAS to a Host that already defines ProxyJump in ~/.ssh/config.
Q: How do I list available environments?
A: The script does not know them in advance – you define them via BASTION_PROFILE_*. To see which ones you have set, use printenv | grep BASTION_PROFILE_.
Q: Why not use autossh?
A: autossh is great for persistence, but this tool focuses on management (start/stop/status) without external monitoring. For production, you can combine it with systemd or cron.
Q: Does this work with MFA or OTP?
A: Yes, as long as you can authenticate non‑interactively (e.g., via ssh-agent with a certificate or using ControlMaster from a previously authenticated session). You may need to pre‑authenticate once manually.
- Inspired by the need for a clean, process‑free SSH tunnel manager.
- Built on OpenSSH's robust
ControlMasterfeature.
Happy Tunneling! 🚇