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.
Files changed (23) hide show
  1. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/CHANGELOG.md +14 -0
  2. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/PKG-INFO +59 -20
  3. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/README.md +58 -19
  4. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/_version.py +2 -2
  5. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/cli.py +2 -0
  6. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/setup.py +278 -129
  7. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/PKG-INFO +59 -20
  8. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/.github/workflows/release.yml +0 -0
  9. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/.gitignore +0 -0
  10. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/.releaserc +0 -0
  11. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/CONTRIBUTING.md +0 -0
  12. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/LICENSE +0 -0
  13. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/_1password.py +0 -0
  14. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/__init__.py +0 -0
  15. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy/core.py +0 -0
  16. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/SOURCES.txt +0 -0
  17. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/dependency_links.txt +0 -0
  18. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/entry_points.txt +0 -0
  19. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/requires.txt +0 -0
  20. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/cloudx_proxy.egg-info/top_level.txt +0 -0
  21. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/package.json +0 -0
  22. {cloudx_proxy-0.4.3 → cloudx_proxy-0.4.5}/pyproject.toml +0 -0
  23. {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
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
- # Base environment config (created once per environment)
165
- # Environment-wide configuration
166
- Host cloudx-dev-*
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
- # Minimal host entry (inherits all settings from environment config)
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 an optimized SSH configuration structure:
202
+ The setup command creates a hierarchical three-tier SSH configuration structure:
195
203
 
196
- 1. A base configuration for each environment (cloudx-{env}-*) with:
197
- - User and key settings
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. Minimal host entries for each instance:
204
- - Uses consistent naming (cloudx-{env}-hostname)
205
- - Only contains the HostName directive for the instance ID
206
- - Inherits all environment-level settings automatically
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 instance-specific settings while preserving the environment config
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
- # Base environment config (created once per environment)
115
- # Environment-wide configuration
116
- Host cloudx-dev-*
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
- # Minimal host entry (inherits all settings from environment config)
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 an optimized SSH configuration structure:
152
+ The setup command creates a hierarchical three-tier SSH configuration structure:
145
153
 
146
- 1. A base configuration for each environment (cloudx-{env}-*) with:
147
- - User and key settings
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. Minimal host entries for each instance:
154
- - Uses consistent naming (cloudx-{env}-hostname)
155
- - Only contains the HostName directive for the instance ID
156
- - Inherits all environment-level settings automatically
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 instance-specific settings while preserving the environment config
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
 
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.4.3'
21
- __version_tuple__ = version_tuple = (0, 4, 3)
20
+ __version__ = version = '0.4.5'
21
+ __version_tuple__ = version_tuple = (0, 4, 5)
@@ -40,6 +40,8 @@ def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str
40
40
  aws_env=aws_env
41
41
  )
42
42
 
43
+ client.log(f"cloudx-proxy@{__version__} Connecting to instance {instance_id} on port {port}...")
44
+
43
45
  if not client.connect():
44
46
  sys.exit(1)
45
47
 
@@ -224,15 +224,21 @@ class CloudXSetup:
224
224
  self.print_status("Invalid input", False, 2)
225
225
  return False
226
226
 
227
- # Create a title for the 1Password item
228
- ssh_key_title = f"cloudX SSH Key - {self.ssh_key}"
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 this title already exists in 1Password
231
+ # Check if a key with either title exists in 1Password
231
232
  ssh_keys = list_ssh_keys()
232
- existing_key = next((key for key in ssh_keys if key['title'] == ssh_key_title), None)
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
- self.print_status(f"SSH key '{ssh_key_title}' already exists in 1Password", True, 2)
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 '{ssh_key_title}' in 1Password...", None, 2)
253
- success, public_key, item_id = create_ssh_key(ssh_key_title, selected_vault)
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 _build_environment_config(self, cloudx_env: str) -> str:
387
- """Build an environment-wide configuration block with all common settings.
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: Complete environment configuration block
408
+ str: Formatted timestamp
394
409
  """
395
- host_entry = f"""
396
- Host cloudx-{cloudx_env}-*
397
- User ec2-user
398
- """
399
- # Add authentication configuration
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
- # Add ProxyCommand
403
- host_entry += f""" ProxyCommand {self._build_proxy_command()}
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
- host_entry += f""" TCPKeepAlive yes
413
- ControlMaster auto
437
+ config += f""" ControlMaster auto
414
438
  ControlPath {control_path}
415
439
  ControlPersist 4h
416
440
  """
417
441
 
418
- return host_entry
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 minimal host configuration block that inherits from the environment.
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: Minimal host configuration block with only hostname
480
+ str: Host configuration block
430
481
  """
431
- return f"""
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
- # Generate the host entry using the consolidated helper method
450
- host_entry = self._build_host_config(cloudx_env, hostname, instance_id)
451
-
452
- # Append host entry
453
- with open(self.ssh_config_file, 'a') as f:
454
- f.write(host_entry)
455
- self.print_status("Host entry added with settings", True, 2)
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 manages the SSH configuration in ~/.ssh/vscode/config, with the following behavior:
502
- 1. For a new environment (if cloudx-{env}-* doesn't exist):
503
- Creates a base config with:
504
- - User and key configuration
505
- - 1Password SSH agent integration if selected
506
- - ProxyCommand using uvx cloudx-proxy with proper parameters
507
- - SSH multiplexing configuration (ControlMaster, ControlPath, ControlPersist)
508
-
509
- 2. For an existing environment:
510
- - Skips creating duplicate environment config
511
- - Only adds the new host entry
512
-
513
- Example config structure:
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
- # Build base configuration with wildcard hostname pattern
586
- # Start with a header comment
587
- base_config = """# cloudx-proxy SSH Configuration
588
- # Environment configuration with settings applied to all hosts in this environment
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
- # Add environment-wide configuration with all common settings
592
- base_config += self._build_environment_config(cloudx_env)
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
- # If file exists, append the new config, otherwise create it
595
- if self.ssh_config_file.exists():
596
- with open(self.ssh_config_file, 'a') as f:
597
- f.write("\n" + base_config)
598
- else:
599
- self.ssh_config_file.write_text(base_config)
600
- self.print_status("Base configuration created", True, 2)
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 (owner read/write)
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 specific host entry - only specifying the hostname
609
- self.print_status(f"Adding host entry for cloudx-{cloudx_env}-{hostname}", None, 2)
610
- host_entry = self._build_host_config(cloudx_env, hostname, instance_id)
611
- with open(self.ssh_config_file, 'a') as f:
612
- f.write(host_entry)
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
- # If our config file is the system config, we're done
626
- if self.ssh_config_file.samefile(system_config_path) if self.ssh_config_file.exists() and system_config_path.exists() else str(self.ssh_config_file) == str(system_config_path):
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
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
- # Base environment config (created once per environment)
165
- # Environment-wide configuration
166
- Host cloudx-dev-*
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
- # Minimal host entry (inherits all settings from environment config)
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 an optimized SSH configuration structure:
202
+ The setup command creates a hierarchical three-tier SSH configuration structure:
195
203
 
196
- 1. A base configuration for each environment (cloudx-{env}-*) with:
197
- - User and key settings
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. Minimal host entries for each instance:
204
- - Uses consistent naming (cloudx-{env}-hostname)
205
- - Only contains the HostName directive for the instance ID
206
- - Inherits all environment-level settings automatically
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 instance-specific settings while preserving the environment config
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