cloudx-proxy 0.4.3__tar.gz → 0.4.5__tar.gz
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.
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/CHANGELOG.md +14 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/PKG-INFO +59 -20
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/README.md +58 -19
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/_version.py +2 -2
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/cli.py +2 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/setup.py +278 -129
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/PKG-INFO +59 -20
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/.github/workflows/release.yml +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/.gitignore +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/.releaserc +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/CONTRIBUTING.md +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/LICENSE +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/_1password.py +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/__init__.py +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/core.py +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/SOURCES.txt +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/dependency_links.txt +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/entry_points.txt +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/requires.txt +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/top_level.txt +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/package.json +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/pyproject.toml +0 -0
- {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/setup.cfg +0 -0
@@ -1,3 +1,17 @@
|
|
1
|
+
## [0.4.5](https://github.com/easytocloud/cloudX-proxy/compare/v0.4.4...v0.4.5) (2025-03-07)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* 1Password key matching ([4583e20](https://github.com/easytocloud/cloudX-proxy/commit/4583e20915b9bf8ab5cb2244676ee735aaa35cfc))
|
7
|
+
|
8
|
+
## [0.4.4](https://github.com/easytocloud/cloudX-proxy/compare/v0.4.3...v0.4.4) (2025-03-07)
|
9
|
+
|
10
|
+
|
11
|
+
### Bug Fixes
|
12
|
+
|
13
|
+
* improved generated ssh config file for multiple cloudx instances ([11c28d1](https://github.com/easytocloud/cloudX-proxy/commit/11c28d1534bddfbbd7d108a2a961aa21166e899a))
|
14
|
+
|
1
15
|
## [0.4.3](https://github.com/easytocloud/cloudX-proxy/compare/v0.4.2...v0.4.3) (2025-03-06)
|
2
16
|
|
3
17
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: cloudx-proxy
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.5
|
4
4
|
Summary: SSH proxy command to connect VSCode with Cloud9/CloudX instance using AWS Systems Manager
|
5
5
|
Author-email: easytocloud <info@easytocloud.com>
|
6
6
|
License: MIT License
|
@@ -158,22 +158,30 @@ The setup command configures SSH to use cloudX-proxy as a ProxyCommand, enabling
|
|
158
158
|
uvx cloudx-proxy setup --profile myprofile --ssh-key mykey
|
159
159
|
```
|
160
160
|
|
161
|
-
Will create a configuration like this:
|
161
|
+
Will create a three-tier configuration structure like this:
|
162
162
|
|
163
163
|
```
|
164
|
-
#
|
165
|
-
#
|
166
|
-
|
164
|
+
# Generic configuration (shared by all environments)
|
165
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
166
|
+
# Configuration type: generic
|
167
|
+
Host cloudx-*
|
167
168
|
User ec2-user
|
168
|
-
IdentityFile ~/.ssh/vscode/mykey
|
169
|
-
IdentitiesOnly yes
|
170
|
-
ProxyCommand uvx cloudx-proxy connect %h %p --profile myprofile --ssh-key mykey
|
171
169
|
TCPKeepAlive yes
|
172
170
|
ControlMaster auto
|
173
171
|
ControlPath ~/.ssh/control/%r@%h:%p
|
174
172
|
ControlPersist 4h
|
175
173
|
|
176
|
-
#
|
174
|
+
# Environment configuration (specific to a single environment)
|
175
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
176
|
+
# Configuration type: environment
|
177
|
+
Host cloudx-dev-*
|
178
|
+
IdentityFile ~/.ssh/vscode/mykey
|
179
|
+
IdentitiesOnly yes
|
180
|
+
ProxyCommand uvx cloudx-proxy connect %h %p --profile myprofile --ssh-key mykey
|
181
|
+
|
182
|
+
# Host configuration (specific to a single instance)
|
183
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
184
|
+
# Configuration type: host
|
177
185
|
Host cloudx-dev-myserver
|
178
186
|
HostName i-0123456789abcdef0
|
179
187
|
```
|
@@ -191,23 +199,54 @@ In these examples, ssh will use cloudx-proxy to connect to AWS with the `myprofi
|
|
191
199
|
VSCode will be able to connect to the instance using the same SSH configuration.
|
192
200
|
|
193
201
|
### SSH Configuration Details
|
194
|
-
The setup command creates
|
202
|
+
The setup command creates a hierarchical three-tier SSH configuration structure:
|
195
203
|
|
196
|
-
1.
|
197
|
-
- User
|
198
|
-
- 1Password SSH agent integration if selected
|
199
|
-
- ProxyCommand with appropriate parameters
|
200
|
-
- SSH multiplexing for better performance
|
204
|
+
1. Generic configuration (cloudx-*) containing common settings shared across all environments:
|
205
|
+
- User settings (ec2-user)
|
201
206
|
- TCP keepalive for connection stability
|
207
|
+
- SSH multiplexing for better performance (ControlMaster, ControlPath, ControlPersist)
|
202
208
|
|
203
|
-
2.
|
204
|
-
-
|
205
|
-
-
|
206
|
-
- Inherits all
|
209
|
+
2. Environment-specific configuration (cloudx-{env}-*) with:
|
210
|
+
- Authentication settings (IdentityFile, IdentityAgent for 1Password)
|
211
|
+
- ProxyCommand with environment-specific parameters
|
212
|
+
- Inherits all settings from the generic configuration
|
213
|
+
|
214
|
+
3. Host-specific entries (cloudx-{env}-hostname) with:
|
215
|
+
- Instance ID (HostName directive)
|
216
|
+
- Inherits all settings from both generic and environment configurations
|
217
|
+
|
218
|
+
Each configuration tier is clearly marked with a timestamp and version information comment, making it easy to track when and how configurations were created.
|
207
219
|
|
208
220
|
When adding new instances to an existing environment, you can choose to:
|
221
|
+
- Keep the existing environment configuration if it's compatible
|
209
222
|
- Override the environment configuration with new settings
|
210
|
-
- Add
|
223
|
+
- Add host-specific settings only
|
224
|
+
|
225
|
+
This three-tier structure offers better maintainability by reducing duplication and making it clear which settings apply broadly and which are specific to an environment or host.
|
226
|
+
|
227
|
+
### Security Model: AWS and SSH Integration
|
228
|
+
|
229
|
+
cloudX-proxy implements a unique dual-layer security approach that combines AWS's robust authentication mechanisms with SSH's connection handling capabilities:
|
230
|
+
|
231
|
+
#### AWS Security Layer (Primary)
|
232
|
+
The primary security boundary is enforced through AWS Systems Manager (SSM) and EC2 Instance Connect. This layer provides:
|
233
|
+
- **Access Control**: Only authenticated AWS users with appropriate IAM permissions can establish SSM sessions
|
234
|
+
- **Dynamic Key Authorization**: EC2 Instance Connect allows temporary injection of SSH public keys, valid only for a single session
|
235
|
+
- **Network Security**: No inbound SSH ports need to be exposed, as all connections are established through AWS SSM's secure tunneling
|
236
|
+
- **Audit Trail**: All connection attempts and key pushes are logged in AWS CloudTrail
|
237
|
+
|
238
|
+
#### SSH Layer (Secondary)
|
239
|
+
SSH serves primarily as a connection handler rather than the main security mechanism:
|
240
|
+
- **Ephemeral Authentication**: The SSH key pair is used only to establish the connection through the SSM tunnel
|
241
|
+
- **Session Management**: SSH handles the actual terminal session, file transfers, and multiplexing
|
242
|
+
- **Key Flexibility**: Since keys are pushed dynamically for each session, the same key can safely be used across multiple instances
|
243
|
+
- **Zero Trust Model**: Even if a key is compromised, access still requires valid AWS credentials and permissions
|
244
|
+
|
245
|
+
This architecture means that:
|
246
|
+
1. The security of the connection relies primarily on AWS IAM permissions and SSM session management
|
247
|
+
2. SSH keys can be reused across instances without security implications
|
248
|
+
3. Each connection gets a fresh key authorization through EC2 Instance Connect
|
249
|
+
4. Instances remain completely closed to direct SSH access from the internet
|
211
250
|
|
212
251
|
### VSCode Configuration
|
213
252
|
|
@@ -108,22 +108,30 @@ The setup command configures SSH to use cloudX-proxy as a ProxyCommand, enabling
|
|
108
108
|
uvx cloudx-proxy setup --profile myprofile --ssh-key mykey
|
109
109
|
```
|
110
110
|
|
111
|
-
Will create a configuration like this:
|
111
|
+
Will create a three-tier configuration structure like this:
|
112
112
|
|
113
113
|
```
|
114
|
-
#
|
115
|
-
#
|
116
|
-
|
114
|
+
# Generic configuration (shared by all environments)
|
115
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
116
|
+
# Configuration type: generic
|
117
|
+
Host cloudx-*
|
117
118
|
User ec2-user
|
118
|
-
IdentityFile ~/.ssh/vscode/mykey
|
119
|
-
IdentitiesOnly yes
|
120
|
-
ProxyCommand uvx cloudx-proxy connect %h %p --profile myprofile --ssh-key mykey
|
121
119
|
TCPKeepAlive yes
|
122
120
|
ControlMaster auto
|
123
121
|
ControlPath ~/.ssh/control/%r@%h:%p
|
124
122
|
ControlPersist 4h
|
125
123
|
|
126
|
-
#
|
124
|
+
# Environment configuration (specific to a single environment)
|
125
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
126
|
+
# Configuration type: environment
|
127
|
+
Host cloudx-dev-*
|
128
|
+
IdentityFile ~/.ssh/vscode/mykey
|
129
|
+
IdentitiesOnly yes
|
130
|
+
ProxyCommand uvx cloudx-proxy connect %h %p --profile myprofile --ssh-key mykey
|
131
|
+
|
132
|
+
# Host configuration (specific to a single instance)
|
133
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
134
|
+
# Configuration type: host
|
127
135
|
Host cloudx-dev-myserver
|
128
136
|
HostName i-0123456789abcdef0
|
129
137
|
```
|
@@ -141,23 +149,54 @@ In these examples, ssh will use cloudx-proxy to connect to AWS with the `myprofi
|
|
141
149
|
VSCode will be able to connect to the instance using the same SSH configuration.
|
142
150
|
|
143
151
|
### SSH Configuration Details
|
144
|
-
The setup command creates
|
152
|
+
The setup command creates a hierarchical three-tier SSH configuration structure:
|
145
153
|
|
146
|
-
1.
|
147
|
-
- User
|
148
|
-
- 1Password SSH agent integration if selected
|
149
|
-
- ProxyCommand with appropriate parameters
|
150
|
-
- SSH multiplexing for better performance
|
154
|
+
1. Generic configuration (cloudx-*) containing common settings shared across all environments:
|
155
|
+
- User settings (ec2-user)
|
151
156
|
- TCP keepalive for connection stability
|
157
|
+
- SSH multiplexing for better performance (ControlMaster, ControlPath, ControlPersist)
|
152
158
|
|
153
|
-
2.
|
154
|
-
-
|
155
|
-
-
|
156
|
-
- Inherits all
|
159
|
+
2. Environment-specific configuration (cloudx-{env}-*) with:
|
160
|
+
- Authentication settings (IdentityFile, IdentityAgent for 1Password)
|
161
|
+
- ProxyCommand with environment-specific parameters
|
162
|
+
- Inherits all settings from the generic configuration
|
163
|
+
|
164
|
+
3. Host-specific entries (cloudx-{env}-hostname) with:
|
165
|
+
- Instance ID (HostName directive)
|
166
|
+
- Inherits all settings from both generic and environment configurations
|
167
|
+
|
168
|
+
Each configuration tier is clearly marked with a timestamp and version information comment, making it easy to track when and how configurations were created.
|
157
169
|
|
158
170
|
When adding new instances to an existing environment, you can choose to:
|
171
|
+
- Keep the existing environment configuration if it's compatible
|
159
172
|
- Override the environment configuration with new settings
|
160
|
-
- Add
|
173
|
+
- Add host-specific settings only
|
174
|
+
|
175
|
+
This three-tier structure offers better maintainability by reducing duplication and making it clear which settings apply broadly and which are specific to an environment or host.
|
176
|
+
|
177
|
+
### Security Model: AWS and SSH Integration
|
178
|
+
|
179
|
+
cloudX-proxy implements a unique dual-layer security approach that combines AWS's robust authentication mechanisms with SSH's connection handling capabilities:
|
180
|
+
|
181
|
+
#### AWS Security Layer (Primary)
|
182
|
+
The primary security boundary is enforced through AWS Systems Manager (SSM) and EC2 Instance Connect. This layer provides:
|
183
|
+
- **Access Control**: Only authenticated AWS users with appropriate IAM permissions can establish SSM sessions
|
184
|
+
- **Dynamic Key Authorization**: EC2 Instance Connect allows temporary injection of SSH public keys, valid only for a single session
|
185
|
+
- **Network Security**: No inbound SSH ports need to be exposed, as all connections are established through AWS SSM's secure tunneling
|
186
|
+
- **Audit Trail**: All connection attempts and key pushes are logged in AWS CloudTrail
|
187
|
+
|
188
|
+
#### SSH Layer (Secondary)
|
189
|
+
SSH serves primarily as a connection handler rather than the main security mechanism:
|
190
|
+
- **Ephemeral Authentication**: The SSH key pair is used only to establish the connection through the SSM tunnel
|
191
|
+
- **Session Management**: SSH handles the actual terminal session, file transfers, and multiplexing
|
192
|
+
- **Key Flexibility**: Since keys are pushed dynamically for each session, the same key can safely be used across multiple instances
|
193
|
+
- **Zero Trust Model**: Even if a key is compromised, access still requires valid AWS credentials and permissions
|
194
|
+
|
195
|
+
This architecture means that:
|
196
|
+
1. The security of the connection relies primarily on AWS IAM permissions and SSM session management
|
197
|
+
2. SSH keys can be reused across instances without security implications
|
198
|
+
3. Each connection gets a fresh key authorization through EC2 Instance Connect
|
199
|
+
4. Instances remain completely closed to direct SSH access from the internet
|
161
200
|
|
162
201
|
### VSCode Configuration
|
163
202
|
|
@@ -224,15 +224,21 @@ class CloudXSetup:
|
|
224
224
|
self.print_status("Invalid input", False, 2)
|
225
225
|
return False
|
226
226
|
|
227
|
-
# Create
|
228
|
-
|
227
|
+
# Create possible title variations for the 1Password item
|
228
|
+
ssh_key_title_with_prefix = f"cloudX SSH Key - {self.ssh_key}"
|
229
|
+
ssh_key_title_without_prefix = self.ssh_key
|
229
230
|
|
230
|
-
# Check if a key with
|
231
|
+
# Check if a key with either title exists in 1Password
|
231
232
|
ssh_keys = list_ssh_keys()
|
232
|
-
|
233
|
+
|
234
|
+
# First check for our prefixed format, then for a plain key with the same name
|
235
|
+
existing_key = next((key for key in ssh_keys if key['title'] == ssh_key_title_with_prefix), None)
|
236
|
+
if not existing_key:
|
237
|
+
existing_key = next((key for key in ssh_keys if key['title'] == ssh_key_title_without_prefix), None)
|
233
238
|
|
234
239
|
if existing_key:
|
235
|
-
|
240
|
+
key_title = existing_key['title']
|
241
|
+
self.print_status(f"SSH key '{key_title}' already exists in 1Password", True, 2)
|
236
242
|
# Get the public key
|
237
243
|
result = subprocess.run(
|
238
244
|
['op', 'item', 'get', existing_key['id'], '--fields', 'public key'],
|
@@ -249,8 +255,8 @@ class CloudXSetup:
|
|
249
255
|
return True
|
250
256
|
else:
|
251
257
|
# Create a new SSH key in 1Password
|
252
|
-
self.print_status(f"Creating new SSH key '{
|
253
|
-
success, public_key, item_id = create_ssh_key(
|
258
|
+
self.print_status(f"Creating new SSH key '{ssh_key_title_with_prefix}' in 1Password...", None, 2)
|
259
|
+
success, public_key, item_id = create_ssh_key(ssh_key_title_with_prefix, selected_vault)
|
254
260
|
|
255
261
|
if not success:
|
256
262
|
self.print_status("Failed to create SSH key in 1Password", False, 2)
|
@@ -346,6 +352,18 @@ class CloudXSetup:
|
|
346
352
|
return True
|
347
353
|
return False
|
348
354
|
|
355
|
+
def _get_version(self) -> str:
|
356
|
+
"""Get the current version of the cloudx-proxy package.
|
357
|
+
|
358
|
+
Returns:
|
359
|
+
str: Version string
|
360
|
+
"""
|
361
|
+
try:
|
362
|
+
from . import __version__
|
363
|
+
return __version__
|
364
|
+
except (ImportError, AttributeError):
|
365
|
+
return "unknown"
|
366
|
+
|
349
367
|
def _build_proxy_command(self) -> str:
|
350
368
|
"""Build the ProxyCommand with appropriate parameters.
|
351
369
|
|
@@ -383,24 +401,31 @@ class CloudXSetup:
|
|
383
401
|
IdentitiesOnly yes
|
384
402
|
"""
|
385
403
|
|
386
|
-
def
|
387
|
-
"""
|
404
|
+
def _get_timestamp(self) -> str:
|
405
|
+
"""Get a formatted timestamp for configuration comments.
|
388
406
|
|
389
|
-
Args:
|
390
|
-
cloudx_env: CloudX environment
|
391
|
-
|
392
407
|
Returns:
|
393
|
-
str:
|
408
|
+
str: Formatted timestamp
|
394
409
|
"""
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
host_entry += self._build_auth_config()
|
410
|
+
from datetime import datetime
|
411
|
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
412
|
+
|
413
|
+
def _build_generic_config(self) -> str:
|
414
|
+
"""Build a generic configuration block with common settings for all environments.
|
401
415
|
|
402
|
-
|
403
|
-
|
416
|
+
Returns:
|
417
|
+
str: Generic configuration block
|
418
|
+
"""
|
419
|
+
version = self._get_version()
|
420
|
+
timestamp = self._get_timestamp()
|
421
|
+
|
422
|
+
# Start with metadata comment
|
423
|
+
config = f"""
|
424
|
+
# Created by cloudx-proxy v{version} on {timestamp}
|
425
|
+
# Configuration type: generic
|
426
|
+
Host cloudx-*
|
427
|
+
User ec2-user
|
428
|
+
TCPKeepAlive yes
|
404
429
|
"""
|
405
430
|
|
406
431
|
# Add SSH multiplexing configuration
|
@@ -409,16 +434,42 @@ Host cloudx-{cloudx_env}-*
|
|
409
434
|
# Use forward slashes for Windows as well, SSH client will handle conversion
|
410
435
|
control_path = "~/.ssh/control/%r@%h:%p"
|
411
436
|
|
412
|
-
|
413
|
-
ControlMaster auto
|
437
|
+
config += f""" ControlMaster auto
|
414
438
|
ControlPath {control_path}
|
415
439
|
ControlPersist 4h
|
416
440
|
"""
|
417
441
|
|
418
|
-
return
|
442
|
+
return config
|
443
|
+
|
444
|
+
def _build_environment_config(self, cloudx_env: str) -> str:
|
445
|
+
"""Build an environment-specific configuration block.
|
446
|
+
|
447
|
+
Args:
|
448
|
+
cloudx_env: CloudX environment
|
449
|
+
|
450
|
+
Returns:
|
451
|
+
str: Environment configuration block
|
452
|
+
"""
|
453
|
+
version = self._get_version()
|
454
|
+
timestamp = self._get_timestamp()
|
455
|
+
|
456
|
+
# Start with metadata comment
|
457
|
+
config = f"""
|
458
|
+
# Created by cloudx-proxy v{version} on {timestamp}
|
459
|
+
# Configuration type: environment
|
460
|
+
Host cloudx-{cloudx_env}-*
|
461
|
+
"""
|
462
|
+
# Add authentication configuration
|
463
|
+
config += self._build_auth_config()
|
464
|
+
|
465
|
+
# Add ProxyCommand
|
466
|
+
config += f""" ProxyCommand {self._build_proxy_command()}
|
467
|
+
"""
|
468
|
+
|
469
|
+
return config
|
419
470
|
|
420
471
|
def _build_host_config(self, cloudx_env: str, hostname: str, instance_id: str) -> str:
|
421
|
-
"""Build a
|
472
|
+
"""Build a host-specific configuration block.
|
422
473
|
|
423
474
|
Args:
|
424
475
|
cloudx_env: CloudX environment
|
@@ -426,13 +477,62 @@ Host cloudx-{cloudx_env}-*
|
|
426
477
|
instance_id: EC2 instance ID
|
427
478
|
|
428
479
|
Returns:
|
429
|
-
str:
|
480
|
+
str: Host configuration block
|
430
481
|
"""
|
431
|
-
|
482
|
+
version = self._get_version()
|
483
|
+
timestamp = self._get_timestamp()
|
484
|
+
|
485
|
+
# Start with metadata comment
|
486
|
+
config = f"""
|
487
|
+
# Created by cloudx-proxy v{version} on {timestamp}
|
488
|
+
# Configuration type: host
|
432
489
|
Host cloudx-{cloudx_env}-{hostname}
|
433
490
|
HostName {instance_id}
|
434
491
|
"""
|
435
492
|
|
493
|
+
return config
|
494
|
+
|
495
|
+
def _check_config_exists(self, pattern: str, current_config: str) -> bool:
|
496
|
+
"""Check if a configuration pattern exists in the current config.
|
497
|
+
|
498
|
+
Args:
|
499
|
+
pattern: Host pattern to look for (e.g., 'cloudx-*', 'cloudx-dev-*')
|
500
|
+
current_config: Current SSH config content
|
501
|
+
|
502
|
+
Returns:
|
503
|
+
bool: True if pattern exists in configuration
|
504
|
+
"""
|
505
|
+
return f"Host {pattern}" in current_config
|
506
|
+
|
507
|
+
def _extract_host_config(self, pattern: str, current_config: str) -> Tuple[str, str]:
|
508
|
+
"""Extract a host configuration block from the current config.
|
509
|
+
|
510
|
+
Args:
|
511
|
+
pattern: Host pattern to extract (e.g., 'cloudx-*', 'cloudx-dev-*')
|
512
|
+
current_config: Current SSH config content
|
513
|
+
|
514
|
+
Returns:
|
515
|
+
Tuple[str, str]: Extracted host configuration, remaining configuration
|
516
|
+
"""
|
517
|
+
lines = current_config.splitlines()
|
518
|
+
host_config_lines = []
|
519
|
+
remaining_lines = []
|
520
|
+
in_host_block = False
|
521
|
+
|
522
|
+
for line in lines:
|
523
|
+
if line.strip() == f"Host {pattern}":
|
524
|
+
in_host_block = True
|
525
|
+
host_config_lines.append(line)
|
526
|
+
elif in_host_block and line.strip().startswith("Host "):
|
527
|
+
in_host_block = False
|
528
|
+
remaining_lines.append(line)
|
529
|
+
elif in_host_block:
|
530
|
+
host_config_lines.append(line)
|
531
|
+
else:
|
532
|
+
remaining_lines.append(line)
|
533
|
+
|
534
|
+
return "\n".join(host_config_lines), "\n".join(remaining_lines)
|
535
|
+
|
436
536
|
def _add_host_entry(self, cloudx_env: str, instance_id: str, hostname: str, current_config: str) -> bool:
|
437
537
|
"""Add settings to a specific host entry.
|
438
538
|
|
@@ -446,13 +546,29 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
446
546
|
bool: True if settings were added successfully
|
447
547
|
"""
|
448
548
|
try:
|
449
|
-
#
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
549
|
+
# Check if host entry already exists
|
550
|
+
host_pattern = f"cloudx-{cloudx_env}-{hostname}"
|
551
|
+
if self._check_config_exists(host_pattern, current_config):
|
552
|
+
# Extract existing host configuration
|
553
|
+
host_config, remaining_config = self._extract_host_config(host_pattern, current_config)
|
554
|
+
|
555
|
+
# Update host configuration
|
556
|
+
host_config = self._build_host_config(cloudx_env, hostname, instance_id)
|
557
|
+
|
558
|
+
# Write updated config
|
559
|
+
with open(self.ssh_config_file, 'w') as f:
|
560
|
+
f.write(remaining_config)
|
561
|
+
f.write(host_config)
|
562
|
+
|
563
|
+
self.print_status(f"Updated existing host entry for {host_pattern}", True, 2)
|
564
|
+
else:
|
565
|
+
# Generate new host entry
|
566
|
+
host_entry = self._build_host_config(cloudx_env, hostname, instance_id)
|
567
|
+
|
568
|
+
# Append host entry
|
569
|
+
with open(self.ssh_config_file, 'a') as f:
|
570
|
+
f.write(host_entry)
|
571
|
+
self.print_status(f"Added new host entry for {host_pattern}", True, 2)
|
456
572
|
|
457
573
|
# Set proper permissions on the config file
|
458
574
|
if platform.system() != 'Windows':
|
@@ -469,6 +585,81 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
469
585
|
self.print_status("Continuing setup despite SSH config issues", None, 2)
|
470
586
|
return True
|
471
587
|
return False
|
588
|
+
|
589
|
+
def _check_and_create_generic_config(self, current_config: str) -> Tuple[bool, str]:
|
590
|
+
"""Check if generic configuration exists and create it if needed.
|
591
|
+
|
592
|
+
Args:
|
593
|
+
current_config: Current SSH config content
|
594
|
+
|
595
|
+
Returns:
|
596
|
+
Tuple[bool, str]: Success flag, Updated configuration
|
597
|
+
"""
|
598
|
+
if self._check_config_exists("cloudx-*", current_config):
|
599
|
+
self.print_status("Found existing generic config for cloudx-*", True, 2)
|
600
|
+
return True, current_config
|
601
|
+
|
602
|
+
self.print_status("Creating generic config for cloudx-*", None, 2)
|
603
|
+
generic_config = self._build_generic_config()
|
604
|
+
|
605
|
+
# Append generic config to current config
|
606
|
+
updated_config = current_config
|
607
|
+
if updated_config and not updated_config.endswith('\n'):
|
608
|
+
updated_config += '\n'
|
609
|
+
updated_config += generic_config
|
610
|
+
|
611
|
+
return True, updated_config
|
612
|
+
|
613
|
+
def _check_and_create_environment_config(self, cloudx_env: str, current_config: str) -> Tuple[bool, str]:
|
614
|
+
"""Check if environment configuration exists and create it if needed.
|
615
|
+
|
616
|
+
Args:
|
617
|
+
cloudx_env: CloudX environment
|
618
|
+
current_config: Current SSH config content
|
619
|
+
|
620
|
+
Returns:
|
621
|
+
Tuple[bool, str]: Success flag, Updated configuration
|
622
|
+
"""
|
623
|
+
if self._check_config_exists(f"cloudx-{cloudx_env}-*", current_config):
|
624
|
+
self.print_status(f"Found existing config for cloudx-{cloudx_env}-*", True, 2)
|
625
|
+
|
626
|
+
# Option to override if needed
|
627
|
+
choice = self.prompt(
|
628
|
+
"Would you like to \n"
|
629
|
+
" 1: override the existing environment config\n"
|
630
|
+
" 2: keep existing environment config?\n"
|
631
|
+
"Select an option",
|
632
|
+
"2"
|
633
|
+
)
|
634
|
+
|
635
|
+
if choice == "1":
|
636
|
+
# Remove existing config for this environment
|
637
|
+
self.print_status("Removing existing environment configuration", None, 2)
|
638
|
+
env_config, remaining_config = self._extract_host_config(f"cloudx-{cloudx_env}-*", current_config)
|
639
|
+
|
640
|
+
# Create new environment config
|
641
|
+
env_config = self._build_environment_config(cloudx_env)
|
642
|
+
|
643
|
+
# Append new environment config to remaining config
|
644
|
+
updated_config = remaining_config
|
645
|
+
if updated_config and not updated_config.endswith('\n'):
|
646
|
+
updated_config += '\n'
|
647
|
+
updated_config += env_config
|
648
|
+
|
649
|
+
return True, updated_config
|
650
|
+
|
651
|
+
return True, current_config
|
652
|
+
|
653
|
+
self.print_status(f"Creating environment config for cloudx-{cloudx_env}-*", None, 2)
|
654
|
+
env_config = self._build_environment_config(cloudx_env)
|
655
|
+
|
656
|
+
# Append environment config to current config
|
657
|
+
updated_config = current_config
|
658
|
+
if updated_config and not updated_config.endswith('\n'):
|
659
|
+
updated_config += '\n'
|
660
|
+
updated_config += env_config
|
661
|
+
|
662
|
+
return True, updated_config
|
472
663
|
|
473
664
|
def _ensure_control_dir(self) -> bool:
|
474
665
|
"""Create SSH control directory with proper permissions.
|
@@ -496,38 +687,21 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
496
687
|
return False
|
497
688
|
|
498
689
|
def setup_ssh_config(self, cloudx_env: str, instance_id: str, hostname: str) -> bool:
|
499
|
-
"""Set up SSH config for the instance.
|
500
|
-
|
501
|
-
This method
|
502
|
-
1.
|
503
|
-
|
504
|
-
-
|
505
|
-
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
```
|
515
|
-
# Base environment config (created only once per environment)
|
516
|
-
Host cloudx-{env}-*
|
517
|
-
User ec2-user
|
518
|
-
IdentityAgent ~/.1password/agent.sock # If using 1Password
|
519
|
-
IdentityFile ~/.ssh/vscode/key.pub # .pub for 1Password, no .pub otherwise
|
520
|
-
IdentitiesOnly yes # If using 1Password
|
521
|
-
TCPKeepAlive yes
|
522
|
-
ControlMaster auto
|
523
|
-
ControlPath ~/.ssh/control/%r@%h:%p
|
524
|
-
ControlPersist 4h
|
525
|
-
ProxyCommand uvx cloudx-proxy connect %h %p --profile profile --aws-env env
|
526
|
-
|
527
|
-
# Host entries (added for each instance)
|
528
|
-
Host cloudx-{env}-hostname
|
529
|
-
HostName i-1234567890
|
530
|
-
```
|
690
|
+
"""Set up SSH config for the instance using a three-tier configuration approach.
|
691
|
+
|
692
|
+
This method implements a hierarchical SSH configuration with three levels:
|
693
|
+
1. Generic (cloudx-*): Common settings for all environments
|
694
|
+
- User settings
|
695
|
+
- TCP keepalive
|
696
|
+
- SSH multiplexing configuration
|
697
|
+
|
698
|
+
2. Environment (cloudx-{env}-*): Environment-specific settings
|
699
|
+
- Authentication configuration (identity settings)
|
700
|
+
- ProxyCommand with environment-specific parameters
|
701
|
+
|
702
|
+
3. Host (cloudx-{env}-hostname): Instance-specific settings
|
703
|
+
- HostName (instance ID)
|
704
|
+
- Optional overrides for incompatible settings
|
531
705
|
|
532
706
|
Args:
|
533
707
|
cloudx_env: CloudX environment (e.g., dev, prod)
|
@@ -538,80 +712,46 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
538
712
|
bool: True if config was set up successfully
|
539
713
|
"""
|
540
714
|
self.print_header("SSH Configuration")
|
541
|
-
self.print_status("Setting up SSH configuration...")
|
715
|
+
self.print_status("Setting up SSH configuration with three-tier approach...")
|
542
716
|
|
543
717
|
try:
|
544
|
-
# Check existing configuration
|
545
|
-
if self.ssh_config_file.exists():
|
546
|
-
current_config = self.ssh_config_file.read_text()
|
547
|
-
# Check if configuration for this environment already exists
|
548
|
-
if f"Host cloudx-{cloudx_env}-*" in current_config:
|
549
|
-
self.print_status(f"Found existing config for cloudx-{cloudx_env}-*", True, 2)
|
550
|
-
choice = self.prompt(
|
551
|
-
"Would you like to \n"
|
552
|
-
" 1: override the existing config\n"
|
553
|
-
" 2: add settings to the specific host entry?\n"
|
554
|
-
"Select an option",
|
555
|
-
"1"
|
556
|
-
)
|
557
|
-
if choice == "2":
|
558
|
-
# Add settings to specific host entry
|
559
|
-
self.print_status("Adding settings to specific host entry", None, 2)
|
560
|
-
return self._add_host_entry(cloudx_env, instance_id, hostname, current_config)
|
561
|
-
else:
|
562
|
-
# Remove existing config for this environment
|
563
|
-
self.print_status("Removing existing configuration", None, 2)
|
564
|
-
lines = current_config.splitlines()
|
565
|
-
new_lines = []
|
566
|
-
skip = False
|
567
|
-
for line in lines:
|
568
|
-
if line.strip() == f"Host cloudx-{cloudx_env}-*":
|
569
|
-
skip = True
|
570
|
-
elif skip and line.startswith("Host "):
|
571
|
-
skip = False
|
572
|
-
if not skip:
|
573
|
-
new_lines.append(line)
|
574
|
-
current_config = "\n".join(new_lines)
|
575
|
-
with open(self.ssh_config_file, 'w') as f:
|
576
|
-
f.write(current_config)
|
577
|
-
|
578
|
-
# Create base config
|
579
|
-
self.print_status(f"Creating new config for cloudx-{cloudx_env}-*", None, 2)
|
580
|
-
|
581
718
|
# Ensure control directory exists with proper permissions
|
582
719
|
if not self._ensure_control_dir():
|
583
720
|
return False
|
584
721
|
|
585
|
-
#
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
"""
|
722
|
+
# Initialize or read current configuration
|
723
|
+
current_config = ""
|
724
|
+
if self.ssh_config_file.exists():
|
725
|
+
current_config = self.ssh_config_file.read_text()
|
590
726
|
|
591
|
-
#
|
592
|
-
|
727
|
+
# 1. Check and create generic config (highest level)
|
728
|
+
self.print_status("Checking generic configuration...", None, 2)
|
729
|
+
success, current_config = self._check_and_create_generic_config(current_config)
|
730
|
+
if not success:
|
731
|
+
return False
|
593
732
|
|
594
|
-
#
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
733
|
+
# 2. Check and create environment config
|
734
|
+
self.print_status("Checking environment configuration...", None, 2)
|
735
|
+
success, current_config = self._check_and_create_environment_config(cloudx_env, current_config)
|
736
|
+
if not success:
|
737
|
+
return False
|
738
|
+
|
739
|
+
# Write the updated config with generic and environment tiers
|
740
|
+
self.ssh_config_file.parent.mkdir(parents=True, exist_ok=True)
|
741
|
+
self.ssh_config_file.write_text(current_config)
|
742
|
+
self.print_status("Generic and environment configurations created", True, 2)
|
601
743
|
|
602
744
|
# Set proper permissions on the config file
|
603
745
|
if platform.system() != 'Windows':
|
604
746
|
import stat
|
605
|
-
self.ssh_config_file.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
|
747
|
+
self.ssh_config_file.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
|
606
748
|
self.print_status("Set config file permissions to 600", True, 2)
|
607
|
-
|
608
|
-
# Add
|
609
|
-
self.print_status(f"Adding host entry for cloudx-{cloudx_env}-{hostname}", None, 2)
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
self.print_status("Host entry added", True, 2)
|
614
|
-
|
749
|
+
|
750
|
+
# 3. Add or update host entry (lowest level)
|
751
|
+
self.print_status(f"Adding/updating host entry for cloudx-{cloudx_env}-{hostname}", None, 2)
|
752
|
+
if not self._add_host_entry(cloudx_env, instance_id, hostname, current_config):
|
753
|
+
return False
|
754
|
+
|
615
755
|
# Handle system SSH config integration
|
616
756
|
system_config_path = Path(self.home_dir) / ".ssh" / "config"
|
617
757
|
|
@@ -622,8 +762,17 @@ Host cloudx-{cloudx_env}-{hostname}
|
|
622
762
|
self.print_status(f"Created SSH directory: {ssh_parent_dir}", True, 2)
|
623
763
|
self._set_directory_permissions(ssh_parent_dir)
|
624
764
|
|
625
|
-
#
|
626
|
-
|
765
|
+
# Handle system config integration
|
766
|
+
same_file = False
|
767
|
+
if self.ssh_config_file.exists() and system_config_path.exists():
|
768
|
+
try:
|
769
|
+
same_file = self.ssh_config_file.samefile(system_config_path)
|
770
|
+
except:
|
771
|
+
same_file = str(self.ssh_config_file) == str(system_config_path)
|
772
|
+
else:
|
773
|
+
same_file = str(self.ssh_config_file) == str(system_config_path)
|
774
|
+
|
775
|
+
if same_file:
|
627
776
|
self.print_status("Using system SSH config directly, no Include needed", True, 2)
|
628
777
|
else:
|
629
778
|
# Otherwise, make sure the system config includes our config file
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: cloudx-proxy
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.5
|
4
4
|
Summary: SSH proxy command to connect VSCode with Cloud9/CloudX instance using AWS Systems Manager
|
5
5
|
Author-email: easytocloud <info@easytocloud.com>
|
6
6
|
License: MIT License
|
@@ -158,22 +158,30 @@ The setup command configures SSH to use cloudX-proxy as a ProxyCommand, enabling
|
|
158
158
|
uvx cloudx-proxy setup --profile myprofile --ssh-key mykey
|
159
159
|
```
|
160
160
|
|
161
|
-
Will create a configuration like this:
|
161
|
+
Will create a three-tier configuration structure like this:
|
162
162
|
|
163
163
|
```
|
164
|
-
#
|
165
|
-
#
|
166
|
-
|
164
|
+
# Generic configuration (shared by all environments)
|
165
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
166
|
+
# Configuration type: generic
|
167
|
+
Host cloudx-*
|
167
168
|
User ec2-user
|
168
|
-
IdentityFile ~/.ssh/vscode/mykey
|
169
|
-
IdentitiesOnly yes
|
170
|
-
ProxyCommand uvx cloudx-proxy connect %h %p --profile myprofile --ssh-key mykey
|
171
169
|
TCPKeepAlive yes
|
172
170
|
ControlMaster auto
|
173
171
|
ControlPath ~/.ssh/control/%r@%h:%p
|
174
172
|
ControlPersist 4h
|
175
173
|
|
176
|
-
#
|
174
|
+
# Environment configuration (specific to a single environment)
|
175
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
176
|
+
# Configuration type: environment
|
177
|
+
Host cloudx-dev-*
|
178
|
+
IdentityFile ~/.ssh/vscode/mykey
|
179
|
+
IdentitiesOnly yes
|
180
|
+
ProxyCommand uvx cloudx-proxy connect %h %p --profile myprofile --ssh-key mykey
|
181
|
+
|
182
|
+
# Host configuration (specific to a single instance)
|
183
|
+
# Created by cloudx-proxy v1.0.0 on 2025-03-07 09:05:23
|
184
|
+
# Configuration type: host
|
177
185
|
Host cloudx-dev-myserver
|
178
186
|
HostName i-0123456789abcdef0
|
179
187
|
```
|
@@ -191,23 +199,54 @@ In these examples, ssh will use cloudx-proxy to connect to AWS with the `myprofi
|
|
191
199
|
VSCode will be able to connect to the instance using the same SSH configuration.
|
192
200
|
|
193
201
|
### SSH Configuration Details
|
194
|
-
The setup command creates
|
202
|
+
The setup command creates a hierarchical three-tier SSH configuration structure:
|
195
203
|
|
196
|
-
1.
|
197
|
-
- User
|
198
|
-
- 1Password SSH agent integration if selected
|
199
|
-
- ProxyCommand with appropriate parameters
|
200
|
-
- SSH multiplexing for better performance
|
204
|
+
1. Generic configuration (cloudx-*) containing common settings shared across all environments:
|
205
|
+
- User settings (ec2-user)
|
201
206
|
- TCP keepalive for connection stability
|
207
|
+
- SSH multiplexing for better performance (ControlMaster, ControlPath, ControlPersist)
|
202
208
|
|
203
|
-
2.
|
204
|
-
-
|
205
|
-
-
|
206
|
-
- Inherits all
|
209
|
+
2. Environment-specific configuration (cloudx-{env}-*) with:
|
210
|
+
- Authentication settings (IdentityFile, IdentityAgent for 1Password)
|
211
|
+
- ProxyCommand with environment-specific parameters
|
212
|
+
- Inherits all settings from the generic configuration
|
213
|
+
|
214
|
+
3. Host-specific entries (cloudx-{env}-hostname) with:
|
215
|
+
- Instance ID (HostName directive)
|
216
|
+
- Inherits all settings from both generic and environment configurations
|
217
|
+
|
218
|
+
Each configuration tier is clearly marked with a timestamp and version information comment, making it easy to track when and how configurations were created.
|
207
219
|
|
208
220
|
When adding new instances to an existing environment, you can choose to:
|
221
|
+
- Keep the existing environment configuration if it's compatible
|
209
222
|
- Override the environment configuration with new settings
|
210
|
-
- Add
|
223
|
+
- Add host-specific settings only
|
224
|
+
|
225
|
+
This three-tier structure offers better maintainability by reducing duplication and making it clear which settings apply broadly and which are specific to an environment or host.
|
226
|
+
|
227
|
+
### Security Model: AWS and SSH Integration
|
228
|
+
|
229
|
+
cloudX-proxy implements a unique dual-layer security approach that combines AWS's robust authentication mechanisms with SSH's connection handling capabilities:
|
230
|
+
|
231
|
+
#### AWS Security Layer (Primary)
|
232
|
+
The primary security boundary is enforced through AWS Systems Manager (SSM) and EC2 Instance Connect. This layer provides:
|
233
|
+
- **Access Control**: Only authenticated AWS users with appropriate IAM permissions can establish SSM sessions
|
234
|
+
- **Dynamic Key Authorization**: EC2 Instance Connect allows temporary injection of SSH public keys, valid only for a single session
|
235
|
+
- **Network Security**: No inbound SSH ports need to be exposed, as all connections are established through AWS SSM's secure tunneling
|
236
|
+
- **Audit Trail**: All connection attempts and key pushes are logged in AWS CloudTrail
|
237
|
+
|
238
|
+
#### SSH Layer (Secondary)
|
239
|
+
SSH serves primarily as a connection handler rather than the main security mechanism:
|
240
|
+
- **Ephemeral Authentication**: The SSH key pair is used only to establish the connection through the SSM tunnel
|
241
|
+
- **Session Management**: SSH handles the actual terminal session, file transfers, and multiplexing
|
242
|
+
- **Key Flexibility**: Since keys are pushed dynamically for each session, the same key can safely be used across multiple instances
|
243
|
+
- **Zero Trust Model**: Even if a key is compromised, access still requires valid AWS credentials and permissions
|
244
|
+
|
245
|
+
This architecture means that:
|
246
|
+
1. The security of the connection relies primarily on AWS IAM permissions and SSM session management
|
247
|
+
2. SSH keys can be reused across instances without security implications
|
248
|
+
3. Each connection gets a fresh key authorization through EC2 Instance Connect
|
249
|
+
4. Instances remain completely closed to direct SSH access from the internet
|
211
250
|
|
212
251
|
### VSCode Configuration
|
213
252
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|