diff --git a/.github/workflows/build-os-image.yml b/.github/workflows/build-os-image.yml index e1a3ec4b8..c5fe45172 100644 --- a/.github/workflows/build-os-image.yml +++ b/.github/workflows/build-os-image.yml @@ -64,33 +64,41 @@ jobs: # TODO: flatten outputs once possible # https://github.com/community/community/discussions/17245 outputs: + image-raw-aws-sha256: ${{ steps.collect-hashes.outputs.image-raw-aws-sha256 }} image-raw-azure-sha256: ${{ steps.collect-hashes.outputs.image-raw-azure-sha256 }} image-raw-gcp-sha256: ${{ steps.collect-hashes.outputs.image-raw-gcp-sha256 }} image-raw-qemu-sha256: ${{ steps.collect-hashes.outputs.image-raw-qemu-sha256 }} + image-efi-aws-sha256: ${{ steps.collect-hashes.outputs.image-efi-aws-sha256 }} image-efi-azure-sha256: ${{ steps.collect-hashes.outputs.image-efi-azure-sha256 }} image-efi-gcp-sha256: ${{ steps.collect-hashes.outputs.image-efi-gcp-sha256 }} image-efi-qemu-sha256: ${{ steps.collect-hashes.outputs.image-efi-qemu-sha256 }} + image-initrd-aws-sha256: ${{ steps.collect-hashes.outputs.image-initrd-aws-sha256 }} image-initrd-azure-sha256: ${{ steps.collect-hashes.outputs.image-initrd-azure-sha256 }} image-initrd-gcp-sha256: ${{ steps.collect-hashes.outputs.image-initrd-gcp-sha256 }} image-initrd-qemu-sha256: ${{ steps.collect-hashes.outputs.image-initrd-qemu-sha256 }} + image-root-raw-aws-sha256: ${{ steps.collect-hashes.outputs.image-root-raw-aws-sha256 }} image-root-raw-azure-sha256: ${{ steps.collect-hashes.outputs.image-root-raw-azure-sha256 }} image-root-raw-gcp-sha256: ${{ steps.collect-hashes.outputs.image-root-raw-gcp-sha256 }} image-root-raw-qemu-sha256: ${{ steps.collect-hashes.outputs.image-root-raw-qemu-sha256 }} + image-root-verity-aws-sha256: ${{ steps.collect-hashes.outputs.image-root-verity-aws-sha256 }} image-root-verity-azure-sha256: ${{ steps.collect-hashes.outputs.image-root-verity-azure-sha256 }} image-root-verity-gcp-sha256: ${{ steps.collect-hashes.outputs.image-root-verity-gcp-sha256 }} image-root-verity-qemu-sha256: ${{ steps.collect-hashes.outputs.image-root-verity-qemu-sha256 }} + image-vmlinuz-aws-sha256: ${{ steps.collect-hashes.outputs.image-vmlinuz-aws-sha256 }} image-vmlinuz-azure-sha256: ${{ steps.collect-hashes.outputs.image-vmlinuz-azure-sha256 }} image-vmlinuz-gcp-sha256: ${{ steps.collect-hashes.outputs.image-vmlinuz-gcp-sha256 }} image-vmlinuz-qemu-sha256: ${{ steps.collect-hashes.outputs.image-vmlinuz-qemu-sha256 }} + image-raw-changelog-aws-sha256: ${{ steps.collect-hashes.outputs.image-raw-changelog-aws-sha256 }} image-raw-changelog-azure-sha256: ${{ steps.collect-hashes.outputs.image-raw-changelog-azure-sha256 }} image-raw-changelog-gcp-sha256: ${{ steps.collect-hashes.outputs.image-raw-changelog-gcp-sha256 }} image-raw-changelog-qemu-sha256: ${{ steps.collect-hashes.outputs.image-raw-changelog-qemu-sha256 }} + image-raw-manifest-aws-sha256: ${{ steps.collect-hashes.outputs.image-raw-manifest-aws-sha256 }} image-raw-manifest-azure-sha256: ${{ steps.collect-hashes.outputs.image-raw-manifest-azure-sha256 }} image-raw-manifest-gcp-sha256: ${{ steps.collect-hashes.outputs.image-raw-manifest-gcp-sha256 }} image-raw-manifest-qemu-sha256: ${{ steps.collect-hashes.outputs.image-raw-manifest-qemu-sha256 }} strategy: matrix: - csp: [azure, gcp, qemu] + csp: [aws, azure, gcp, qemu] steps: - name: Checkout uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 @@ -183,9 +191,12 @@ jobs: name: "Upload OS image to CSP" needs: make-os-image runs-on: ubuntu-22.04 + permissions: + id-token: write + contents: read strategy: matrix: - csp: [azure, gcp] + csp: [aws, azure, gcp] upload-variant: [""] include: - csp: azure @@ -205,9 +216,19 @@ jobs: run: | echo "::group::Install tools" sudo apt-get update - sudo apt-get install -y pigz qemu-utils + sudo apt-get install -y \ + pigz \ + qemu-utils \ + python3-crc32c echo "::endgroup::" + - name: Login to AWS + uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 + if: ${{ matrix.csp == 'aws' || matrix.csp == 'azure' }} + with: + role-to-assume: arn:aws:iam::795746500882:role/GitHubConstellationImagePipeline + aws-region: eu-central-1 + - name: Login to Azure uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 if: ${{ matrix.csp == 'azure' }} @@ -229,7 +250,7 @@ jobs: id: version uses: ./.github/actions/pseudo_version - # Make sure to set valid names for GCP and Azure + # Make sure to set valid names for AWS, Azure and GCP # Azure # gallery name may include alphanumeric characters, dots and underscores. Must end and begin with an alphanumeric character # image definition may include alphanumeric characters, dots, dashes and underscores. Must end and begin with an alphanumeric character @@ -245,6 +266,13 @@ jobs: imageVersion=${{ inputs.imageVersion }} pseudover=${{ steps.version.outputs.pseudoVersion }} echo "PKI=${{ github.workspace }}/image/pki" >> $GITHUB_ENV + echo "AWS_REGION=eu-central-1" >> $GITHUB_ENV + echo "AWS_REPLICATION_REGIONS=us-east-2 ap-south-1" >> $GITHUB_ENV + echo "AWS_BUCKET=constellation-images" >> $GITHUB_ENV + echo "AWS_EFIVARS_PATH=${{ github.workspace }}/image/mkosi.output.aws/fedora~36/efivars.bin" >> $GITHUB_ENV + echo "AWS_IMAGE_PATH=${{ github.workspace }}/image/mkosi.output.aws/fedora~36/image.raw" >> $GITHUB_ENV + echo "AWS_AMI_OUTPUT=${{ github.workspace }}/image/mkosi.output.aws/fedora~36/ami.json" >> $GITHUB_ENV + echo "AWS_IMAGE_FILENAME=image-$(date +%s).raw" >> $GITHUB_ENV echo "GCP_PROJECT=constellation-images" >> $GITHUB_ENV echo "GCP_BUCKET=constellation-images" >> $GITHUB_ENV echo "GCP_REGION=europe-west3" >> $GITHUB_ENV @@ -266,6 +294,7 @@ jobs: echo "AZURE_DISK_NAME=constellation-${pseudover//./-}-${AZURE_SECURITY_TYPE,,}" >> $GITHUB_ENV if [ "${{ startsWith(github.ref, 'refs/heads/release/') && (inputs.debug == false) }}" = true ] then + echo "AWS_IMAGE_NAME=constellation-${imageVersion}" >> $GITHUB_ENV GCP_IMAGE_NAME=constellation-${imageVersion//./-} echo "GCP_IMAGE_FAMILY=constellation" >> $GITHUB_ENV AZURE_IMAGE_DEFINITION=constellation @@ -273,12 +302,14 @@ jobs: AZURE_GALLERY_NAME=Constellation elif [ "${{ ((github.ref == 'refs/heads/main') || startsWith(github.ref, 'refs/heads/release/')) && (inputs.debug == true) }}" = true ] then + echo "AWS_IMAGE_NAME=constellation-debug-${semver}-${{ steps.version.outputs.timestamp }}" >> $GITHUB_ENV GCP_IMAGE_NAME=constellation-${{ steps.version.outputs.timestamp }} echo "GCP_IMAGE_FAMILY=constellation-debug-${semver//./-}" >> $GITHUB_ENV AZURE_IMAGE_DEFINITION=${semver} echo "AZURE_IMAGE_VERSION=${timestamp:0:4}.${timestamp:4:4}.${timestamp:8}" >> $GITHUB_ENV AZURE_GALLERY_NAME=Constellation_Debug else + echo "AWS_IMAGE_NAME=constellation-${{ steps.version.outputs.branchName }}-${{ steps.version.outputs.timestamp }}" >> $GITHUB_ENV GCP_IMAGE_NAME=constellation-${{ steps.version.outputs.timestamp }} echo "GCP_IMAGE_FAMILY=constellation-${{ steps.version.outputs.branchName }}" >> $GITHUB_ENV AZURE_IMAGE_DEFINITION=${{ steps.version.outputs.branchName }} @@ -299,12 +330,25 @@ jobs: echo "GCP_IMAGE_FILENAME=${GCP_IMAGE_NAME}.tar.gz" >> $GITHUB_ENV - name: Download VMGS blob - run: aws s3 cp \ - s3://constellation-secure-boot/pki_testing/${AZURE_SECURITY_TYPE}.vmgs \ - pki_testing/${AZURE_SECURITY_TYPE}.vmgs \ - --no-progress + run: | + aws s3 cp \ + --region ${AWS_REGION} \ + s3://constellation-secure-boot/pki_testing/${AZURE_SECURITY_TYPE}.vmgs \ + pki_testing/${AZURE_SECURITY_TYPE}.vmgs \ + --no-progress working-directory: ${{ github.workspace }}/image - if: ${{ matrix.csp == 'azure' }} + if: ${{ matrix.csp == 'azure' && !endsWith(env.AZURE_SECURITY_TYPE, 'Supported') }} + + - name: Upload AWS image + shell: bash + run: | + echo "::group::Upload AWS image" + secure-boot/aws/create_uefivars.sh "${AWS_EFIVARS_PATH}" + upload/upload_aws.sh "${AWS_AMI_OUTPUT}" + echo -e "Uploaded AWS image: \`\`\`$(cat "${AWS_AMI_OUTPUT}" | jq)\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "::endgroup::" + working-directory: ${{ github.workspace }}/image + if: ${{ matrix.csp == 'aws' }} - name: Upload GCP image shell: bash @@ -334,7 +378,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - csp: [azure, gcp, qemu] + csp: [aws, azure, gcp, qemu] steps: - name: Checkout repository uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b @@ -417,6 +461,14 @@ jobs: cat > SHA256SUMS < @@ -134,6 +135,34 @@ secure-boot/azure/delete.sh --name "${AZURE_DISK_NAME}-setup-secure-boot" ## Upload to CSP +
+AWS + +- Install `aws` cli (see [here](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)) +- Login to AWS (see [here](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html)) +- Choose secure boot PKI public keys (one of `pki_dev`, `pki_test`, `pki_prod`) + - `pki_dev` can be used for local image builds + - `pki_test` is used by the CI for non-release images + - `pki_prod` is used for release images + +```sh +# set these variables +export AWS_IMAGE_NAME= # e.g. "constellation-v1.0.0" +export PKI=${PWD}/pki + +export AWS_REGION=eu-central-1 +export AWS_REPLICATION_REGIONS="us-east-2" +export AWS_BUCKET=constellation-images +export AWS_EFIVARS_PATH=${PWD}/mkosi.output.aws/fedora~36/efivars.bin +export AWS_IMAGE_PATH=${PWD}/mkosi.output.aws/fedora~36/image.raw +export AWS_IMAGE_FILENAME=image-$(date +%s).raw +export AWS_AMI_OUTPUT=${PWD}/mkosi.output.aws/fedora~36/ami.txt +secure-boot/aws/create_uefivars.sh "${AWS_EFIVARS_PATH}" +upload/upload_aws.sh "${AWS_AMI_OUTPUT}" +``` + +
+
GCP diff --git a/image/mkosi.files/mkosi.aws.conf b/image/mkosi.files/mkosi.aws.conf new file mode 100644 index 000000000..50d569d09 --- /dev/null +++ b/image/mkosi.files/mkosi.aws.conf @@ -0,0 +1,3 @@ +[Output] +KernelCommandLine=constel.csp=aws +OutputDirectory=mkosi.output.aws diff --git a/image/secure-boot/aws/create_uefivars.sh b/image/secure-boot/aws/create_uefivars.sh new file mode 100755 index 000000000..af78d6f54 --- /dev/null +++ b/image/secure-boot/aws/create_uefivars.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +TMPDIR=$(mktemp -d /tmp/uefivars-XXXXXXXXXXXXXX) +git clone https://github.com/awslabs/python-uefivars ${TMPDIR} + +"${TMPDIR}/uefivars.py" -i none -o aws -O "$1" -P ${PKI}/PK.esl -K ${PKI}/KEK.esl --db ${PKI}/db.esl + +rm -rf "${TMPDIR}" diff --git a/image/upload/upload_aws.sh b/image/upload/upload_aws.sh new file mode 100755 index 000000000..2d259365e --- /dev/null +++ b/image/upload/upload_aws.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# Copyright (c) Edgeless Systems GmbH +# +# SPDX-License-Identifier: AGPL-3.0-only + +set -euo pipefail + +if [ -z "${CONFIG_FILE-}" ] && [ -f "${CONFIG_FILE-}" ]; then + . "${CONFIG_FILE}" +fi + +PK_FILE=${PKI}/PK.cer +KEK_FILES=${PKI}/KEK.cer,${PKI}/MicCorKEKCA2011_2011-06-24.crt +DB_FILES=${PKI}/db.cer,${PKI}/MicWinProPCA2011_2011-10-19.crt,${PKI}/MicCorUEFCA2011_2011-06-27.crt +CONTAINERS_JSON=$(mktemp /tmp/containers-XXXXXXXXXXXXXX.json) +declare -A AMI_FOR_REGION +AMI_OUTPUT=$1 + +import_status() { + local import_task_id=$1 + aws ec2 describe-import-snapshot-tasks --region "${AWS_REGION}" --import-task-ids "${import_task_id}" | jq -r '.ImportSnapshotTasks[0].SnapshotTaskDetail.Status' +} + +wait_for_import() { + local import_task_id=$1 + local status + echo -n "Waiting for import to finish" + while true; do + local status=$(import_status "${import_task_id}") + case "${status}" in + completed) + echo -e "\nImport completed." + break + ;; + active) + echo -n "." + sleep 5 + ;; + *) + echo "Unexpected status: ${status}" + exit 1 + ;; + esac + done +} + +wait_for_image_available() { + local ami_id=$1 + local region=$2 + echo -n "Waiting for image ${ami_id} to be available" + while true; do + # Waiter ImageAvailable failed: Max attempts exceeded + local status=$(aws ec2 wait image-available \ + --region ${region} \ + --image-ids "${ami_id}" 2>&1 || true) + case "${status}" in + "") + echo -e "\nImage available." + break + ;; + *"Max attempts exceeded"*) + echo -n "." + ;; + *) + echo "Unexpected status: ${status}" + exit 1 + ;; + esac + done +} + +tag_ami_with_backing_snapshot() { + local ami_id=$1 + local region=$2 + wait_for_image_available "${ami_id}" "${region}" + local snapshot_id=$(aws ec2 describe-images \ + --region "${region}" \ + --image-ids "${ami_id}" \ + --output text --query "Images[0].BlockDeviceMappings[0].Ebs.SnapshotId") + aws ec2 create-tags \ + --region "${region}" \ + --resources "${ami_id}" "${snapshot_id}" \ + --tags "Key=Name,Value=${AWS_IMAGE_NAME}" +} + +create_ami_from_raw_disk() { + echo "Uploading raw disk image to S3" + aws s3 cp "${AWS_IMAGE_PATH}" "s3://${AWS_BUCKET}/${AWS_IMAGE_FILENAME}" --no-progress + printf '{ + "Description": "%s", + "Format": "raw", + "UserBucket": { + "S3Bucket": "%s", + "S3Key": "%s" + } + }' "${AWS_IMAGE_NAME}" "${AWS_BUCKET}" "${AWS_IMAGE_FILENAME}" > "${CONTAINERS_JSON}" + IMPORT_SNAPSHOT=$(aws ec2 import-snapshot --region "${AWS_REGION}" --disk-container "file://${CONTAINERS_JSON}") + echo $IMPORT_SNAPSHOT + IMPORT_TASK_ID=$(echo $IMPORT_SNAPSHOT | jq -r '.ImportTaskId') + aws ec2 describe-import-snapshot-tasks --region "${AWS_REGION}" --import-task-ids "${IMPORT_TASK_ID}" + wait_for_import "${IMPORT_TASK_ID}" + AWS_SNAPSHOT=$(aws ec2 describe-import-snapshot-tasks --region "${AWS_REGION}" --import-task-ids "${IMPORT_TASK_ID}" | jq -r '.ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId') + echo "Deleting raw disk image from S3" + aws s3 rm "s3://${AWS_BUCKET}/${AWS_IMAGE_FILENAME}" + rm "${CONTAINERS_JSON}" + REGISTER_OUT=$(aws ec2 register-image \ + --region "${AWS_REGION}" \ + --name "${AWS_IMAGE_NAME}" \ + --boot-mode uefi \ + --architecture x86_64 \ + --root-device-name /dev/xvda \ + --block-device-mappings "DeviceName=/dev/xvda,Ebs={SnapshotId=${AWS_SNAPSHOT}}" \ + --ena-support \ + --tpm-support v2.0 \ + --uefi-data $(cat ${AWS_EFIVARS_PATH})) + IMAGE_ID=$(echo $REGISTER_OUT | jq -r '.ImageId') + AMI_FOR_REGION=( ["${AWS_REGION}"]="${IMAGE_ID}") + tag_ami_with_backing_snapshot "${IMAGE_ID}" "${AWS_REGION}" + echo "Imported initial AMI as ${IMAGE_ID} in ${AWS_REGION}" +} + +replicate_ami() { + local target_region=$1 + local replicated_image_out=$(aws ec2 copy-image \ + --name "${AWS_IMAGE_NAME}" \ + --source-region "${AWS_REGION}" \ + --source-image-id "${IMAGE_ID}" \ + --region "${target_region}") + local replicated_image_id=$(echo $replicated_image_out | jq -r '.ImageId') + AMI_FOR_REGION["${target_region}"]=${replicated_image_id} + echo "Replicated AMI as ${replicated_image_id} in ${target_region}" +} + + + +create_ami_from_raw_disk +# replicate in parallel +for region in ${AWS_REPLICATION_REGIONS}; do + replicate_ami "${region}" +done +# wait for all images to be available and tag them +for region in ${AWS_REPLICATION_REGIONS}; do + tag_ami_with_backing_snapshot "${AMI_FOR_REGION[${region}]}" "${region}" +done +echo -n "{\"${AWS_REGION}\": \"${AMI_FOR_REGION[${AWS_REGION}]}\"" > "${AMI_OUTPUT}" +for region in ${AWS_REPLICATION_REGIONS}; do + echo -n ", \"${region}\": \"${AMI_FOR_REGION[${region}]}\"" >> "${AMI_OUTPUT}" +done +echo "}" >> "${AMI_OUTPUT}"