tunnel-manager 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tunnel-manager might be problematic. Click here for more details.

@@ -8,9 +8,16 @@ import concurrent.futures
8
8
  import yaml
9
9
  import asyncio
10
10
  from typing import Optional, Dict, List, Union
11
- from tunnel_manager.tunnel_manager import Tunnel
12
- from fastmcp import FastMCP, Context
13
11
  from pydantic import Field
12
+ from fastmcp import FastMCP, Context
13
+ from fastmcp.server.auth.oidc_proxy import OIDCProxy
14
+ from fastmcp.server.auth import OAuthProxy, RemoteAuthProvider
15
+ from fastmcp.server.auth.providers.jwt import JWTVerifier, StaticTokenVerifier
16
+ from fastmcp.server.middleware.logging import LoggingMiddleware
17
+ from fastmcp.server.middleware.timing import TimingMiddleware
18
+ from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
19
+ from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
20
+ from tunnel_manager.tunnel_manager import Tunnel
14
21
 
15
22
  # Initialize FastMCP
16
23
  mcp = FastMCP(name="TunnelServer")
@@ -2120,6 +2127,83 @@ def tunnel_manager_mcp():
2120
2127
  default=8000,
2121
2128
  help="Port number for HTTP transport (default: 8000)",
2122
2129
  )
2130
+ parser.add_argument(
2131
+ "--auth-type",
2132
+ default="none",
2133
+ choices=["none", "static", "jwt", "oauth-proxy", "oidc-proxy", "remote-oauth"],
2134
+ help="Authentication type for MCP server: 'none' (disabled), 'static' (internal), 'jwt' (external token verification), 'oauth-proxy', 'oidc-proxy', 'remote-oauth' (external) (default: none)",
2135
+ )
2136
+ # JWT/Token params
2137
+ parser.add_argument(
2138
+ "--token-jwks-uri", default=None, help="JWKS URI for JWT verification"
2139
+ )
2140
+ parser.add_argument(
2141
+ "--token-issuer", default=None, help="Issuer for JWT verification"
2142
+ )
2143
+ parser.add_argument(
2144
+ "--token-audience", default=None, help="Audience for JWT verification"
2145
+ )
2146
+ # OAuth Proxy params
2147
+ parser.add_argument(
2148
+ "--oauth-upstream-auth-endpoint",
2149
+ default=None,
2150
+ help="Upstream authorization endpoint for OAuth Proxy",
2151
+ )
2152
+ parser.add_argument(
2153
+ "--oauth-upstream-token-endpoint",
2154
+ default=None,
2155
+ help="Upstream token endpoint for OAuth Proxy",
2156
+ )
2157
+ parser.add_argument(
2158
+ "--oauth-upstream-client-id",
2159
+ default=None,
2160
+ help="Upstream client ID for OAuth Proxy",
2161
+ )
2162
+ parser.add_argument(
2163
+ "--oauth-upstream-client-secret",
2164
+ default=None,
2165
+ help="Upstream client secret for OAuth Proxy",
2166
+ )
2167
+ parser.add_argument(
2168
+ "--oauth-base-url", default=None, help="Base URL for OAuth Proxy"
2169
+ )
2170
+ # OIDC Proxy params
2171
+ parser.add_argument(
2172
+ "--oidc-config-url", default=None, help="OIDC configuration URL"
2173
+ )
2174
+ parser.add_argument("--oidc-client-id", default=None, help="OIDC client ID")
2175
+ parser.add_argument("--oidc-client-secret", default=None, help="OIDC client secret")
2176
+ parser.add_argument("--oidc-base-url", default=None, help="Base URL for OIDC Proxy")
2177
+ # Remote OAuth params
2178
+ parser.add_argument(
2179
+ "--remote-auth-servers",
2180
+ default=None,
2181
+ help="Comma-separated list of authorization servers for Remote OAuth",
2182
+ )
2183
+ parser.add_argument(
2184
+ "--remote-base-url", default=None, help="Base URL for Remote OAuth"
2185
+ )
2186
+ # Common
2187
+ parser.add_argument(
2188
+ "--allowed-client-redirect-uris",
2189
+ default=None,
2190
+ help="Comma-separated list of allowed client redirect URIs",
2191
+ )
2192
+ # Eunomia params
2193
+ parser.add_argument(
2194
+ "--eunomia-type",
2195
+ default="none",
2196
+ choices=["none", "embedded", "remote"],
2197
+ help="Eunomia authorization type: 'none' (disabled), 'embedded' (built-in), 'remote' (external) (default: none)",
2198
+ )
2199
+ parser.add_argument(
2200
+ "--eunomia-policy-file",
2201
+ default="mcp_policies.json",
2202
+ help="Policy file for embedded Eunomia (default: mcp_policies.json)",
2203
+ )
2204
+ parser.add_argument(
2205
+ "--eunomia-remote-url", default=None, help="URL for remote Eunomia server"
2206
+ )
2123
2207
 
