Skip to content

Commit 4ff3af4

Browse files
authored
Validate SSH Example + Extra documentation (#30)
1 parent f08144c commit 4ff3af4

File tree

9 files changed

+522
-135
lines changed

9 files changed

+522
-135
lines changed

ark_sdk_python/common/ark_jwt_utils.py

+10
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,13 @@ def get_unverified_claims(token: str) -> Dict[str, Any]:
1010
token,
1111
options={'verify_signature': False},
1212
)
13+
14+
@staticmethod
15+
def get_subdomain_from_token(token: str) -> str:
16+
claims = ArkJWTUtils.get_unverified_claims(token)
17+
return claims.get('subdomain', '')
18+
19+
@staticmethod
20+
def get_platform_domain_from_token(token: str) -> str:
21+
claims = ArkJWTUtils.get_unverified_claims(token)
22+
return claims.get('platform_domain', '')
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ark_sdk_python.common.connections.ssh.ark_pty_ssh_connection import ArkPTYSSHConnection
12
from ark_sdk_python.common.connections.ssh.ark_ssh_connection import SSH_PORT, ArkSSHConnection
23

3-
__all__ = ['ArkSSHConnection', 'SSH_PORT']
4+
__all__ = ['ArkSSHConnection', 'ArkPTYSSHConnection', 'SSH_PORT']
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
from typing import Any, Optional
6+
7+
from overrides import overrides
8+
from pexpect import pxssh
9+
10+
from ark_sdk_python.common.connections.ark_connection import ArkConnection
11+
from ark_sdk_python.common.connections.ssh.ark_ssh_connection import SSH_PORT
12+
from ark_sdk_python.models import ArkException
13+
from ark_sdk_python.models.common.connections import ArkConnectionCommand, ArkConnectionDetails, ArkConnectionResult
14+
15+
16+
class ArkPTYSSHConnection(ArkConnection):
17+
__ANSI_STRIPPER: Any = re.compile(r'\x1b[^m]*m|\x1b\[\?2004[hl]')
18+
19+
def __init__(self):
20+
super().__init__()
21+
self.__is_connected: bool = False
22+
self.__is_suspended: bool = False
23+
self.__ssh_client: Optional[pxssh.pxssh] = None
24+
25+
@overrides
26+
def connect(self, connection_details: ArkConnectionDetails) -> None:
27+
"""
28+
Performs SSH connection with given details or keys
29+
Saves the ssh session to be used for command executions
30+
31+
Args:
32+
connection_details (ArkConnectionDetails): _description_
33+
34+
Raises:
35+
ArkException: _description_
36+
"""
37+
if self.__is_connected:
38+
return
39+
try:
40+
target_port = SSH_PORT
41+
user = None
42+
if connection_details.port:
43+
target_port = connection_details.port
44+
credentials_map = {}
45+
if connection_details.credentials:
46+
user = connection_details.credentials.user
47+
if connection_details.credentials.password:
48+
credentials_map['password'] = connection_details.credentials.password.get_secret_value()
49+
elif connection_details.credentials.private_key_filepath:
50+
path = Path(connection_details.credentials.private_key_filepath)
51+
if not path.exists():
52+
raise ArkException(f'Given private key path [{path}] does not exist')
53+
credentials_map['ssh_key'] = connection_details.credentials.private_key_filepath
54+
self.__ssh_client = pxssh.pxssh()
55+
self.__ssh_client.login(
56+
server=connection_details.address,
57+
username=user,
58+
port=target_port,
59+
login_timeout=30,
60+
**credentials_map,
61+
)
62+
self.__is_connected = True
63+
self.__is_suspended = False
64+
except Exception as ex:
65+
raise ArkException(f'Failed to ssh connect [{str(ex)}]') from ex
66+
67+
@overrides
68+
def disconnect(self) -> None:
69+
"""
70+
Disconnects the ssh session
71+
"""
72+
if not self.__is_connected:
73+
return
74+
self.__ssh_client.logout()
75+
self.__ssh_client = None
76+
self.__is_connected = False
77+
self.__is_suspended = False
78+
79+
@overrides
80+
def suspend_connection(self) -> None:
81+
"""
82+
Suspends execution of ssh commands
83+
"""
84+
self.__is_suspended = True
85+
86+
@overrides
87+
def restore_connection(self) -> None:
88+
"""
89+
Restores execution of ssh commands
90+
"""
91+
self.__is_suspended = False
92+
93+
@overrides
94+
def is_suspended(self) -> bool:
95+
"""
96+
Checks whether ssh commands can be executed or not
97+
98+
Returns:
99+
bool: _description_
100+
"""
101+
return self.__is_suspended
102+
103+
@overrides
104+
def is_connected(self) -> bool:
105+
"""
106+
Checks whether theres a ssh session connected
107+
108+
Returns:
109+
bool: _description_
110+
"""
111+
return self.__is_connected
112+
113+
@overrides
114+
def run_command(self, command: ArkConnectionCommand) -> ArkConnectionResult:
115+
"""
116+
Runs a command over ssh session, returning the result accordingly
117+
118+
Args:
119+
command (ArkConnectionCommand): _description_
120+
121+
Raises:
122+
ArkException: _description_
123+
124+
Returns:
125+
ArkConnectionResult: _description_
126+
"""
127+
if not self.__is_connected or self.__is_suspended:
128+
raise ArkException('Cannot run command while not being connected')
129+
self._logger.debug(f'Running command [{command.command}]')
130+
self.__ssh_client.sendline(command.command)
131+
self.__ssh_client.prompt()
132+
stdout = self.__ssh_client.before.decode()
133+
self.__ssh_client.sendline('echo $?')
134+
self.__ssh_client.prompt()
135+
exit_code_output = self.__ssh_client.before.decode()
136+
exit_code_output = ArkPTYSSHConnection.__ANSI_STRIPPER.sub('', exit_code_output)
137+
rc = int(exit_code_output.strip().splitlines()[-1])
138+
if rc != command.expected_rc and command.raise_on_error:
139+
raise ArkException(f'Failed to execute command [{command.command}] - [{rc}] - [{stdout}]')
140+
self._logger.debug(f'Command rc: [{rc}]')
141+
self._logger.debug(f'Command stdout: [{stdout}]')
142+
return ArkConnectionResult(stdout=stdout, rc=rc)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import argparse
2+
import tempfile
3+
4+
from ark_sdk_python.auth import ArkISPAuth
5+
from ark_sdk_python.common.ark_jwt_utils import ArkJWTUtils
6+
from ark_sdk_python.common.connections.ssh.ark_pty_ssh_connection import ArkPTYSSHConnection
7+
from ark_sdk_python.models.auth import ArkAuthMethod, ArkAuthProfile, ArkSecret
8+
from ark_sdk_python.models.auth.ark_auth_method import IdentityServiceUserArkAuthMethodSettings
9+
from ark_sdk_python.models.common.connections.ark_connection_command import ArkConnectionCommand
10+
from ark_sdk_python.models.common.connections.ark_connection_credentials import ArkConnectionCredentials
11+
from ark_sdk_python.models.common.connections.ark_connection_details import ArkConnectionDetails
12+
from ark_sdk_python.models.services.sia.sso.ark_sia_sso_get_ssh_key import ArkSIASSOGetSSHKey
13+
from ark_sdk_python.services.sia.sso import ArkSIASSOService
14+
15+
16+
def login_to_identity_security_platform(service_user: str, service_token: str, application_name: str) -> ArkISPAuth:
17+
"""
18+
This will perform login to the tenant with the given service user credentials
19+
Caching the logged in token is disabled and will perform a full login on each call
20+
Will return an identity security platform authenticated class
21+
22+
Args:
23+
service_user (str): _description_
24+
service_token (str): _description_
25+
application_name (str): _description_
26+
27+
Returns:
28+
ArkISPAuth: _description_
29+
"""
30+
print('Logging in to the tenant')
31+
isp_auth = ArkISPAuth(cache_authentication=False)
32+
isp_auth.authenticate(
33+
auth_profile=ArkAuthProfile(
34+
username=service_user,
35+
auth_method=ArkAuthMethod.IdentityServiceUser,
36+
auth_method_settings=IdentityServiceUserArkAuthMethodSettings(identity_authorization_application=application_name),
37+
),
38+
secret=ArkSecret(secret=service_token),
39+
)
40+
print('Logged in successfully')
41+
return isp_auth
42+
43+
44+
def construct_ssh_proxy_address(isp_auth: ArkISPAuth) -> str:
45+
"""
46+
For a given authentication, construct the SSH proxy address
47+
This'll grab from the ID token of the logged in user the name of the tenant (subdomain)
48+
And the platform domain (cyberark.cloud)
49+
And will concatenante everything to construct the SSH proxy address
50+
will return the constructed ssh proxy address
51+
52+
Args:
53+
isp_auth (ArkISPAuth): _description_
54+
55+
Returns:
56+
str: _description_
57+
"""
58+
token = isp_auth.token.token.get_secret_value()
59+
return f'{ArkJWTUtils.get_subdomain_from_token(token)}.ssh.{ArkJWTUtils.get_platform_domain_from_token(token)}'
60+
61+
62+
def generate_mfa_caching_ssh_key(isp_auth: ArkISPAuth, folder: str) -> str:
63+
"""
64+
This function will use the logged in user
65+
And generate an SSH key into a given folder
66+
Will return the path to the SSH key
67+
68+
Args:
69+
isp_auth (ArkISPAuth): _description_
70+
temp_folder (str): _description_
71+
72+
Returns:
73+
str: _description_
74+
"""
75+
print('Generating SSH MFA Caching Key')
76+
sso_service = ArkSIASSOService(isp_auth=isp_auth)
77+
path = sso_service.short_lived_ssh_key(get_ssh_key=ArkSIASSOGetSSHKey(folder=folder))
78+
print(f'MFA Caching SSH Key generated to [{path}]')
79+
return path
80+
81+
82+
def connect_and_validate_connection(ssh_key_path: str, proxy_address: str, connection_string: str, command: str) -> None:
83+
"""
84+
This function will perform a connection via SSH and via SIA proxy to the target
85+
For the given proxy address and connection string, a connection will be made with the ssh key
86+
Once connected, the given command will be performed, expecting a return code of 0 as success
87+
88+
Args:
89+
ssh_key_path (str): _description_
90+
proxy_address (str): _description_
91+
connection_string (str): _description_
92+
command (str): _description_
93+
"""
94+
print(f'Connecting to proxy [{proxy_address}] and connection string [{connection_string}]')
95+
ssh_connection = ArkPTYSSHConnection()
96+
ssh_connection.connect(
97+
ArkConnectionDetails(
98+
address=proxy_address,
99+
credentials=ArkConnectionCredentials(
100+
user=connection_string,
101+
private_key_filepath=ssh_key_path,
102+
),
103+
),
104+
)
105+
print(f'Running test command [{command}]')
106+
ssh_connection.run_command(
107+
ArkConnectionCommand(command=command, expected_rc=0),
108+
)
109+
print('Finished Successfully!')
110+
111+
112+
if __name__ == '__main__':
113+
# Construct an argument parser for CLI parameters for the script
114+
parser = argparse.ArgumentParser()
115+
parser.add_argument('--service-user', required=True, help='Service user to login and perform the operation with')
116+
parser.add_argument('--service-token', required=True, help='Service user token to use for logging in and connecting')
117+
parser.add_argument(
118+
'--service-application',
119+
default='__idaptive_cybr_user_oidc',
120+
help='Service application to login to, defaults to cyberark ISP application',
121+
)
122+
parser.add_argument(
123+
'--connection-string',
124+
required=True,
125+
help='SSH connection string to use for the connection, without the proxy address.'
126+
'A connection string may look like the following'
127+
'<service-user>#<tenant_subdomain>@<target_user>@<target_address>#<optional_network_name>',
128+
)
129+
parser.add_argument('--test-command', default='ls -l', help='Test command to use once connected to the target, defaults to ls -l')
130+
131+
# Parse the CLI parameters
132+
args = parser.parse_args()
133+
134+
# Construct a temporary folder to be used for the MFA Caching SSH key generation
135+
# At the end of the execution, the temporary folder will be deleted alongside the SSH key
136+
with tempfile.TemporaryDirectory() as temp_folder:
137+
# Perform the flow:
138+
# - Login to the tenant
139+
# - Generate an MFA Caching SSH key
140+
# - Construct the ssh proxy address
141+
# - Connect in SSH to the proxy / target with the MFA Caching SSH Key and perform the command
142+
isp_auth = login_to_identity_security_platform(args.service_user, args.service_token, args.service_application)
143+
ssh_key_path = generate_mfa_caching_ssh_key(isp_auth, temp_folder)
144+
proxy_address = construct_ssh_proxy_address(isp_auth)
145+
connect_and_validate_connection(ssh_key_path, proxy_address, args.connection_string, args.test_command)

