cloudx-proxy 0.4.4__py3-none-any.whl → 0.4.6__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.
- cloudx_proxy/_version.py +2 -2
- cloudx_proxy/cli.py +15 -4
- cloudx_proxy/setup.py +70 -39
- {cloudx_proxy-0.4.4.dist-info → cloudx_proxy-0.4.6.dist-info}/METADATA +25 -1
- cloudx_proxy-0.4.6.dist-info/RECORD +12 -0
- cloudx_proxy-0.4.4.dist-info/RECORD +0 -12
- {cloudx_proxy-0.4.4.dist-info → cloudx_proxy-0.4.6.dist-info}/LICENSE +0 -0
- {cloudx_proxy-0.4.4.dist-info → cloudx_proxy-0.4.6.dist-info}/WHEEL +0 -0
- {cloudx_proxy-0.4.4.dist-info → cloudx_proxy-0.4.6.dist-info}/entry_points.txt +0 -0
- {cloudx_proxy-0.4.4.dist-info → cloudx_proxy-0.4.6.dist-info}/top_level.txt +0 -0
cloudx_proxy/_version.py
CHANGED
cloudx_proxy/cli.py
CHANGED
@@ -55,7 +55,10 @@ def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str
|
|
55
55
|
@click.option('--ssh-config', help='SSH config file to use (default: ~/.ssh/vscode/config)')
|
56
56
|
@click.option('--aws-env', help='AWS environment directory (default: ~/.aws, use name of directory in ~/.aws/aws-envs/)')
|
57
57
|
@click.option('--1password', 'use_1password', is_flag=True, help='Use 1Password SSH agent for SSH authentication')
|
58
|
-
|
58
|
+
@click.option('--instance', help='EC2 instance ID to set up connection for')
|
59
|
+
@click.option('--yes', 'non_interactive', is_flag=True, help='Non-interactive mode, use default values for all prompts')
|
60
|
+
def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str, use_1password: bool,
|
61
|
+
instance: str, non_interactive: bool):
|
59
62
|
"""Set up AWS profile, SSH keys, and configuration for CloudX.
|
60
63
|
|
61
64
|
This command will:
|
@@ -69,6 +72,7 @@ def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str, use_1passwo
|
|
69
72
|
cloudx-proxy setup --profile myprofile --ssh-key mykey
|
70
73
|
cloudx-proxy setup --ssh-config ~/.ssh/cloudx/config
|
71
74
|
cloudx-proxy setup --1password
|
75
|
+
cloudx-proxy setup --instance i-0123456789abcdef0 --yes
|
72
76
|
"""
|
73
77
|
try:
|
74
78
|
setup = CloudXSetup(
|
@@ -76,7 +80,9 @@ def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str, use_1passwo
|
|
76
80
|
ssh_key=ssh_key,
|
77
81
|
ssh_config=ssh_config,
|
78
82
|
aws_env=aws_env,
|
79
|
-
use_1password=use_1password
|
83
|
+
use_1password=use_1password,
|
84
|
+
instance_id=instance,
|
85
|
+
non_interactive=non_interactive
|
80
86
|
)
|
81
87
|
|
82
88
|
print("\n\033[1;95m=== cloudx-proxy Setup ===\033[0m\n")
|
@@ -91,8 +97,13 @@ def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str, use_1passwo
|
|
91
97
|
|
92
98
|
# Get environment and instance details
|
93
99
|
cloudx_env = setup.prompt("Enter environment", getattr(setup, 'default_env', None))
|
94
|
-
|
95
|
-
|
100
|
+
|
101
|
+
# Use the --instance parameter if provided, otherwise prompt
|
102
|
+
instance_id = instance or setup.prompt("Enter EC2 instance ID (e.g., i-0123456789abcdef0)")
|
103
|
+
|
104
|
+
# Generate a default hostname based on instance ID if we're in non-interactive mode
|
105
|
+
hostname_default = f"instance-{instance_id[-7:]}" if non_interactive else None
|
106
|
+
hostname = setup.prompt("Enter hostname for the instance", hostname_default)
|
96
107
|
|
97
108
|
# Set up SSH config
|
98
109
|
if not setup.setup_ssh_config(cloudx_env, instance_id, hostname):
|
cloudx_proxy/setup.py
CHANGED
@@ -10,8 +10,12 @@ from botocore.exceptions import ClientError
|
|
10
10
|
from ._1password import check_1password_cli, check_ssh_agent, list_ssh_keys, create_ssh_key, get_vaults, save_public_key
|
11
11
|
|
12
12
|
class CloudXSetup:
|
13
|
+
# Define SSH key prefix as a constant
|
14
|
+
SSH_KEY_PREFIX = "cloudX SSH Key - "
|
15
|
+
|
13
16
|
def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", ssh_config: str = None,
|
14
|
-
aws_env: str = None, use_1password: bool = False
|
17
|
+
aws_env: str = None, use_1password: bool = False, instance_id: str = None,
|
18
|
+
non_interactive: bool = False):
|
15
19
|
"""Initialize cloudx-proxy setup.
|
16
20
|
|
17
21
|
Args:
|
@@ -20,11 +24,15 @@ class CloudXSetup:
|
|
20
24
|
ssh_config: SSH config file path (default: None, uses ~/.ssh/vscode/config)
|
21
25
|
aws_env: AWS environment directory (default: None)
|
22
26
|
use_1password: Use 1Password SSH agent for authentication (default: False)
|
27
|
+
instance_id: EC2 instance ID to set up connection for (optional)
|
28
|
+
non_interactive: Non-interactive mode, use defaults for all prompts (default: False)
|
23
29
|
"""
|
24
30
|
self.profile = profile
|
25
31
|
self.ssh_key = ssh_key
|
26
32
|
self.aws_env = aws_env
|
27
33
|
self.use_1password = use_1password
|
34
|
+
self.instance_id = instance_id
|
35
|
+
self.non_interactive = non_interactive
|
28
36
|
self.home_dir = str(Path.home())
|
29
37
|
self.onepassword_agent_sock = Path(self.home_dir) / ".1password" / "agent.sock"
|
30
38
|
|
@@ -74,6 +82,16 @@ class CloudXSetup:
|
|
74
82
|
Returns:
|
75
83
|
str: User's input or default value
|
76
84
|
"""
|
85
|
+
# In non-interactive mode, always use the default value
|
86
|
+
if self.non_interactive:
|
87
|
+
if default:
|
88
|
+
self.print_status(f"{message}: Using default [{default}]", None, 2)
|
89
|
+
return default
|
90
|
+
else:
|
91
|
+
self.print_status(f"{message}: No default value available", False, 2)
|
92
|
+
raise ValueError(f"Non-interactive mode requires default value for: {message}")
|
93
|
+
|
94
|
+
# Interactive prompt
|
77
95
|
if default:
|
78
96
|
prompt_text = f"\033[93m{message} [{default}]: \033[0m"
|
79
97
|
else:
|
@@ -200,6 +218,43 @@ class CloudXSetup:
|
|
200
218
|
bool: True if successful
|
201
219
|
"""
|
202
220
|
try:
|
221
|
+
# Create possible title variations for the 1Password item
|
222
|
+
ssh_key_title_with_prefix = f"{self.SSH_KEY_PREFIX}{self.ssh_key}"
|
223
|
+
ssh_key_title_without_prefix = self.ssh_key
|
224
|
+
|
225
|
+
# First check if key exists in any vault
|
226
|
+
ssh_keys = list_ssh_keys()
|
227
|
+
|
228
|
+
# Check for both prefixed and non-prefixed format
|
229
|
+
existing_key = next((key for key in ssh_keys if key['title'] == ssh_key_title_with_prefix), None)
|
230
|
+
if not existing_key:
|
231
|
+
existing_key = next((key for key in ssh_keys if key['title'] == ssh_key_title_without_prefix), None)
|
232
|
+
|
233
|
+
if existing_key:
|
234
|
+
key_title = existing_key['title']
|
235
|
+
self.print_status(f"SSH key '{key_title}' already exists in 1Password", True, 2)
|
236
|
+
# Get the public key
|
237
|
+
result = subprocess.run(
|
238
|
+
['op', 'item', 'get', existing_key['id'], '--fields', 'public key'],
|
239
|
+
capture_output=True,
|
240
|
+
text=True,
|
241
|
+
check=False
|
242
|
+
)
|
243
|
+
|
244
|
+
if result.returncode == 0:
|
245
|
+
public_key = result.stdout.strip()
|
246
|
+
# Save it to the expected location
|
247
|
+
if save_public_key(public_key, f"{self.ssh_key_file}.pub"):
|
248
|
+
self.print_status(f"Saved existing public key to {self.ssh_key_file}.pub", True, 2)
|
249
|
+
return True
|
250
|
+
else:
|
251
|
+
self.print_status(f"Failed to save public key to {self.ssh_key_file}.pub", False, 2)
|
252
|
+
return False
|
253
|
+
else:
|
254
|
+
self.print_status(f"Failed to retrieve public key from 1Password", False, 2)
|
255
|
+
return False
|
256
|
+
|
257
|
+
# If we reach here, the key doesn't exist and we need to create it
|
203
258
|
# Get vaults to determine where to store the key
|
204
259
|
vaults = get_vaults()
|
205
260
|
if not vaults:
|
@@ -223,48 +278,24 @@ class CloudXSetup:
|
|
223
278
|
except ValueError:
|
224
279
|
self.print_status("Invalid input", False, 2)
|
225
280
|
return False
|
281
|
+
|
282
|
+
# Create a new SSH key in 1Password
|
283
|
+
self.print_status(f"Creating new SSH key '{ssh_key_title_with_prefix}' in 1Password...", None, 2)
|
284
|
+
success, public_key, item_id = create_ssh_key(ssh_key_title_with_prefix, selected_vault)
|
226
285
|
|
227
|
-
|
228
|
-
|
286
|
+
if not success:
|
287
|
+
self.print_status("Failed to create SSH key in 1Password", False, 2)
|
288
|
+
return False
|
229
289
|
|
230
|
-
|
231
|
-
ssh_keys = list_ssh_keys()
|
232
|
-
existing_key = next((key for key in ssh_keys if key['title'] == ssh_key_title), None)
|
290
|
+
self.print_status("SSH key created successfully in 1Password", True, 2)
|
233
291
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
['op', 'item', 'get', existing_key['id'], '--fields', 'public key'],
|
239
|
-
capture_output=True,
|
240
|
-
text=True,
|
241
|
-
check=False
|
242
|
-
)
|
243
|
-
|
244
|
-
if result.returncode == 0:
|
245
|
-
public_key = result.stdout.strip()
|
246
|
-
# Save it to the expected location
|
247
|
-
if save_public_key(public_key, f"{self.ssh_key_file}.pub"):
|
248
|
-
self.print_status(f"Saved existing public key to {self.ssh_key_file}.pub", True, 2)
|
249
|
-
return True
|
292
|
+
# Save the public key to the expected location
|
293
|
+
if save_public_key(public_key, f"{self.ssh_key_file}.pub"):
|
294
|
+
self.print_status(f"Saved public key to {self.ssh_key_file}.pub", True, 2)
|
295
|
+
return True
|
250
296
|
else:
|
251
|
-
|
252
|
-
|
253
|
-
success, public_key, item_id = create_ssh_key(ssh_key_title, selected_vault)
|
254
|
-
|
255
|
-
if not success:
|
256
|
-
self.print_status("Failed to create SSH key in 1Password", False, 2)
|
257
|
-
return False
|
258
|
-
|
259
|
-
self.print_status("SSH key created successfully in 1Password", True, 2)
|
260
|
-
|
261
|
-
# Save the public key to the expected location
|
262
|
-
if save_public_key(public_key, f"{self.ssh_key_file}.pub"):
|
263
|
-
self.print_status(f"Saved public key to {self.ssh_key_file}.pub", True, 2)
|
264
|
-
return True
|
265
|
-
else:
|
266
|
-
self.print_status(f"Failed to save public key to {self.ssh_key_file}.pub", False, 2)
|
267
|
-
return False
|
297
|
+
self.print_status(f"Failed to save public key to {self.ssh_key_file}.pub", False, 2)
|
298
|
+
return False
|
268
299
|
|
269
300
|
# Remind user to enable the key in 1Password SSH agent
|
270
301
|
self.print_status("\033[93mImportant: Make sure the key is enabled in 1Password's SSH agent settings\033[0m", None, 2)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: cloudx-proxy
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.6
|
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
|
@@ -224,6 +224,30 @@ When adding new instances to an existing environment, you can choose to:
|
|
224
224
|
|
225
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
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
|
250
|
+
|
227
251
|
### VSCode Configuration
|
228
252
|
|
229
253
|
1. Install the "Remote - SSH" extension in VSCode
|
@@ -0,0 +1,12 @@
|
|
1
|
+
cloudx_proxy/_1password.py,sha256=uxyCfVvO1eQrOfYRojst_LN2DV4fIwxM5moaQTn3wQY,5853
|
2
|
+
cloudx_proxy/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
|
3
|
+
cloudx_proxy/_version.py,sha256=KTwoycq1UXsZO5AtXG8jzeyeaE1ORqxkwO9Pn0qrlwg,511
|
4
|
+
cloudx_proxy/cli.py,sha256=VqYdcTn_PC-mhcCpNEa_PcpHjV7vxIDzhfxu-WKlDdU,4904
|
5
|
+
cloudx_proxy/core.py,sha256=RF3bX5MQiokRKjYEPnfWdKywGdtoVUvV2xZqm9uOl1g,8135
|
6
|
+
cloudx_proxy/setup.py,sha256=QrInbZdghpAYn064h02KmpA9r-nEbVm-LyTcXLxSmXY,39823
|
7
|
+
cloudx_proxy-0.4.6.dist-info/LICENSE,sha256=i7P2OR4zsJYsMWcCUDe_B9ZfGi9bU0K5I2nKfDrW_N8,1068
|
8
|
+
cloudx_proxy-0.4.6.dist-info/METADATA,sha256=LV5xMR7w2qrmcID563IlI83R7X2SOxzcRo_qKLbBa9k,18522
|
9
|
+
cloudx_proxy-0.4.6.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
10
|
+
cloudx_proxy-0.4.6.dist-info/entry_points.txt,sha256=HGt743N2lVlKd7O1qWq3C0aEHyS5PjPnxzDHh7hwtSg,54
|
11
|
+
cloudx_proxy-0.4.6.dist-info/top_level.txt,sha256=2wtEote1db21j-VvkCJFfT-dLlauuG5indjggYh3xDg,13
|
12
|
+
cloudx_proxy-0.4.6.dist-info/RECORD,,
|
@@ -1,12 +0,0 @@
|
|
1
|
-
cloudx_proxy/_1password.py,sha256=uxyCfVvO1eQrOfYRojst_LN2DV4fIwxM5moaQTn3wQY,5853
|
2
|
-
cloudx_proxy/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
|
3
|
-
cloudx_proxy/_version.py,sha256=rQpExgwkkSMYhPbtDdfhSejoe7mM9tgzyWoNno0mgIw,511
|
4
|
-
cloudx_proxy/cli.py,sha256=5IcfYFACUOa4pqSKuHucqZionI9P8n5ZLvtzyXYeTvw,4218
|
5
|
-
cloudx_proxy/core.py,sha256=RF3bX5MQiokRKjYEPnfWdKywGdtoVUvV2xZqm9uOl1g,8135
|
6
|
-
cloudx_proxy/setup.py,sha256=11DsAVt6L4d3VmuJNpU07-QdF3FwahOUpJyLDksMeaE,38216
|
7
|
-
cloudx_proxy-0.4.4.dist-info/LICENSE,sha256=i7P2OR4zsJYsMWcCUDe_B9ZfGi9bU0K5I2nKfDrW_N8,1068
|
8
|
-
cloudx_proxy-0.4.4.dist-info/METADATA,sha256=22Yrnn7514Dd8eYte1c03xXpSjMhgATvwIftn1A95iQ,16757
|
9
|
-
cloudx_proxy-0.4.4.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
10
|
-
cloudx_proxy-0.4.4.dist-info/entry_points.txt,sha256=HGt743N2lVlKd7O1qWq3C0aEHyS5PjPnxzDHh7hwtSg,54
|
11
|
-
cloudx_proxy-0.4.4.dist-info/top_level.txt,sha256=2wtEote1db21j-VvkCJFfT-dLlauuG5indjggYh3xDg,13
|
12
|
-
cloudx_proxy-0.4.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|