2124
2208
  args = parser.parse_args()
2125
2209
 
@@ -2127,6 +2211,133 @@ def tunnel_manager_mcp():
2127
2211
  print(f"Error: Port {args.port} is out of valid range (0-65535).")
2128
2212
  sys.exit(1)
2129
2213
 
2214
+ # Set auth based on type
2215
+ auth = None
2216
+ allowed_uris = (
2217
+ args.allowed_client_redirect_uris.split(",")
2218
+ if args.allowed_client_redirect_uris
2219
+ else None
2220
+ )
2221
+
2222
+ if args.auth_type == "none":
2223
+ auth = None
2224
+ elif args.auth_type == "static":
2225
+ # Internal static tokens (hardcoded example)
2226
+ auth = StaticTokenVerifier(
2227
+ tokens={
2228
+ "test-token": {"client_id": "test-user", "scopes": ["read", "write"]},
2229
+ "admin-token": {"client_id": "admin", "scopes": ["admin"]},
2230
+ }
2231
+ )
2232
+ elif args.auth_type == "jwt":
2233
+ if not (args.token_jwks_uri and args.token_issuer and args.token_audience):
2234
+ print(
2235
+ "Error: jwt requires --token-jwks-uri, --token-issuer, --token-audience"
2236
+ )
2237
+ sys.exit(1)
2238
+ auth = JWTVerifier(
2239
+ jwks_uri=args.token_jwks_uri,
2240
+ issuer=args.token_issuer,
2241
+ audience=args.token_audience,
2242
+ )
2243
+ elif args.auth_type == "oauth-proxy":
2244
+ if not (
2245
+ args.oauth_upstream_auth_endpoint
2246
+ and args.oauth_upstream_token_endpoint
2247
+ and args.oauth_upstream_client_id
2248
+ and args.oauth_upstream_client_secret
2249
+ and args.oauth_base_url
2250
+ and args.token_jwks_uri
2251
+ and args.token_issuer
2252
+ and args.token_audience
2253
+ ):
2254
+ print(
2255
+ "Error: oauth-proxy requires --oauth-upstream-auth-endpoint, --oauth-upstream-token-endpoint, --oauth-upstream-client-id, --oauth-upstream-client-secret, --oauth-base-url, --token-jwks-uri, --token-issuer, --token-audience"
2256
+ )
2257
+ sys.exit(1)
2258
+ token_verifier = JWTVerifier(
2259
+ jwks_uri=args.token_jwks_uri,
2260
+ issuer=args.token_issuer,
2261
+ audience=args.token_audience,
2262
+ )
2263
+ auth = OAuthProxy(
2264
+ upstream_authorization_endpoint=args.oauth_upstream_auth_endpoint,
2265
+ upstream_token_endpoint=args.oauth_upstream_token_endpoint,
2266
+ upstream_client_id=args.oauth_upstream_client_id,
2267
+ upstream_client_secret=args.oauth_upstream_client_secret,
2268
+ token_verifier=token_verifier,
2269
+ base_url=args.oauth_base_url,
2270
+ allowed_client_redirect_uris=allowed_uris,
2271
+ )
2272
+ elif args.auth_type == "oidc-proxy":
2273
+ if not (
2274
+ args.oidc_config_url
2275
+ and args.oidc_client_id
2276
+ and args.oidc_client_secret
2277
+ and args.oidc_base_url
2278
+ ):
2279
+ print(
2280
+ "Error: oidc-proxy requires --oidc-config-url, --oidc-client-id, --oidc-client-secret, --oidc-base-url"
2281
+ )
2282
+ sys.exit(1)
2283
+ auth = OIDCProxy(
2284
+ config_url=args.oidc_config_url,
2285
+ client_id=args.oidc_client_id,
2286
+ client_secret=args.oidc_client_secret,
2287
+ base_url=args.oidc_base_url,
2288
+ allowed_client_redirect_uris=allowed_uris,
2289
+ )
2290
+ elif args.auth_type == "remote-oauth":
2291
+ if not (
2292
+ args.remote_auth_servers
2293
+ and args.remote_base_url
2294
+ and args.token_jwks_uri
2295
+ and args.token_issuer
2296
+ and args.token_audience
2297
+ ):
2298
+ print(
2299
+ "Error: remote-oauth requires --remote-auth-servers, --remote-base-url, --token-jwks-uri, --token-issuer, --token-audience"
2300
+ )
2301
+ sys.exit(1)
2302
+ auth_servers = [url.strip() for url in args.remote_auth_servers.split(",")]
2303
+ token_verifier = JWTVerifier(
2304
+ jwks_uri=args.token_jwks_uri,
2305
+ issuer=args.token_issuer,
2306
+ audience=args.token_audience,
2307
+ )
2308
+ auth = RemoteAuthProvider(
2309
+ token_verifier=token_verifier,
2310
+ authorization_servers=auth_servers,
2311
+ base_url=args.remote_base_url,
2312
+ )
2313
+ mcp.auth = auth
2314
+ if args.eunomia_type != "none":
2315
+ from eunomia_mcp import create_eunomia_middleware
2316
+
2317
+ if args.eunomia_type == "embedded":
2318
+ if not args.eunomia_policy_file:
2319
+ print("Error: embedded Eunomia requires --eunomia-policy-file")
2320
+ sys.exit(1)
2321
+ middleware = create_eunomia_middleware(policy_file=args.eunomia_policy_file)
2322
+ mcp.add_middleware(middleware)
2323
+ elif args.eunomia_type == "remote":
2324
+ if not args.eunomia_remote_url:
2325
+ print("Error: remote Eunomia requires --eunomia-remote-url")
2326
+ sys.exit(1)
2327
+ middleware = create_eunomia_middleware(
2328
+ use_remote_eunomia=args.eunomia_remote_url
2329
+ )
2330
+ mcp.add_middleware(middleware)
2331
+
2332
+ mcp.add_middleware(
2333
+ ErrorHandlingMiddleware(include_traceback=True, transform_errors=True)
2334
+ )
2335
+ mcp.add_middleware(
2336
+ RateLimitingMiddleware(max_requests_per_second=10.0, burst_capacity=20)
2337
+ )
2338
+ mcp.add_middleware(TimingMiddleware())
2339
+ mcp.add_middleware(LoggingMiddleware())
2340
+
2130
2341
  if args.transport == "stdio":
