Skip to content

Commit

Permalink
Add sample Azure implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
abuabraham-ttd committed Nov 20, 2024
1 parent bdfe34b commit 01c3850
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 23 deletions.
46 changes: 26 additions & 20 deletions scripts/aws/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down
17 changes: 14 additions & 3 deletions scripts/azure-cc/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
136 changes: 136 additions & 0 deletions scripts/azure-cc/conf/azure.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions scripts/confidential_compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class OperatorConfig(TypedDict):
api_token: str
core_base_url: str
optout_base_url: str
environment: str


class ConfidentialCompute(ABC):
Expand Down

0 comments on commit 01c3850

Please sign in to comment.