docs/examples/commands_examples.md

+26-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,21 @@ ark_public exec sia secrets vm add-secret --secret-type ProvisionerUser --provis
125125
ark exec sia access connector-setup-script -ct onprem -co windows -cpi 588741d5-e059-479d-b4c4-3d821a87f012
126126
```
127127

128+
### Install a DPA Windows Connector Remotely
129+
```shell
130+
ark exec sia access install-connector --connector-pool-id abcd --connector-type onprem --connector-os windows --target-machine 1.2.3.4 --username myuser --password mypassword
131+
```
132+
133+
### Install a DPA Linux Connector Remotely
134+
```shell
135+
ark exec sia access install-connector --connector-pool-id abcd --connector-type aws --connector-os linux --target-machine 1.2.3.4 --username ec2-user --private-key-path /path/to/key.pem
136+
```
137+
138+
### Delete and uninstall a DPA Connector
139+
```shell
140+
ark exec sia access delete-connector --connector-id=CMSConnector_e9685e0d-a92e-4097-ad4d-b54eadb69bcb_81fa03c5-d0d3-4157-95f8-6a1903900fa0 --uninstall-connector --target-machine 1.2.3.4 --username ec2-user --private-key-path /path/to/key.pem
141+
```
142+
128143
### List All Session Monitoring sessions from the last 24 hours
129144
```shell
130145
ark exec sm list-sessions
@@ -237,5 +252,15 @@ ark exec pcloud platforms list-platforms
237252