2131
2342
  mcp.run(transport="stdio")
2132
2343
  elif args.transport == "http":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunnel-manager
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: Create SSH Tunnels to your remote hosts and host as an MCP Server for Agentic AI!
5
5
  Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
6
  License: MIT
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: fastmcp>=2.11.3
15
+ Requires-Dist: fastmcp>=2.12.4
16
16
  Requires-Dist: paramiko>=4.0.0
17
17
  Dynamic: license-file
18
18
 
@@ -38,7 +38,7 @@ Dynamic: license-file
38
38
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/tunnel-manager)
39
39
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/tunnel-manager)
40
40
 
41
- *Version: 1.0.3*
41
+ *Version: 1.0.5*
42
42
 
43
43
  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.
44
44
 
@@ -84,135 +84,49 @@ This project provides a Python-based `Tunnel` class for secure SSH connections a
84
84
  <details>
85
85
  <summary><b>Usage:</b></summary>
86
86
 
87
- ## Tunnel Class
88
- The `Tunnel` class can be used standalone for SSH operations. Examples:
89
-
90
- ### Using RSA Keys
91
- ```python
92
- from tunnel_manager.tunnel_manager import Tunnel
93
-
94
- # Initialize with a remote host (assumes ~/.ssh/config or explicit params)
95
- tunnel = Tunnel(
96
- remote_host="192.168.1.10",
97
- username="admin",
98
- password="mypassword",
99
- identity_file="/path/to/id_rsa",
100
- certificate_file="/path/to/cert", # Optional for Teleport
101
- proxy_command="tsh proxy ssh %h", # Optional for Teleport
102
- ssh_config_file="~/.ssh/config",
103
- )
104
-
105
- # Connect and run a command
106
- tunnel.connect()
107
- out, err = tunnel.run_command("ls -la /tmp")
108
- print(f"Output: {out}\nError: {err}")
109
-
110
- # Upload a file
111
- tunnel.send_file("/local/file.txt", "/remote/file.txt")
112
-
113
- # Download a file
114
- tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
115
-
116
- # Setup passwordless SSH with RSA
117
- tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_rsa", key_type="rsa")
118
-
119
- # Copy SSH config
120
- tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
121
-
122
- # Rotate SSH key with RSA
123
- tunnel.rotate_ssh_key("/path/to/new_rsa_key", key_type="rsa")
124
-
125
- # Close the connection
126
- tunnel.close()
127
- ```
128
-
129
- ### Using Ed25519 Keys
130
- ```python
131
- from tunnel_manager.tunnel_manager import Tunnel
132
-
133
- # Initialize with a remote host (assumes ~/.ssh/config or explicit params)
134
- tunnel = Tunnel(
135
- remote_host="192.168.1.10",
136
- username="admin",
137
- password="mypassword",
138
- identity_file="/path/to/id_ed25519",
139
- certificate_file="/path/to/cert", # Optional for Teleport
140
- proxy_command="tsh proxy ssh %h", # Optional for Teleport
141
- ssh_config_file="~/.ssh/config",
142
- )
143
-
144
- # Connect and run a command
145
- tunnel.connect()
146
- out, err = tunnel.run_command("ls -la /tmp")
147
- print(f"Output: {out}\nError: {err}")
148
-
149
- # Upload a file
150
- tunnel.send_file("/local/file.txt", "/remote/file.txt")
151
-
152
- # Download a file
153
- tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
154
-
155
- # Setup passwordless SSH with Ed25519
156
- tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_ed25519", key_type="ed25519")
157
-
158
- # Copy SSH config
159
- tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
160
-
161
- # Rotate SSH key with Ed25519
162
- tunnel.rotate_ssh_key("/path/to/new_ed25519_key", key_type="ed25519")
163
-
164
- # Close the connection
165
- tunnel.close()
166
- ```
167
-
168
- ## Tunnel Manager CLI Usage
169
- The `tunnel_manager.py` script provides a CLI for managing SSH operations across hosts defined in an Ansible-style YAML inventory file. Below are examples for each command, targeting different inventory groups (`all`, `homelab`, `poweredge`). The CLI now supports both RSA and Ed25519 keys via the `--key-type` flag for relevant commands (default: `ed25519`).
170
-
171
- **Inventory File Example (`inventory.yml`)**:
172
- ```yaml
173
- all:
174
- hosts:
175
- r510:
176
- ansible_host: 192.168.1.10
177
- ansible_user: admin
178
- ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
179
- r710:
180
- ansible_host: 192.168.1.11
181
- ansible_user: admin
182
- ansible_ssh_pass: mypassword
183
- gr1080:
184
- ansible_host: 192.168.1.14
185
- ansible_user: admin
186
- ansible_ssh_private_key_file: "~/.ssh/id_rsa"
187
- homelab:
188
- hosts:
189
- r510:
190
- ansible_host: 192.168.1.10
191
- ansible_user: admin
192
- ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
193
- r710:
194
- ansible_host: 192.168.1.11
195
- ansible_user: admin
196
- ansible_ssh_pass: mypassword
197
- gr1080:
198
- ansible_host: 192.168.1.14
199
- ansible_user: admin
200
- ansible_ssh_private_key_file: "~/.ssh/id_rsa"
201
- poweredge:
202
- hosts:
203
- r510:
204
- ansible_host: 192.168.1.10
205
- ansible_user: admin
206
- ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
207
- r710:
208
- ansible_host: 192.168.1.11
209
- ansible_user: admin
210
- ansible_ssh_pass: mypassword
211
- ```
87
+ ### CLI
88
+ | Short Flag | Long Flag | Description | Required | Default Value |
89
+ |------------|----------------------|----------------------------------------------------------|----------|---------------|
90
+ | -h | --help | Show usage for the script | No | None |
91
+ | | --log-file | Log to specified file (default: console output) | No | Console |
92
+ | | setup-all | Setup passwordless SSH for all hosts in inventory | Yes* | None |
93
+ | | --inventory | YAML inventory path | Yes | None |
94
+ | | --shared-key-path | Path to shared private key | No | ~/.ssh/id_shared |
95
+ | | --key-type | Key type (rsa or ed25519) | No | ed25519 |
96
+ | | --group | Inventory group to target | No | all |
97
+ | | --parallel | Run operation in parallel | No | False |
98
+ | | --max-threads | Max threads for parallel execution | No | 5 |
99
+ | | run-command | Run a shell command on all hosts in inventory | Yes* | None |
100
+ | | --remote-command | Shell command to run | Yes | None |
101
+ | | copy-config | Copy SSH config to all hosts in inventory | Yes* | None |
102
+ | | --local-config-path | Local SSH config path | Yes | None |
103
+ | | --remote-config-path | Remote path for SSH config | No | ~/.ssh/config |
104
+ | | rotate-key | Rotate SSH keys for all hosts in inventory | Yes* | None |
105
+ | | --key-prefix | Prefix for new key paths (appends hostname) | No | ~/.ssh/id_ |
106
+ | | --key-type | Key type (rsa or ed25519) | No | ed25519 |
107
+ | | send-file | Upload a file to all hosts in inventory | Yes* | None |
108
+ | | --local-path | Local file path to upload | Yes | None |
109
+ | | --remote-path | Remote destination path | Yes | None |
110
+ | | receive-file | Download a file from all hosts in inventory | Yes* | None |
111
+ | | --remote-path | Remote file path to download | Yes | None |
112
+ | | --local-path-prefix | Local directory path prefix to save files | Yes | None |
212
113
 
