-
Notifications
You must be signed in to change notification settings - Fork 17
Add validations on EC2 enclave pre-init #1159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
77b457a
4fc8c15
130340d
278152a
6929f3a
1daba54
bdfe34b
01c3850
d8e2663
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| Flask==2.3.2 | ||
| Werkzeug==3.0.3 | ||
| setuptools==70.0.0 | ||
| requests==2.32.3 | ||
| boto3==1.35.59 | ||
| urllib3==2.2.3 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| import boto3 | ||
| import json | ||
| import os | ||
| import subprocess | ||
| import re | ||
| import multiprocessing | ||
| import requests | ||
| import signal | ||
| import argparse | ||
| from botocore.exceptions import ClientError | ||
| import sys | ||
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
| from confidential_compute import ConfidentialCompute | ||
|
|
||
| class EC2(ConfidentialCompute): | ||
|
|
||
| def __init__(self): | ||
| super().__init__() | ||
| self.config = {} | ||
|
|
||
| def __get_aws_token(self): | ||
| try: | ||
| token_url = "http://169.254.169.254/latest/api/token" | ||
| token_response = requests.put(token_url, headers={"X-aws-ec2-metadata-token-ttl-seconds": "3600"}, timeout=2) | ||
| return token_response.text | ||
| except Exception as e: | ||
| return "blank" | ||
|
||
|
|
||
| def __get_current_region(self): | ||
| token = self.__get_aws_token() | ||
| metadata_url = "http://169.254.169.254/latest/dynamic/instance-identity/document" | ||
| headers = {"X-aws-ec2-metadata-token": token} | ||
| try: | ||
| response = requests.get(metadata_url, headers=headers,timeout=2) | ||
| if response.status_code == 200: | ||
| return response.json().get("region") | ||
| else: | ||
| print(f"Failed to fetch region, status code: {response.status_code}") | ||
abuabraham-ttd marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| except Exception as e: | ||
| raise Exception(f"Region not found, are you running in EC2 environment. {e}") | ||
|
|
||
| def _get_secret(self, secret_identifier): | ||
| client = boto3.client("secretsmanager", region_name=self.__get_current_region()) | ||
| try: | ||
| secret = client.get_secret_value(SecretId=secret_identifier) | ||
| return json.loads(secret["SecretString"]) | ||
| except ClientError as e: | ||
| raise Exception("Unable to access secret store") | ||
abuabraham-ttd marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def __add_defaults(self, configs): | ||
|
||
| configs.setdefault("enclave_memory_mb", 24576) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where are these default coming from? Why are they reasonable? |
||
| configs.setdefault("enclave_cpu_count", 6) | ||
| configs.setdefault("debug_mode", False) | ||
| return configs | ||
|
|
||
| def __setup_vsockproxy(self, log_level): | ||
| thread_count = int((multiprocessing.cpu_count() + 1) // 2) | ||
| log_level = log_level | ||
|
||
| try: | ||
| subprocess.Popen(["/usr/bin/vsockpx", "-c", "/etc/uid2operator/proxy.yaml", "--workers", str(thread_count), "--log-level", log_level, "--daemon"]) | ||
|
||
| print("VSOCK proxy is now running in the background") | ||
| except FileNotFoundError: | ||
| print("Error: vsockpx not found. Please ensure the path is correct") | ||
| except Exception as e: | ||
| print("Failed to start VSOCK proxy") | ||
|
|
||
| def __run_config_server(self, log_level): | ||
| os.makedirs("/etc/secret/secret-value", exist_ok=True) | ||
| with open('/etc/secret/secret-value/config', 'w') as fp: | ||
| json.dump(self.configs, fp) | ||
| os.chdir("/opt/uid2operator/config-server") | ||
| # TODO: Add --log-level to flask. | ||
| try: | ||
| subprocess.Popen(["./bin/flask", "run", "--host", "127.0.0.1", "--port", "27015"]) | ||
| print("Config server is now running in the background.") | ||
| except Exception as e: | ||
| print(f"Failed to start config server: {e}") | ||
|
|
||
| def __run_socks_proxy(self, log_level): | ||
| subprocess.Popen(["sockd", "-d"]) | ||
|
|
||
| def __get_secret_name_from_userdata(self): | ||
| token = self.__get_aws_token() | ||
| user_data_url = "http://169.254.169.254/latest/user-data" | ||
| user_data_response = requests.get(user_data_url, headers={"X-aws-ec2-metadata-token": token}) | ||
|
||
| user_data = user_data_response.text | ||
| identity_scope = open("/opt/uid2operator/identity_scope.txt").read().strip() | ||
|
||
| default_name = "{}-operator-config-key".format(identity_scope.lower()) | ||
| hardcoded_value = "{}_CONFIG_SECRET_KEY".format(identity_scope.upper()) | ||
| match = re.search(rf'^export {hardcoded_value}="(.+?)"$', user_data, re.MULTILINE) | ||
| return match.group(1) if match else default_name | ||
|
|
||
| def _setup_auxilaries(self): | ||
| hostname = os.getenv("HOSTNAME", default=os.uname()[1]) | ||
| file_path = "HOSTNAME" | ||
|
||
| try: | ||
| with open(file_path, "w") as file: | ||
| file.write(hostname) | ||
| print(f"Hostname '{hostname}' written to {file_path}") | ||
| except Exception as e: | ||
| print(f"An error occurred : {e}") | ||
| config = self._get_secret(self.__get_secret_name_from_userdata()) | ||
| self.configs = self.__add_defaults(config) | ||
| log_level = 3 if self.configs['debug_mode'] else 1 | ||
| self.__setup_vsockproxy(log_level) | ||
| self.__run_config_server(log_level) | ||
| self.__run_socks_proxy(log_level) | ||
|
|
||
|
|
||
| def _validate_auxilaries(self): | ||
| proxy = "socks5h://127.0.0.1:3305" | ||
| url = "http://127.0.0.1:27015/getConfig" | ||
| response = requests.get(url) | ||
| if response.status_code != 200: | ||
| raise Exception("Config server unreachable") | ||
| proxies = { | ||
| "http": proxy, | ||
| "https": proxy, | ||
| } | ||
| try: | ||
| response = requests.get(url, proxies=proxies) | ||
| response.raise_for_status() | ||
| except Exception as e: | ||
| raise Exception(f"Cannot conect to config server through socks5: {e}") | ||
|
|
||
| def run_compute(self): | ||
| self._setup_auxilaries() | ||
| self._validate_auxilaries() | ||
| command = [ | ||
| "nitro-cli", "run-enclave", | ||
| "--eif-path", "/opt/uid2operator/uid2operator.eif", | ||
| "--memory", self.config['enclave_memory_mb'], | ||
| "--cpu-count", self.config['enclave_cpu_count'], | ||
| "--enclave-cid", 42, | ||
| "--enclave-name", "uid2operator" | ||
| ] | ||
| if self.config['debug']: | ||
| command+=["--debug-mode", "--attach-console"] | ||
| subprocess.run(command, check=True) | ||
|
|
||
| def cleanup(self): | ||
| describe_output = subprocess.check_output(["nitro-cli", "describe-enclaves"], text=True) | ||
|
||
| enclaves = json.loads(describe_output) | ||
| enclave_id = enclaves[0].get("EnclaveID") if enclaves else None | ||
|
||
| if enclave_id: | ||
| subprocess.run(["nitro-cli", "terminate-enclave", "--enclave-id", enclave_id]) | ||
| print(f"Enclave with ID {enclave_id} has been terminated.") | ||
| else: | ||
| print("No enclave found or EnclaveID is null.") | ||
|
|
||
| def kill_process(self, process_name): | ||
| try: | ||
| result = subprocess.run( | ||
| ["pgrep", "-f", process_name], | ||
| stdout=subprocess.PIPE, | ||
| text=True, | ||
| check=False | ||
| ) | ||
| if result.stdout.strip(): | ||
| for pid in result.stdout.strip().split("\n"): | ||
| os.kill(int(pid), signal.SIGKILL) | ||
| print(f"{process_name} exited") | ||
| else: | ||
| print(f"Process {process_name} not found") | ||
| except Exception as e: | ||
| print(f"Failed to shut down {process_name}: {e}") | ||
|
|
||
| if __name__ == "__main__": | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument("-o", "--operation", required=False) | ||
| args = parser.parse_args() | ||
| ec2 = EC2() | ||
| if args.operation and args.operation == "stop": | ||
| ec2.cleanup() | ||
| [ec2.kill_process(process) for process in ["vsockpx", "sockd", "vsock-proxy", "nohup"]] | ||
|
||
| else: | ||
| ec2.run_compute() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,6 +80,7 @@ else | |
| exit 1 | ||
| fi | ||
|
|
||
| # DO WE NEED THIS? do we expect customers to change URL? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've never heard of it being done before. |
||
| # -- replace base URLs if both CORE_BASE_URL and OPTOUT_BASE_URL are provided | ||
| # -- using hardcoded domains is fine because they should not be changed frequently | ||
| if [ -n "${CORE_BASE_URL}" ] && [ "${CORE_BASE_URL}" != "null" ] && [ -n "${OPTOUT_BASE_URL}" ] && [ "${OPTOUT_BASE_URL}" != "null" ] && [ "${DEPLOYMENT_ENVIRONMENT}" != "prod" ]; then | ||
|
|
||
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below the member variable being used appears to be
self.configs.