238253
### List CMGR connector pools
239254
```shell
240-
ark exec exec cmgr list-pools
255+
ark exec cmgr list-pools
256+
```
257+
258+
### Add CMGR network
259+
```shell
260+
ark exec cmgr add-network --name mynetwork
261+
```
262+
263+
### Add CMGR connector pool
264+
```shell
265+
ark exec cmgr add-pool --name mypool --assigned-network-ids mynetwork_id
241266
```

docs/howto/install_sia_connectors.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
title: Install SIA connectors
3+
description: Install SIA connectors
4+
---
5+
6+
# Install SIA connectors
7+
Here is an example workflow for installing a connector on a linux / windows box:
8+
9+
1. Install Ark SDK: `pip3 install ark-sdk-python`
10+
1. Create a profile:
11+
* Interactively:
12+
```shell linenums="0"
13+
ark configure
14+
```
15+
* Silently:
16+
```shell linenums="0"
17+
ark configure --silent --work-with-isp --isp-username myuser
18+
```
19+
1. Log in to Ark:
20+
```shell linenums="0"
21+
ark login --silent --isp-secret <my-ark-secret>
22+
```
23+
1. Create a network and connector pool:
24+
```shell linenums="0"
25+
ark exec cmgr add-network --name mynetwork
26+
ark exec cmgr add-pool --name mypool --assigned-network-ids mynetwork_id
27+
```
28+
1. Install a connector:
29+
* Windows:
30+
```shell linenums="0"
31+
ark exec sia access install-connector --connector-pool-id mypool_id --connector-type onprem --connector-os windows --target-machine 1.2.3.4 --username myuser --password mypassword
32+
```
33+
* Linux:
34+
```shell linenums="0"
35+
ark exec sia access install-connector --connector-pool-id mypool_id --connector-type aws --connector-os linux --target-machine 1.2.3.4 --username ec2-user --private-key-path /path/to/key.pem
36+
```

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ nav:
1616
- End-user Kubernetes workflow: howto/enduser_kubernetes_workflow.md
1717
- End-user SSH workflow: howto/enduser_ssh_workflow.md
1818
- End-user RDP workflow: howto/enduser_rdp_workflow.md
19+
- Install SIA connectors: howto/install_sia_connectors.md
1920
- Simple commands workflow: howto/simple_commands_workflow.md
2021
- Simple SDK workflow: howto/simple_sdk_workflow.md
2122
- Refresh authentication: howto/refreshing_authentication.md

0 commit comments

Comments
 (0)