213
- Replace IPs, usernames, and passwords with your actual values.
114
+ ### Notes
115
+ 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:
116
+ - `setup-all`: Requires `--inventory`.
117
+ - `run-command`: Requires `--inventory` and `--remote-command`.
118
+ - `copy-config`: Requires `--inventory` and `--local-config-path`.
119
+ - `rotate-key`: Requires `--inventory`.
120
+ - `send-file`: Requires `--inventory`, `--local-path`, and `--remote-path`.
121
+ - `receive-file`: Requires `--inventory`, `--remote-path`, and `--local-path-prefix`.
214
122
 
215
- ### CLI Commands
123
+ ### Additional Notes
124
+ - Ensure `ansible_host` values in `inventory.yml` are resolvable IPs or hostnames.
125
+ - Update `ansible_ssh_private_key_file` in the inventory after running `rotate-key`.
126
+ - Use `--log-file` for file-based logging or omit for console output.
127
+ - The `--parallel` option speeds up operations but may overload resources; adjust `--max-threads` as needed.
128
+ - The `receive-file` command saves files to `local_path_prefix/<hostname>/<filename>` to preserve original filenames and avoid conflicts.
129
+ - Ed25519 keys are recommended for better security and performance over RSA, but RSA is supported for compatibility with older systems.
216
130
 
