Version: 1.0.8
This project provides a Python-based Tunnel class for secure SSH connections and file transfers, integrated with a FastMCP server (tunnel_manager_mcp.py) to expose these capabilities as tools for AI-driven workflows. The implementation supports both standard SSH (e.g., for local networks) and Teleport's secure access platform, leveraging the paramiko library for SSH operations.
- Purpose: Facilitates secure SSH connections, file transfers, and key management for single or multiple hosts.
- Key Functionality:
- Run Remote Commands: Execute shell commands on a remote host and retrieve output.
- File Upload/Download: Transfer files to/from a single host or all hosts in an inventory group using SFTP.
- Passwordless SSH Setup: Configure key-based authentication for secure, passwordless access, with support for RSA and Ed25519 key types.
- SSH Config Management: Copy local SSH config files to remote hosts.
- Key Rotation: Generate and deploy new SSH key pairs (RSA or Ed25519), updating
authorized_keys. - Inventory Support: Operate on multiple hosts defined in an Ansible-style YAML inventory, with group targeting (e.g.,
all,homelab,poweredge). - Teleport Support: Seamlessly integrates with Teleport's certificate-based authentication and proxying.
- Configuration Flexibility: Loads SSH settings from
~/.ssh/configby default, with optional overrides for username, password, identity files, certificates, and proxy commands. - Logging: Optional file-based logging for debugging and auditing.
- Parallel Execution: Support for parallel operations across multiple hosts with configurable thread limits.
- Key Type Support: Explicit support for both RSA and Ed25519 keys in authentication, generation, and rotation for enhanced security and compatibility.
- Purpose: Exposes
Tunnelclass functionality as a FastMCP server, enabling AI tools to perform remote operations programmatically. - Tools Provided:
run_command_on_remote_host: Runs a shell command on a single remote host.send_file_to_remote_host: Uploads a file to a single remote host via SFTP.receive_file_from_remote_host: Downloads a file from a single remote host via SFTP.check_ssh_server: Checks if the SSH server is running and configured for key-based authentication.test_key_auth: Tests key-based authentication for a host.setup_passwordless_ssh: Sets up passwordless SSH for a single host.copy_ssh_config: Copies an SSH config file to a single remote host.rotate_ssh_key: Rotates SSH keys for a single host.remove_host_key: Removes a host’s key from the localknown_hostsfile.configure_key_auth_on_inventory: Sets up passwordless SSH for all hosts in an inventory group.run_command_on_inventory: Runs a command on all hosts in an inventory group.copy_ssh_config_on_inventory: Copies an SSH config file to all hosts in an inventory group.rotate_ssh_key_on_inventory: Rotates SSH keys for all hosts in an inventory group.send_file_to_inventory: Uploads a file to all hosts in an inventory group via SFTP.receive_file_from_inventory: Downloads a file from all hosts in an inventory group via SFTP.
- Transport Options: Supports
stdio(for local scripting) andhttp(for networked access) transport modes. - Progress Reporting: Integrates with FastMCP's
Contextfor progress updates during operations. - Logging: Comprehensive logging to a file (
tunnel_mcp.logby default) or a user-specified file.
Usage:
| Short Flag | Long Flag | Description | Required | Default Value |
|---|---|---|---|---|
| -h | --help | Show usage for the script | No | None |
| --log-file | Log to specified file (default: console output) | No | Console | |
| setup-all | Setup passwordless SSH for all hosts in inventory | Yes* | None | |
| --inventory | YAML inventory path | Yes | None | |
| --shared-key-path | Path to shared private key | No | ~/.ssh/id_shared | |
| --key-type | Key type (rsa or ed25519) | No | ed25519 | |
| --group | Inventory group to target | No | all | |
| --parallel | Run operation in parallel | No | False | |
| --max-threads | Max threads for parallel execution | No | 5 | |
| run-command | Run a shell command on all hosts in inventory | Yes* | None | |
| --remote-command | Shell command to run | Yes | None | |
| copy-config | Copy SSH config to all hosts in inventory | Yes* | None | |
| --local-config-path | Local SSH config path | Yes | None | |
| --remote-config-path | Remote path for SSH config | No | ~/.ssh/config | |
| rotate-key | Rotate SSH keys for all hosts in inventory | Yes* | None | |
| --key-prefix | Prefix for new key paths (appends hostname) | No | ~/.ssh/id_ | |
| --key-type | Key type (rsa or ed25519) | No | ed25519 | |
| send-file | Upload a file to all hosts in inventory | Yes* | None | |
| --local-path | Local file path to upload | Yes | None | |
| --remote-path | Remote destination path | Yes | None | |
| receive-file | Download a file from all hosts in inventory | Yes* | None | |
| --remote-path | Remote file path to download | Yes | None | |
| --local-path-prefix | Local directory path prefix to save files | Yes | None |
One of the commands (setup-all, run-command, copy-config, rotate-key, send-file, receive-file) must be specified as the first argument to tunnel_manager.py. Each command has required arguments that must be specified with flags:
setup-all: Requires--inventory.run-command: Requires--inventoryand--remote-command.copy-config: Requires--inventoryand--local-config-path.rotate-key: Requires--inventory.send-file: Requires--inventory,--local-path, and--remote-path.receive-file: Requires--inventory,--remote-path, and--local-path-prefix.
- Ensure
ansible_hostvalues ininventory.ymlare resolvable IPs or hostnames. - Update
ansible_ssh_private_key_filein the inventory after runningrotate-key. - Use
--log-filefor file-based logging or omit for console output. - The
--paralleloption speeds up operations but may overload resources; adjust--max-threadsas needed. - The
receive-filecommand saves files tolocal_path_prefix/<hostname>/<filename>to preserve original filenames and avoid conflicts. - Ed25519 keys are recommended for better security and performance over RSA, but RSA is supported for compatibility with older systems.
Set up passwordless SSH for hosts in the inventory, distributing a shared key. Use --key-type to specify RSA or Ed25519 (default: ed25519).
- Target
allgroup (sequential, Ed25519):tunnel-manager setup-all --inventory inventory.yml --shared-key-path ~/.ssh/id_shared --key-type ed25519 - Target
homelabgroup (parallel, 3 threads, RSA):tunnel-manager setup-all --inventory inventory.yml --shared-key-path ~/.ssh/id_shared_rsa --key-type rsa --group homelab --parallel --max-threads 3 - Target
poweredgegroup (sequential, Ed25519):tunnel-manager --log-file setup_poweredge.log setup-all --inventory inventory.yml --shared-key-path ~/.ssh/id_shared --key-type ed25519 --group poweredge
Execute a shell command on all hosts in the specified group.
- Run
uptimeonallgroup (sequential):tunnel-manager run-command --inventory inventory.yml --remote-command "uptime" - Run
df -honhomelabgroup (parallel, 5 threads):tunnel-manager run-command --inventory inventory.yml --remote-command "df -h" --group homelab --parallel --max-threads 5 - Run
whoamionpoweredgegroup (sequential):tunnel-manager run-command --inventory inventory.yml --remote-command "whoami" --group poweredge
Copy a local SSH config file to the remote hosts’ ~/.ssh/config.
- Copy to
allgroup (sequential):tunnel-manager copy-config --inventory inventory.yml --local-config-path ~/.ssh/config - Copy to
homelabgroup (parallel, 4 threads):tunnel-manager copy-config --inventory inventory.yml --local-config-path ~/.ssh/config --group homelab --parallel --max-threads 4 - Copy to
poweredgegroup with custom remote path:tunnel-manager --log-file copy_config.log copy-config --inventory inventory.yml --local-config-path ~/.ssh/config --remote-config-path ~/.ssh/custom_config --group poweredge
Rotate SSH keys for hosts, generating new keys with a prefix. Use --key-type to specify RSA or Ed25519 (default: ed25519).
- Rotate keys for
allgroup (sequential, Ed25519):tunnel-manager rotate-key --inventory inventory.yml --key-prefix ~/.ssh/id_ --key-type ed25519 - Rotate keys for
homelabgroup (parallel, 3 threads, RSA):tunnel-manager rotate-key --inventory inventory.yml --key-prefix ~/.ssh/id_rsa_ --key-type rsa --group homelab --parallel --max-threads 3 - Rotate keys for
poweredgegroup (sequential, Ed25519):tunnel-manager --log-file rotate.log rotate-key --inventory inventory.yml --key-prefix ~/.ssh/id_ --key-type ed25519 --group poweredge
Upload a local file to all hosts in the specified group.
- Upload to
allgroup (sequential):tunnel-manager send-file --inventory inventory.yml --local-path ./myfile.txt --remote-path /home/user/myfile.txt
- Upload to
homelabgroup (parallel, 3 threads):tunnel-manager send-file --inventory inventory.yml --local-path ./myfile.txt --remote-path /home/user/myfile.txt --group homelab --parallel --max-threads 3
- Upload to
poweredgegroup (sequential):tunnel-manager --log-file upload_poweredge.log send-file --inventory inventory.yml --local-path ./myfile.txt --remote-path /home/user/myfile.txt --group poweredge
Download a file from all hosts in the specified group, saving to host-specific subdirectories (e.g., downloads/R510/myfile.txt).
- Download from
allgroup (sequential):tunnel-manager receive-file --inventory inventory.yml --remote-path /home/user/myfile.txt --local-path-prefix ./downloads
- Download from
homelabgroup (parallel, 3 threads):tunnel-manager receive-file --inventory inventory.yml --remote-path /home/user/myfile.txt --local-path-prefix ./downloads --group homelab --parallel --max-threads 3
- Download from
poweredgegroup (sequential):tunnel-manager --log-file download_poweredge.log receive-file --inventory inventory.yml --remote-path /home/user/myfile.txt --local-path-prefix ./downloads --group poweredge
Inventory File Example (inventory.yml):
all:
hosts:
r510:
ansible_host: 192.168.1.10
ansible_user: admin
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
r710:
ansible_host: 192.168.1.11
ansible_user: admin
ansible_ssh_pass: mypassword
gr1080:
ansible_host: 192.168.1.14
ansible_user: admin
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
homelab:
hosts:
r510:
ansible_host: 192.168.1.10
ansible_user: admin
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
r710:
ansible_host: 192.168.1.11
ansible_user: admin
ansible_ssh_pass: mypassword
gr1080:
ansible_host: 192.168.1.14
ansible_user: admin
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
poweredge:
hosts:
r510:
ansible_host: 192.168.1.10
ansible_user: admin
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
r710:
ansible_host: 192.168.1.11
ansible_user: admin
ansible_ssh_pass: mypasswordReplace IPs, usernames, and passwords with your actual values.
| Short Flag | Long Flag | Description |
|---|---|---|
| -h | --help | Display help information |
| -t | --transport | Transport method: 'stdio', 'http', or 'sse' [legacy] (default: stdio) |
| -s | --host | Host address for HTTP transport (default: 0.0.0.0) |
| -p | --port | Port number for HTTP transport (default: 8000) |
| --auth-type | Authentication type: 'none', 'static', 'jwt', 'oauth-proxy', 'oidc-proxy', 'remote-oauth' (default: none) | |
| --token-jwks-uri | JWKS URI for JWT verification | |
| --token-issuer | Issuer for JWT verification | |
| --token-audience | Audience for JWT verification | |
| --oauth-upstream-auth-endpoint | Upstream authorization endpoint for OAuth Proxy | |
| --oauth-upstream-token-endpoint | Upstream token endpoint for OAuth Proxy | |
| --oauth-upstream-client-id | Upstream client ID for OAuth Proxy | |
| --oauth-upstream-client-secret | Upstream client secret for OAuth Proxy | |
| --oauth-base-url | Base URL for OAuth Proxy | |
| --oidc-config-url | OIDC configuration URL | |
| --oidc-client-id | OIDC client ID | |
| --oidc-client-secret | OIDC client secret | |
| --oidc-base-url | Base URL for OIDC Proxy | |
| --remote-auth-servers | Comma-separated list of authorization servers for Remote OAuth | |
| --remote-base-url | Base URL for Remote OAuth | |
| --allowed-client-redirect-uris | Comma-separated list of allowed client redirect URIs | |
| --eunomia-type | Eunomia authorization type: 'none', 'embedded', 'remote' (default: none) | |
| --eunomia-policy-file | Policy file for embedded Eunomia (default: mcp_policies.json) | |
| --eunomia-remote-url | URL for remote Eunomia server |
The MCP Server can be run in two modes: stdio (for local testing) or http (for networked access). To start the server, use the following commands:
tunnel-manager-mcp --transport "stdio"tunnel-manager-mcp --transport "http" --host "0.0.0.0" --port "8000"The Tunnel class can be used standalone for SSH operations. Examples:
from tunnel_manager.tunnel_manager import Tunnel
# Initialize with a remote host (assumes ~/.ssh/config or explicit params)
tunnel = Tunnel(
remote_host="192.168.1.10",
username="admin",
password="mypassword",
identity_file="/path/to/id_rsa",
certificate_file="/path/to/cert", # Optional for Teleport
proxy_command="tsh proxy ssh %h", # Optional for Teleport
ssh_config_file="~/.ssh/config",
)
# Connect and run a command
tunnel.connect()
out, err = tunnel.run_command("ls -la /tmp")
print(f"Output: {out}\nError: {err}")
# Upload a file
tunnel.send_file("/local/file.txt", "/remote/file.txt")
# Download a file
tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
# Setup passwordless SSH with RSA
tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_rsa", key_type="rsa")
# Copy SSH config
tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
# Rotate SSH key with RSA
tunnel.rotate_ssh_key("/path/to/new_rsa_key", key_type="rsa")
# Close the connection
tunnel.close()from tunnel_manager.tunnel_manager import Tunnel
# Initialize with a remote host (assumes ~/.ssh/config or explicit params)
tunnel = Tunnel(
remote_host="192.168.1.10",
username="admin",
password="mypassword",
identity_file="/path/to/id_ed25519",
certificate_file="/path/to/cert", # Optional for Teleport
proxy_command="tsh proxy ssh %h", # Optional for Teleport
ssh_config_file="~/.ssh/config",
)
# Connect and run a command
tunnel.connect()
out, err = tunnel.run_command("ls -la /tmp")
print(f"Output: {out}\nError: {err}")
# Upload a file
tunnel.send_file("/local/file.txt", "/remote/file.txt")
# Download a file
tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
# Setup passwordless SSH with Ed25519
tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_ed25519", key_type="ed25519")
# Copy SSH config
tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
# Rotate SSH key with Ed25519
tunnel.rotate_ssh_key("/path/to/new_ed25519_key", key_type="ed25519")
# Close the connection
tunnel.close()The MCP server can be deployed using Docker, with configurable authentication, middleware, and Eunomia authorization.
docker pull knucklessg1/tunnel-manager:latest
docker run -d \
--name tunnel-manager-mcp \
-p 8004:8004 \
-e HOST=0.0.0.0 \
-e PORT=8004 \
-e TRANSPORT=http \
-e AUTH_TYPE=none \
-e EUNOMIA_TYPE=none \
knucklessg1/tunnel-manager:latestFor advanced authentication (e.g., JWT, OAuth Proxy, OIDC Proxy, Remote OAuth) or Eunomia, add the relevant environment variables:
docker run -d \
--name tunnel-manager-mcp \
-p 8004:8004 \
-e HOST=0.0.0.0 \
-e PORT=8004 \
-e TRANSPORT=http \
-e AUTH_TYPE=oidc-proxy \
-e OIDC_CONFIG_URL=https://provider.com/.well-known/openid-configuration \
-e OIDC_CLIENT_ID=your-client-id \
-e OIDC_CLIENT_SECRET=your-client-secret \
-e OIDC_BASE_URL=https://your-server.com \
-e ALLOWED_CLIENT_REDIRECT_URIS=http://localhost:*,https://*.example.com/* \
-e EUNOMIA_TYPE=embedded \
-e EUNOMIA_POLICY_FILE=/app/mcp_policies.json \
knucklessg1/tunnel-manager:latestCreate a docker-compose.yml file:
services:
tunnel-manager-mcp:
image: knucklessg1/tunnel-manager:latest
environment:
- HOST=0.0.0.0
- PORT=8004
- TRANSPORT=http
- AUTH_TYPE=none
- EUNOMIA_TYPE=none
ports:
- 8004:8004For advanced setups with authentication and Eunomia:
services:
tunnel-manager-mcp:
image: knucklessg1/tunnel-manager:latest
environment:
- HOST=0.0.0.0
- PORT=8004
- TRANSPORT=http
- AUTH_TYPE=oidc-proxy
- OIDC_CONFIG_URL=https://provider.com/.well-known/openid-configuration
- OIDC_CLIENT_ID=your-client-id
- OIDC_CLIENT_SECRET=your-client-secret
- OIDC_BASE_URL=https://your-server.com
- ALLOWED_CLIENT_REDIRECT_URIS=http://localhost:*,https://*.example.com/*
- EUNOMIA_TYPE=embedded
- EUNOMIA_POLICY_FILE=/app/mcp_policies.json
ports:
- 8004:8004
volumes:
- ./mcp_policies.json:/app/mcp_policies.jsonRun the service:
docker-compose up -d{
"mcpServers": {
"tunnel_manager": {
"command": "uv",
"args": [
"run",
"--with",
"tunnel-manager",
"tunnel_manager_mcp"
],
"env": {
"TUNNEL_REMOTE_HOST": "192.168.1.12", // Optional
"TUNNEL_USERNAME": "admin", // Optional
"TUNNEL_PASSWORD": "", // Optional
"TUNNEL_REMOTE_PORT": "22", // Optional
"TUNNEL_IDENTITY_FILE": "", // Optional
"TUNNEL_INVENTORY": "~/inventory.yaml", // Optional
"TUNNEL_INVENTORY_GROUP": "all", // Optional
"TUNNEL_PARALLEL": "true", // Optional
"TUNNEL_CERTIFICATE": "", // Optional
"TUNNEL_PROXY_COMMAND": "", // Optional
"TUNNEL_LOG_FILE": "~/tunnel_log.txt", // Optional
"TUNNEL_MAX_THREADS": "6" // Optional
},
"timeout": 200000
}
}
}Installation Instructions:
python -m pip install tunnel-manageror
uv pip install --upgrade tunnel-manager