diff --git a/scripts/aws/ec2.py b/scripts/aws/ec2.py index e60544dbf..11578dc9a 100755 --- a/scripts/aws/ec2.py +++ b/scripts/aws/ec2.py @@ -51,6 +51,7 @@ def _get_secret(self, secret_identifier: str) -> Dict: client = boto3.client("secretsmanager", region_name=region) try: secret = client.get_secret_value(SecretId=secret_identifier) + #TODO: validate secret string has operator key, environment and other required values return json.loads(secret["SecretString"]) except ClientError as e: raise RuntimeError(f"Unable to access Secrets Manager: {e}") @@ -65,37 +66,42 @@ def __add_defaults(configs: Dict[str, any]) -> OperatorConfig: configs.setdefault("optout_base_url", "https://optout.uidapi.com" if configs["environment"] == "prod" else "https://optout-integ.uidapi.com") return configs - @staticmethod - def __error_out_on_execute(command: list, error_message: str) -> None: - """Runs a command in the background and handles exceptions.""" - try: - subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except Exception as e: - print(f"{error_message} \n '{' '.join(command)}': {e}") - def __setup_vsockproxy(self, log_level: int) -> None: - """Sets up the vsock proxy service.""" + """ + Sets up the vsock proxy service. + TODO: Evaluate adding vsock logging based on log_level here + """ thread_count = (multiprocessing.cpu_count() + 1) // 2 command = [ "/usr/bin/vsockpx", "-c", "/etc/uid2operator/proxy.yaml", "--workers", str(thread_count), "--log-level", str(log_level), "--daemon" ] - self.__error_out_on_execute(command, "vsockpx not found. Ensure it is installed.") + subprocess.run(command) - def __run_config_server(self) -> None: - """Starts the Flask configuration server.""" + def __run_config_server(self,log_level) -> None: + """ + Starts the Flask configuration server. + TODO: Based on log level add logging to flask + """ os.makedirs("/etc/secret/secret-value", exist_ok=True) config_path = "/etc/secret/secret-value/config" with open(config_path, 'w') as config_file: json.dump(self.configs, config_file) os.chdir("/opt/uid2operator/config-server") command = ["./bin/flask", "run", "--host", "127.0.0.1", "--port", "27015"] - self.__error_out_on_execute(command, "Failed to start the Flask config server.") - - def __run_socks_proxy(self) -> None: - """Starts the SOCKS proxy service.""" + try: + subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception as e: + print(f"Failed to start the Flask config server.\n '{' '.join(command)}': {e}") + raise RuntimeError ("Failed to start required flask server") + + def __run_socks_proxy(self, log_level) -> None: + """ + Starts the SOCKS proxy service. + TODO: Based on log level add logging to sockd + """ command = ["sockd", "-d"] - self.__error_out_on_execute(command, "Failed to start socks proxy.") + subprocess.run(command) def __get_secret_name_from_userdata(self) -> str: """Extracts the secret name from EC2 user data.""" @@ -125,13 +131,13 @@ def _setup_auxiliaries(self) -> None: But can be added in future for tracibility on debug """ print(f"Error writing hostname: {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() - self.__run_socks_proxy() + self.__run_config_server(log_level) + self.__run_socks_proxy(log_level) def _validate_auxiliaries(self) -> None: """Validates auxiliary services.""" diff --git a/scripts/azure-cc/Dockerfile b/scripts/azure-cc/Dockerfile index bb0c96b70..5704f15fc 100644 --- a/scripts/azure-cc/Dockerfile +++ b/scripts/azure-cc/Dockerfile @@ -17,6 +17,17 @@ ENV IMAGE_VERSION=${IMAGE_VERSION} ENV REGION=default ENV LOKI_HOSTNAME=loki +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN python3 -m pip install --upgrade pip + +RUN pip install --no-cache-dir \ + azure-identity \ + azure-keyvault-secrets + COPY ./target/${JAR_NAME}-${JAR_VERSION}-jar-with-dependencies.jar /app/${JAR_NAME}-${JAR_VERSION}.jar COPY ./target/${JAR_NAME}-${JAR_VERSION}-sources.jar /app COPY ./target/${JAR_NAME}-${JAR_VERSION}-static.tar.gz /app/static.tar.gz @@ -25,10 +36,10 @@ COPY ./conf/*.xml /app/conf/ RUN tar xzvf /app/static.tar.gz --no-same-owner --no-same-permissions && rm -f /app/static.tar.gz -COPY ./entrypoint.sh /app/ -RUN chmod a+x /app/entrypoint.sh +COPY ./azure.py /app/ +RUN chmod a+x /app/azure.py RUN adduser -D uid2-operator && mkdir -p /opt/uid2 && chmod 777 -R /opt/uid2 && mkdir -p /app && chmod 705 -R /app && mkdir -p /app/file-uploads && chmod 777 -R /app/file-uploads USER uid2-operator -CMD ["/app/entrypoint.sh"] +CMD ["/app/azure.py"] diff --git a/scripts/azure-cc/conf/azure.py b/scripts/azure-cc/conf/azure.py new file mode 100644 index 000000000..44d5e80be --- /dev/null +++ b/scripts/azure-cc/conf/azure.py @@ -0,0 +1,136 @@ +import os +import subprocess +import time +import json +import sys +import requests +import re +from typing import Dict +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from confidential_compute import ConfidentialCompute, OperatorConfig +from azure.identity import DefaultAzureCredential +from azure.keyvault.secrets import SecretClient + +class AzureCC(ConfidentialCompute): + + def __init__(self): + super().__init__() + self.configs: OperatorConfig = {} + + + def _get_secret(self, secret_identifier): + """Fetches a secret value from Azure Key Value, reads environment variables and returns config""" + key_vault_url = "https://{}.vault.azure.net/".format(secret_identifier["key_vault"]) + credential = DefaultAzureCredential() + secret_client = SecretClient(vault_url=key_vault_url, credential=credential) + try: + config = { + "api_key" : secret_client.get_secret(secret_identifier["secret_name"]), + "environment": os.getenv("DEPLOYMENT_ENVIRONMENT"), + "core_base_url": os.getenv("CORE_BASE_URL"), + "optout_base_url": os.getenv("OPTOUT_BASE_URL") + } + return {key: value for key, value in config.items() if value is not None} + except Exception as e: + raise RuntimeError(f"Unable to access Secrets Manager: {e}") + + def _setup_auxiliaries(self, secrets): + """Sets up auxiliary configurations (placeholder for extension).""" + pass + + def __validate_sidecar(self): + """Validates the required sidecar is running""" + url = "http://169.254.169.254/ping" + delay = 1 + max_retries = 15 + while True: + try: + response = requests.get(url, timeout=5) + if response.status_code == 200: + print("Sidecar started") + break + except requests.RequestException: + print(f"Sidecar not started. Retrying in {delay} seconds...") + time.sleep(delay) + if delay > max_retries: + raise RuntimeError("Unable to start operator as sidecar failed to start") + delay += 1 + + + def _validate_auxiliaries(self, secrets): + """Validates the presence of required environment variables, and sidecar is up""" + self.__validate_sidecar() + config_env_vars = [ + "VAULT_NAME", + "OPERATOR_KEY_SECRET_NAME", + "DEPLOYMENT_ENVIRONMENT" + ] + pre_set_env_vars = [ + "JAR_NAME", + "JAR_VERSION" + ] + for variable in (config_env_vars + pre_set_env_vars): + value = os.getenv(variable) + if not value: + raise ValueError("{} is not set. Please update it".format(variable)) + if os.getenv("DEPLOYMENT_ENVIRONMENT") not in ["prod","integ"]: + raise ValueError("DEPLOYMENT_ENVIRONMENT should be prod/integ. It is currently set to {}".format(os.getenv("DEPLOYMENT_ENVIRONMENT"))) + + @staticmethod + def __add_defaults(configs: Dict[str, any]) -> OperatorConfig: + """Adds default values to configuration if missing.""" + configs.setdefault("enclave_memory_mb", -1) + configs.setdefault("enclave_cpu_count", -1) + configs.setdefault("debug_mode", False) + configs.setdefault("core_base_url", "https://core.uidapi.com" if configs["environment"] == "prod" else "https://core-integ.uidapi.com") + configs.setdefault("optout_base_url", "https://optout.uidapi.com" if configs["environment"] == "prod" else "https://optout-integ.uidapi.com") + return configs + + def __update_config_file(self, config_path): + """Updates configuration file with base URLs if in a non-production environment.""" + if not os.path.exists(config_path): + raise FileNotFoundError(f"Configuration file not found: {config_path}") + with open(config_path) as f: + config_data = json.load(f) + if all([os.getenv("CORE_BASE_URL"), os.getenv("OPTOUT_BASE_URL")]) and self.configs["environment"] != "prod": + config_data = re.sub(r"https://core-integ\.uidapi\.com", os.getenv("CORE_BASE_URL"), config_data) + config_data = re.sub(r"https://optout-integ\.uidapi\.com", os.getenv("OPTOUT_BASE_URL"), config_data) + with open(config_path, "w") as file: + file.write(config_data) + + + def run_compute(self): + """Main execution flow for confidential compute.""" + self._setup_auxiliaries(None) + self._validate_auxiliaries(None) + secret_identifier = { + "key_vault": os.getenv("OPERATOR_KEY_SECRET_NAME"), + "secret_name": os.getenv("VAULT_NAME") + } + self.configs = self.__add_defaults(self._get_secret(secret_identifier)) + self.validate_operator_key(self.configs) + self.validate_connectivity(self.configs) + os.environ["azure_vault_name"] = os.getenv("VAULT_NAME") + os.environ["azure_secret_name"] = os.getenv("OPERATOR_KEY_SECRET_NAME") + config_path="/app/conf/${}-uid2-config.json".format(os.getenv("DEPLOYMENT_ENVIRONMENT")) + self.__update_config_file(config_path=config_path) + java_command = [ + "java", + "-XX:MaxRAMPercentage=95", + "-XX:-UseCompressedOops", + "-XX:+PrintFlagsFinal", + "-Djava.security.egd=file:/dev/./urandom", + "-Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory", + "-Dlogback.configurationFile=/app/conf/logback.xml", + "-Dvertx-config-path={}".format(config_path), + "-jar", + "{}-{}.jar".format(os.getenv("JAR_NAME"), os.getenv("JAR_VERSION")) + ] + try: + subprocess.run(java_command, check=True) + except subprocess.CalledProcessError as e: + print(f"Error starting the Java application: {e}") + + +if __name__ == "__main__": + AzureCC().run_compute() \ No newline at end of file diff --git a/scripts/confidential_compute.py b/scripts/confidential_compute.py index 32e509ad3..124f2b7fc 100644 --- a/scripts/confidential_compute.py +++ b/scripts/confidential_compute.py @@ -13,6 +13,7 @@ class OperatorConfig(TypedDict): api_token: str core_base_url: str optout_base_url: str + environment: str class ConfidentialCompute(ABC):