217
131
  #### 1. Setup Passwordless SSH
218
132
  Set up passwordless SSH for hosts in the inventory, distributing a shared key. Use `--key-type` to specify RSA or Ed25519 (default: ed25519).
@@ -304,70 +218,267 @@ Download a file from all hosts in the specified group, saving to host-specific s
304
218
  tunnel-manager --log-file download_poweredge.log receive-file --inventory inventory.yml --remote-path /home/user/myfile.txt --local-path-prefix ./downloads --group poweredge
305
219
  ```
306
220
 
307
- ### CLI Command Table
308
- | Short Flag | Long Flag | Description | Required | Default Value |
309
- |------------|----------------------|----------------------------------------------------------|----------|---------------|
310
- | -h | --help | Show usage for the script | No | None |
311
- | | --log-file | Log to specified file (default: console output) | No | Console |
312
- | | setup-all | Setup passwordless SSH for all hosts in inventory | Yes* | None |
313
- | | --inventory | YAML inventory path | Yes | None |
314
- | | --shared-key-path | Path to shared private key | No | ~/.ssh/id_shared |
315
- | | --key-type | Key type (rsa or ed25519) | No | ed25519 |
316
- | | --group | Inventory group to target | No | all |
317
- | | --parallel | Run operation in parallel | No | False |
318
- | | --max-threads | Max threads for parallel execution | No | 5 |
319
- | | run-command | Run a shell command on all hosts in inventory | Yes* | None |
320
- | | --remote-command | Shell command to run | Yes | None |
321
- | | copy-config | Copy SSH config to all hosts in inventory | Yes* | None |
322
- | | --local-config-path | Local SSH config path | Yes | None |
323
- | | --remote-config-path | Remote path for SSH config | No | ~/.ssh/config |
324
- | | rotate-key | Rotate SSH keys for all hosts in inventory | Yes* | None |
325
- | | --key-prefix | Prefix for new key paths (appends hostname) | No | ~/.ssh/id_ |
326
- | | --key-type | Key type (rsa or ed25519) | No | ed25519 |
327
- | | send-file | Upload a file to all hosts in inventory | Yes* | None |
328
- | | --local-path | Local file path to upload | Yes | None |
329
- | | --remote-path | Remote destination path | Yes | None |
330
- | | receive-file | Download a file from all hosts in inventory | Yes* | None |
331
- | | --remote-path | Remote file path to download | Yes | None |
332
- | | --local-path-prefix | Local directory path prefix to save files | Yes | None |
221
+ ### Tunnel Manager Inventory
333
222
 
334
- ### Notes
335
- 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:
336
- - `setup-all`: Requires `--inventory`.
337
- - `run-command`: Requires `--inventory` and `--remote-command`.
338
- - `copy-config`: Requires `--inventory` and `--local-config-path`.
339
- - `rotate-key`: Requires `--inventory`.
340
- - `send-file`: Requires `--inventory`, `--local-path`, and `--remote-path`.
341
- - `receive-file`: Requires `--inventory`, `--remote-path`, and `--local-path-prefix`.
223
+ **Inventory File Example (`inventory.yml`)**:
342
224
 
343
- ### Additional Notes
344
- - Ensure `ansible_host` values in `inventory.yml` are resolvable IPs or hostnames.
345
- - Update `ansible_ssh_private_key_file` in the inventory after running `rotate-key`.
346
- - Use `--log-file` for file-based logging or omit for console output.
347
- - The `--parallel` option speeds up operations but may overload resources; adjust `--max-threads` as needed.
348
- - The `receive-file` command saves files to `local_path_prefix/<hostname>/<filename>` to preserve original filenames and avoid conflicts.
349
- - Ed25519 keys are recommended for better security and performance over RSA, but RSA is supported for compatibility with older systems.
225
+ ```yaml
226
+ all:
227
+ hosts:
228
+ r510:
229
+ ansible_host: 192.168.1.10
230
+ ansible_user: admin
231
+ ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
232
+ r710:
233
+ ansible_host: 192.168.1.11
234
+ ansible_user: admin
235
+ ansible_ssh_pass: mypassword
236
+ gr1080:
237
+ ansible_host: 192.168.1.14
238
+ ansible_user: admin
239
+ ansible_ssh_private_key_file: "~/.ssh/id_rsa"
240
+ homelab:
241
+ hosts:
242
+ r510:
243
+ ansible_host: 192.168.1.10
244
+ ansible_user: admin
245
+ ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
246
+ r710:
247
+ ansible_host: 192.168.1.11
248
+ ansible_user: admin
249
+ ansible_ssh_pass: mypassword
250
+ gr1080:
251
+ ansible_host: 192.168.1.14
252
+ ansible_user: admin
253
+ ansible_ssh_private_key_file: "~/.ssh/id_rsa"
254
+ poweredge:
255
+ hosts:
256
+ r510:
257
+ ansible_host: 192.168.1.10
258
+ ansible_user: admin
259
+ ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
260
+ r710:
261
+ ansible_host: 192.168.1.11
262
+ ansible_user: admin
263
+ ansible_ssh_pass: mypassword
264
+ ```
265
+
266
+ Replace IPs, usernames, and passwords with your actual values.
350
267
 
351
- ## FastMCP Server
352
- The FastMCP server exposes the `Tunnel` functionality as AI-accessible tools. Start the server with:
353
268
 
269
+ ### MCP CLI
270
+
271
+ | Short Flag | Long Flag | Description |
272
+ |------------|------------------------------------|-----------------------------------------------------------------------------|
273
+ | -h | --help | Display help information |
274
+ | -t | --transport | Transport method: 'stdio', 'http', or 'sse' [legacy] (default: stdio) |
275
+ | -s | --host | Host address for HTTP transport (default: 0.0.0.0) |
276
+ | -p | --port | Port number for HTTP transport (default: 8000) |
277
+ | | --auth-type | Authentication type: 'none', 'static', 'jwt', 'oauth-proxy', 'oidc-proxy', 'remote-oauth' (default: none) |
278
+ | | --token-jwks-uri | JWKS URI for JWT verification |
279
+ | | --token-issuer | Issuer for JWT verification |
280
+ | | --token-audience | Audience for JWT verification |
281
+ | | --oauth-upstream-auth-endpoint | Upstream authorization endpoint for OAuth Proxy |
282
+ | | --oauth-upstream-token-endpoint | Upstream token endpoint for OAuth Proxy |
283
+ | | --oauth-upstream-client-id | Upstream client ID for OAuth Proxy |
284
+ | | --oauth-upstream-client-secret | Upstream client secret for OAuth Proxy |
285
+ | | --oauth-base-url | Base URL for OAuth Proxy |
286
+ | | --oidc-config-url | OIDC configuration URL |
287
+ | | --oidc-client-id | OIDC client ID |
288
+ | | --oidc-client-secret | OIDC client secret |
289
+ | | --oidc-base-url | Base URL for OIDC Proxy |
290
+ | | --remote-auth-servers | Comma-separated list of authorization servers for Remote OAuth |
291
+ | | --remote-base-url | Base URL for Remote OAuth |
292
+ | | --allowed-client-redirect-uris | Comma-separated list of allowed client redirect URIs |
293
+ | | --eunomia-type | Eunomia authorization type: 'none', 'embedded', 'remote' (default: none) |
294
+ | | --eunomia-policy-file | Policy file for embedded Eunomia (default: mcp_policies.json) |
295
+ | | --eunomia-remote-url | URL for remote Eunomia server |
296
+
297
+ ### Using as an MCP Server
298
+
299
+ 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:
300
+
301
+ #### Run in stdio mode (default):
354
302
  ```bash
