diff --git a/.github/runners/azure-cvm/README.md b/.github/runners/azure-cvm/README.md new file mode 100644 index 000000000..b87e83121 --- /dev/null +++ b/.github/runners/azure-cvm/README.md @@ -0,0 +1,22 @@ +# General +This folder contains the files to setup an Azure function and ARM template in order to deploy Azure CVMs with a webhook. + +- `cvm-template.json`: An ARM template that deploys one CVM and the required resources. It is deployed by the Azure Function +- `azure-function`: All necessary files to redeploy the function. Changes in `requirements.txt` are installed during deployment of the function. `cloud-init.txt` is put into the CVM by supplying it as a parameter to the ARM template deployment. + +# Update cvm-template +- Look for the `Template spec` resource in your Azure project (e.g. "snp-value-reporter-template"). +- Click on "Create new version". +- Select the latest version available. +- Use `current_version+1` as new version. +- Go to "Edit template" and make your changes. +- Go to "Review + Save" and save your changes. + +# Deploy azure function +Background info can be found in the [Azure docs](https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-python?tabs=azure-cli%2Cbash%2Cbrowser#deploy-the-function-project-to-azure). +To deploy your Azure CLI needs to be authenticated and [Azure Function Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Clinux%2Ccsharp%2Cportal%2Cbash#v2) needs to be installed. + +```bash +cd .github/runners/azure-cvm/azure-function +func azure functionapp publish edgeless-snp-reporter +``` diff --git a/.github/runners/azure-cvm/azure-function/.funcignore b/.github/runners/azure-cvm/azure-function/.funcignore new file mode 100644 index 000000000..414df2f01 --- /dev/null +++ b/.github/runners/azure-cvm/azure-function/.funcignore @@ -0,0 +1,4 @@ +.git* +.vscode +local.settings.json +test \ No newline at end of file diff --git a/.github/runners/azure-cvm/azure-function/.gitignore b/.github/runners/azure-cvm/azure-function/.gitignore new file mode 100644 index 000000000..7685fc4ac --- /dev/null +++ b/.github/runners/azure-cvm/azure-function/.gitignore @@ -0,0 +1,135 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json + +# Azurite artifacts +__blobstorage__ +__queuestorage__ +__azurite_db*__.json +.python_packages \ No newline at end of file diff --git a/.github/runners/azure-cvm/azure-function/cvm-creator/__init__.py b/.github/runners/azure-cvm/azure-function/cvm-creator/__init__.py new file mode 100644 index 000000000..2600e0d2b --- /dev/null +++ b/.github/runners/azure-cvm/azure-function/cvm-creator/__init__.py @@ -0,0 +1,132 @@ +import os +import logging +import hmac +import hashlib +import random +import string +import base64 + +import azure.functions as func + +from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.resource.resources.v2021_04_01.models import Deployment, DeploymentProperties +from azure.keyvault.secrets import SecretClient +from azure.identity import DefaultAzureCredential + +LABEL = "azure-cvm" +SUBSCRIPTION_ID = "0d202bbb-4fa7-4af8-8125-58c269a05435" +VAULT_URL = "https://github-token.vault.azure.net/" +TOKEN_SECRET_NAME = "gh-webhook-secret" +SSH_KEY_SECRET_NAME = "snp-reporter-pubkey" + +def main(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + allow, reason = authorize(req) + if not allow: + return func.HttpResponse(f'unauthorized: {reason}', status_code=404) + + request_json = req.get_json() + if request_json and 'action' in request_json: + if request_json['action'] == 'queued': + return job_queued(request_json['workflow_job']) + elif request_json['action'] == 'completed': + return job_completed(request_json['workflow_job']) + elif request_json['action'] == 'in_progress': + return f'nothing to do here' + else: + return func.HttpResponse(f'invalid message format', status_code=400) + +def authorize(request) -> (bool, str) : + credentials = DefaultAzureCredential() + client = SecretClient(vault_url=VAULT_URL, credential=credentials) + correct_token = client.get_secret(TOKEN_SECRET_NAME).value + + if correct_token is None: + return False, 'correct token not set' + correct_hmac = 'sha256=' + hmac.new(correct_token.encode('utf-8'), request.get_body(), hashlib.sha256).hexdigest() + request_hmac = request.headers.get('X-Hub-Signature-256') + + if request_hmac is None: + return False, 'X-Hub-Signature-256 not set' + if correct_hmac == request_hmac: + return True, '' + else: + return False, f'X-Hub-Signature-256 incorrect' + +def job_queued(workflow_job) -> str: + if not LABEL in workflow_job['labels']: + return func.HttpResponse(f'irrelevant job labels: {workflow_job["labels"]}', status_code=200) + cloud_init = generate_cloud_init() + instance_uid = ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(6)) + + credentials = DefaultAzureCredential() + client = SecretClient(vault_url=VAULT_URL, credential=credentials) + ssh_key = client.get_secret(SSH_KEY_SECRET_NAME).value + + try: + create_cvm(instance_uid, cloud_init, ssh_key) + except Exception as e: + return func.HttpResponse(f'creating instance failed: {e}', status_code=400) + return 'success' + +def job_completed(workflow_job) -> str: + if not LABEL in workflow_job['labels']: + return func.HttpResponse(f'irrelevant job labels: {workflow_job["labels"]}', status_code=200) + instance_name = workflow_job["runner_name"] + try: + delete_cvm(machine_name=instance_name) + except Exception as e: + return func.HttpResponse(f'deleting instance failed: {e}', status_code=400) + return 'success' + +def generate_cloud_init() -> str: + path = os.path.join(os.path.dirname(__file__), "cloud-init.txt") + with open(path, "r") as f: + cloud_init = f.read() + + return base64.b64encode(cloud_init.encode('utf-8')) + +def delete_cvm(machine_name): + credentials = DefaultAzureCredential() + resource_client = ResourceManagementClient( + credentials, + SUBSCRIPTION_ID, + ) + + path = f"/subscriptions/{SUBSCRIPTION_ID}/resourceGroups/snp-value-reporting/providers" + + async_vm_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Compute/virtualMachines/{machine_name}", api_version="2022-08-01") + async_vm_delete.wait() + async_osdisk_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Compute/disks/{machine_name}-osdisk", api_version="2022-07-02") + async_nic_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Network/networkInterfaces/{machine_name}-nic", api_version="2022-08-01") + async_nsg_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Network/networkSecurityGroups/{machine_name}-nsg", api_version="2022-05-01") + async_vnet_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Network/virtualNetworks/{machine_name}-vnet", api_version="2022-05-01") + async_ip_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Network/publicIPAddresses/{machine_name}-ip", api_version="2022-05-01") + + async_vnet_delete.wait() + async_nic_delete.wait() + async_ip_delete.wait() + async_nsg_delete.wait() + async_osdisk_delete.wait() + + return True + +def create_cvm(instance_uid, cloud_init, ssh_key) -> str: + credentials = DefaultAzureCredential() + resource_client = ResourceManagementClient( + credentials, + SUBSCRIPTION_ID, + ) + + template_id = "https://raw.githubusercontent.com/edgelesssys/constellation/main/.github/runners/azure-cvm/cvm-template.json" + + depl_properties = DeploymentProperties(mode="Incremental", template_link={"uri": template_id}, parameters={"instanceUid": {"value": instance_uid}, "customData": {"value": cloud_init.decode("utf-8")}, "pubKey": {"value": ssh_key}}) + depl = Deployment(properties=depl_properties) + + async_vm_start = resource_client.deployments.begin_create_or_update( + "snp-value-reporting", "snp-value-reporter-deployment", depl) + + async_vm_start.wait() + + return True diff --git a/.github/runners/azure-cvm/azure-function/cvm-creator/cloud-init.txt b/.github/runners/azure-cvm/azure-function/cvm-creator/cloud-init.txt new file mode 100644 index 000000000..bc953acc6 --- /dev/null +++ b/.github/runners/azure-cvm/azure-function/cvm-creator/cloud-init.txt @@ -0,0 +1,36 @@ +#cloud-config + +users: + - default + - name: github-actions-runner-user + groups: docker + sudo: ALL=(ALL) NOPASSWD:ALL + homedir: /home/github-actions-runner-user + +package_update: true +packages: + - git + - cryptsetup + - build-essential + - libguestfs-tools + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - jq + - pv + +runcmd: + - [/bin/bash, -c, "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg"] + - [/bin/bash, -c, "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null "] + - [apt-get, update] + - [apt-get, install, -y, docker-ce, docker-ce-cli, containerd.io, libssl-dev, pigz, azure-cli] + - [/bin/bash, -c, "sudo service docker start"] + - [mkdir, -p, /actions-runner] + - [curl, -o, "/actions-runner/actions-runner-linux-x64-2.286.1.tar.gz", -L, "https://github.com/actions/runner/releases/download/v2.286.1/actions-runner-linux-x64-2.286.1.tar.gz"] + - [/bin/bash, -c, "cd /actions-runner && tar xzf /actions-runner/actions-runner-linux-x64-2.286.1.tar.gz"] + - [chown, -R, github-actions-runner-user:github-actions-runner-user, /actions-runner] + - [sudo, -u, github-actions-runner-user, /bin/bash, -c, "cd /actions-runner && /actions-runner/config.sh --url https://github.com/edgelesssys/constellation --ephemeral --labels azure-cvm --replace --unattended --token $(curl -X POST -H \"Accept: application/vnd.github+json\" -H \"Authorization: Bearer $(curl -s -H Metadata:true -H \"Authorization: Bearer $(curl -s -H Metadata:true --noproxy \"*\" \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net\" | jq -r .access_token)\" --noproxy \"*\" \"https://github-token.vault.azure.net/secrets/github-access-token?api-version=2016-10-01\" | jq -r .value)\" https://api.github.com/repos/edgelesssys/constellation/actions/runners/registration-token | jq -r .token)"] + - [/bin/bash, -c, "cd /actions-runner && ./svc.sh install"] + - [/bin/bash, -c, "systemctl enable --now actions.runner.edgelesssys-constellation.$(hostname | cut -c -31).service"] diff --git a/.github/runners/azure-cvm/azure-function/cvm-creator/function.json b/.github/runners/azure-cvm/azure-function/cvm-creator/function.json new file mode 100644 index 000000000..b8dc650e9 --- /dev/null +++ b/.github/runners/azure-cvm/azure-function/cvm-creator/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/.github/runners/azure-cvm/azure-function/host.json b/.github/runners/azure-cvm/azure-function/host.json new file mode 100644 index 000000000..519fe11b5 --- /dev/null +++ b/.github/runners/azure-cvm/azure-function/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } +} diff --git a/.github/runners/azure-cvm/azure-function/requirements.txt b/.github/runners/azure-cvm/azure-function/requirements.txt new file mode 100644 index 000000000..f4fd59323 --- /dev/null +++ b/.github/runners/azure-cvm/azure-function/requirements.txt @@ -0,0 +1,9 @@ +# DO NOT include azure-functions-worker in this file +# The Python Worker is managed by Azure Functions platform +# Manually managing azure-functions-worker may cause unexpected issues + +azure-functions +azure-mgmt-resource==21.1.0 +azure-identity==1.10.0 +azure-mgmt-subscription==3.1.1 +azure-keyvault==4.2.0 diff --git a/.github/runners/azure-cvm/cvm-template.json b/.github/runners/azure-cvm/cvm-template.json new file mode 100644 index 000000000..6a441cf15 --- /dev/null +++ b/.github/runners/azure-cvm/cvm-template.json @@ -0,0 +1,236 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "instanceUid": { + "type": "string" + }, + "customData": { + "type": "string" + }, + "pubKey": { + "type": "string" + } + }, + "variables": { + "virtualMachineName": "[concat('snp-value-reporter-', parameters('instanceUid'))]", + "osDiskName": "[concat(variables('virtualMachineName'), '-osdisk')]", + "vnetName": "[concat(variables('virtualMachineName'), '-vnet')]", + "nicName": "[concat(variables('virtualMachineName'), '-nic')]", + "nsgName": "[concat(variables('virtualMachineName'), '-nsg')]", + "subnetName": "[concat(variables('vnetName'), '/subnets/default')]", + "nicID": "[concat('/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.Network/networkInterfaces/', variables('nicName'))]", + "osDiskId": "[concat('/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.Compute/disks/', variables('osDiskName'))]", + "nsgId": "[concat('/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.Network/networkSecurityGroups/', variables('nsgName'))]", + "subnetId": "[concat('/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.Network/virtualNetworks/', variables('subnetName'))]", + "imageId": "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/Providers/Microsoft.Compute/Locations/northeurope/Publishers/canonical/ArtifactTypes/VMImage/Offers/0001-com-ubuntu-confidential-vm-focal/Skus/20_04-lts-cvm/Versions/20.04.202208240" + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2020-11-01", + "name": "[variables('vnetName')]", + "location": "northeurope", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "172.20.0.0/16" + ] + }, + "subnets": [ + { + "name": "default", + "properties": { + "addressPrefix": "172.20.0.0/24", + "delegations": [], + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled" + } + } + ], + "virtualNetworkPeerings": [], + "enableDdosProtection": false + } + }, + { + "type": "Microsoft.Network/virtualNetworks/subnets", + "apiVersion": "2020-11-01", + "name": "[concat(variables('vnetName'), '/default')]", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]" + ], + "properties": { + "addressPrefix": "172.20.0.0/24", + "delegations": [], + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled" + } + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2020-11-01", + "name": "[variables('nsgName')]", + "location": "northeurope", + "properties": { + "securityRules": [ + { + "name": "SSH", + "properties": { + "protocol": "TCP", + "sourcePortRange": "*", + "destinationPortRange": "22", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 300, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + } + ] + } + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "apiVersion": "2020-11-01", + "name": "[concat(variables('nsgName'), '/SSH')]", + "dependsOn": [ + "[variables('nsgId')]" + ], + "properties": { + "protocol": "TCP", + "sourcePortRange": "*", + "destinationPortRange": "22", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 300, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2020-11-01", + "name": "[variables('nicName')]", + "dependsOn": [ + "[variables('subnetId')]" + ], + "location": "northeurope", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAddress": "172.20.0.4", + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[variables('subnetID')]" + }, + "primary": true, + "privateIPAddressVersion": "IPv4" + } + } + ], + "dnsSettings": { + "dnsServers": [] + }, + "enableAcceleratedNetworking": false, + "enableIPForwarding": false, + "networkSecurityGroup": { + "id": "[variables('nsgId')]" + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2022-03-01", + "name": "[variables('virtualMachineName')]", + "dependsOn": [ + "[variables('nicID')]" + ], + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.ManagedIdentity/userAssignedIdentities/TokenAccess": {} + } + }, + "location": "northeurope", + "zones": [ + "3" + ], + "properties": { + "hardwareProfile": { + "vmSize": "Standard_DC2as_v5" + }, + "storageProfile": { + "imageReference": { + "publisher": "canonical", + "offer": "0001-com-ubuntu-confidential-vm-focal", + "sku": "20_04-lts-cvm", + "version": "latest" + }, + "osDisk": { + "osType": "Linux", + "name": "[variables('osDiskName')]", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "securityProfile": { + "securityEncryptionType": "VMGuestStateOnly" + }, + "storageAccountType": "Premium_LRS" + }, + "deleteOption": "Delete" + }, + "dataDisks": [] + }, + "osProfile": { + "computerName": "[variables('virtualMachineName')]", + "adminUsername": "azureuser", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "ssh": { + "publicKeys": [ + { + "path": "/home/azureuser/.ssh/authorized_keys", + "keyData": "[parameters('pubKey')]" + } + ] + } + }, + "allowExtensionOperations": true, + "customData": "[parameters('customData')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[variables('nicID')]", + "properties": { + "deleteOption": "Delete" + } + } + ] + }, + "securityProfile": { + "uefiSettings": { + "secureBootEnabled": true, + "vTpmEnabled": true + }, + "securityType": "ConfidentialVM" + } + } + } + ] +}