355
- python tunnel_manager_mcp.py --transport stdio
303
+ tunnel-manager-mcp --transport "stdio"
356
304
  ```
357
305
 
358
- Or for HTTP transport:
306
+ #### Run in HTTP mode:
359
307
  ```bash
360
- python tunnel_manager_mcp.py --transport http --host 127.0.0.1 --port 8080
308
+ tunnel-manager-mcp --transport "http" --host "0.0.0.0" --port "8000"
361
309
  ```
362
310
 
363
- </details>
311
+ ### Tunnel Class
312
+ The `Tunnel` class can be used standalone for SSH operations. Examples:
364
313
 
365
- <details>
366
- <summary><b>Installation Instructions:</b></summary>
314
+ #### Using RSA Keys
315
+ ```python
316
+ from tunnel_manager.tunnel_manager import Tunnel
367
317
 
368
- ## Use with AI
318
+ # Initialize with a remote host (assumes ~/.ssh/config or explicit params)
319
+ tunnel = Tunnel(
320
+ remote_host="192.168.1.10",
321
+ username="admin",
322
+ password="mypassword",
323
+ identity_file="/path/to/id_rsa",
324
+ certificate_file="/path/to/cert", # Optional for Teleport
325
+ proxy_command="tsh proxy ssh %h", # Optional for Teleport
326
+ ssh_config_file="~/.ssh/config",
327
+ )
328
+
329
+ # Connect and run a command
330
+ tunnel.connect()
331
+ out, err = tunnel.run_command("ls -la /tmp")
332
+ print(f"Output: {out}\nError: {err}")
333
+
334
+ # Upload a file
335
+ tunnel.send_file("/local/file.txt", "/remote/file.txt")
336
+
337
+ # Download a file
338
+ tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
339
+
340
+ # Setup passwordless SSH with RSA
341
+ tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_rsa", key_type="rsa")
342
+
343
+ # Copy SSH config
344
+ tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
345
+
346
+ # Rotate SSH key with RSA
347
+ tunnel.rotate_ssh_key("/path/to/new_rsa_key", key_type="rsa")
348
+
349
+ # Close the connection
350
+ tunnel.close()
351
+ ```
352
+
353
+ #### Using Ed25519 Keys
354
+ ```python
355
+ from tunnel_manager.tunnel_manager import Tunnel
356
+
357
+ # Initialize with a remote host (assumes ~/.ssh/config or explicit params)
358
+ tunnel = Tunnel(
359
+ remote_host="192.168.1.10",
360
+ username="admin",
361
+ password="mypassword",
362
+ identity_file="/path/to/id_ed25519",
363
+ certificate_file="/path/to/cert", # Optional for Teleport
364
+ proxy_command="tsh proxy ssh %h", # Optional for Teleport
365
+ ssh_config_file="~/.ssh/config",
366
+ )
367
+
368
+ # Connect and run a command
369
+ tunnel.connect()
370
+ out, err = tunnel.run_command("ls -la /tmp")
371
+ print(f"Output: {out}\nError: {err}")
372
+
373
+ # Upload a file
374
+ tunnel.send_file("/local/file.txt", "/remote/file.txt")
375
+
376
+ # Download a file
377
+ tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
378
+
379
+ # Setup passwordless SSH with Ed25519
380
+ tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_ed25519", key_type="ed25519")
381
+
382
+ # Copy SSH config
383
+ tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
384
+
385
+ # Rotate SSH key with Ed25519
386
+ tunnel.rotate_ssh_key("/path/to/new_ed25519_key", key_type="ed25519")
387
+
388
+ # Close the connection
389
+ tunnel.close()
390
+ ```
391
+
392
+ ### Deploy MCP Server as a Service
393
+
394
+ The MCP server can be deployed using Docker, with configurable authentication, middleware, and Eunomia authorization.
395
+
396
+ #### Using Docker Run
397
+
398
+ ```bash
399
+ docker pull knucklessg1/tunnel-manager:latest
400
+
401
+ docker run -d \
402
+ --name tunnel-manager-mcp \
403
+ -p 8004:8004 \
404
+ -e HOST=0.0.0.0 \
405
+ -e PORT=8004 \
406
+ -e TRANSPORT=http \
407
+ -e AUTH_TYPE=none \
408
+ -e EUNOMIA_TYPE=none \
409
+ knucklessg1/tunnel-manager:latest
410
+ ```
411
+
412
+ For advanced authentication (e.g., JWT, OAuth Proxy, OIDC Proxy, Remote OAuth) or Eunomia, add the relevant environment variables:
413
+
414
+ ```bash
415
+ docker run -d \
416
+ --name tunnel-manager-mcp \
417
+ -p 8004:8004 \
418
+ -e HOST=0.0.0.0 \
419
+ -e PORT=8004 \
420
+ -e TRANSPORT=http \
421
+ -e AUTH_TYPE=oidc-proxy \
422
+ -e OIDC_CONFIG_URL=https://provider.com/.well-known/openid-configuration \
423
+ -e OIDC_CLIENT_ID=your-client-id \
424
+ -e OIDC_CLIENT_SECRET=your-client-secret \
425
+ -e OIDC_BASE_URL=https://your-server.com \
426
+ -e ALLOWED_CLIENT_REDIRECT_URIS=http://localhost:*,https://*.example.com/* \
427
+ -e EUNOMIA_TYPE=embedded \
428
+ -e EUNOMIA_POLICY_FILE=/app/mcp_policies.json \
429
+ knucklessg1/tunnel-manager:latest
430
+ ```
431
+
432
+ #### Using Docker Compose
433
+
434
+ Create a `docker-compose.yml` file:
435
+
436
+ ```yaml
437
+ services:
438
+ tunnel-manager-mcp:
439
+ image: knucklessg1/tunnel-manager:latest
440
+ environment:
441
+ - HOST=0.0.0.0
442
+ - PORT=8004
443
+ - TRANSPORT=http
444
+ - AUTH_TYPE=none
445
+ - EUNOMIA_TYPE=none
446
+ ports:
447
+ - 8004:8004
448
+ ```
449
+
450
+ For advanced setups with authentication and Eunomia:
451
+
452
+ ```yaml
453
+ services:
454
+ tunnel-manager-mcp:
455
+ image: knucklessg1/tunnel-manager:latest
456
+ environment:
457
+ - HOST=0.0.0.0
458
+ - PORT=8004
459
+ - TRANSPORT=http
460
+ - AUTH_TYPE=oidc-proxy
461
+ - OIDC_CONFIG_URL=https://provider.com/.well-known/openid-configuration
462
+ - OIDC_CLIENT_ID=your-client-id
463
+ - OIDC_CLIENT_SECRET=your-client-secret
464
+ - OIDC_BASE_URL=https://your-server.com
465
+ - ALLOWED_CLIENT_REDIRECT_URIS=http://localhost:*,https://*.example.com/*
466
+ - EUNOMIA_TYPE=embedded
467
+ - EUNOMIA_POLICY_FILE=/app/mcp_policies.json
468
+ ports:
469
+ - 8004:8004
470
+ volumes:
471
+ - ./mcp_policies.json:/app/mcp_policies.json
472
+ ```
473
+
474
+ Run the service:
475
+
476
+ ```bash
477
+ docker-compose up -d
478
+ ```
479
+
480
+ #### Configure `mcp.json` for AI Integration
369
481
 
370
- Configure `mcp.json`
371
482
  ```json
372
483
  {
373
484
  "mcpServers": {
@@ -398,23 +509,10 @@ Configure `mcp.json`
398
509
  }
399
510
  }
400
511
  ```
512
+ </details>
401
513
 
402
- ### Deploy MCP Server as a Container
403
- ```bash
404
- docker pull knucklessg1/tunnel-manager:latest
405
- ```
406
-
407
- Modify the `compose.yml`
408
- ```yaml
409
- services:
410
- tunnel-manager:
411
- image: knucklessg1/tunnel-manager:latest
412
- environment:
413
- - HOST=0.0.0.0
414
- - PORT=8021
415
- ports:
416
- - 8021:8021
417
- ```
514
+ <details>
515
+ <summary><b>Installation Instructions:</b></summary>
418
516
 
419
517
  ### Install Python Package
420
518
  ```bash
@@ -0,0 +1,11 @@
1
+ tests/test_tunnel.py,sha256=Dj6z1EM50uGqEJA0GBDP6L8ZPUMRAJNT7dAnwkuSgNU,2461
2
+ tunnel_manager/__init__.py,sha256=cqAitAkoCcEkaCQcP7Y8tngiUK7pU6SIMlmpABShh9g,807
3
+ tunnel_manager/__main__.py,sha256=Z1uxNLjwIjJpvu97bXrvsawnghJScA52E2wtAgg5MLo,152
4
+ tunnel_manager/tunnel_manager.py,sha256=DZn2Zs0OPxB_2wWqkro--UbFLdoe8kivaeLvKZKWANM,39384
5
+ tunnel_manager/tunnel_manager_mcp.py,sha256=W8glkvrtD0jxdddkOgEAz4YZMoKfqOYsNB2jdrTrqSY,84492
6
+ tunnel_manager-1.0.5.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
7
+ tunnel_manager-1.0.5.dist-info/METADATA,sha256=6B1XWaT318I_xGl2cxwbnuJTztrOmC88fynozPQI_P4,25585
8
+ tunnel_manager-1.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ tunnel_manager-1.0.5.dist-info/entry_points.txt,sha256=hYtm4jvOAew8CbeqqUBH2nXY51mSQwhF4GhU0yclV6c,154
10
+ tunnel_manager-1.0.5.dist-info/top_level.txt,sha256=W4J-lyPPNeOS696f0LneZsP2MVERR8HE9UHbAFQ550A,21
11
+ tunnel_manager-1.0.5.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- tests/test_tunnel.py,sha256=Dj6z1EM50uGqEJA0GBDP6L8ZPUMRAJNT7dAnwkuSgNU,2461
2
- tunnel_manager/__init__.py,sha256=cqAitAkoCcEkaCQcP7Y8tngiUK7pU6SIMlmpABShh9g,807
3
- tunnel_manager/__main__.py,sha256=Z1uxNLjwIjJpvu97bXrvsawnghJScA52E2wtAgg5MLo,152
4
- tunnel_manager/tunnel_manager.py,sha256=DZn2Zs0OPxB_2wWqkro--UbFLdoe8kivaeLvKZKWANM,39384
5
- tunnel_manager/tunnel_manager_mcp.py,sha256=_TVDIH7fZpNsADbCKpMU8uzTUU41l9YqKNuK6dvaxl4,76157
6
- tunnel_manager-1.0.3.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
7
- tunnel_manager-1.0.3.dist-info/METADATA,sha256=Es2jT6ZIlWs1miyKjAw6mK3K0V59ZkLIHtR81YFVh10,20683
8
- tunnel_manager-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- tunnel_manager-1.0.3.dist-info/entry_points.txt,sha256=hYtm4jvOAew8CbeqqUBH2nXY51mSQwhF4GhU0yclV6c,154
10
- tunnel_manager-1.0.3.dist-info/top_level.txt,sha256=W4J-lyPPNeOS696f0LneZsP2MVERR8HE9UHbAFQ550A,21
11
- tunnel_manager-1.0.3.dist-info/RECORD,,