diff --git a/.github/workflows/ami-release-nix.yml b/.github/workflows/ami-release-nix.yml index 773aee018c..a6cf2de67a 100644 --- a/.github/workflows/ami-release-nix.yml +++ b/.github/workflows/ami-release-nix.yml @@ -105,6 +105,41 @@ jobs: postgres_version: ${{ matrix.postgres_version }} region: ${{ env.AWS_REGION }} + - name: Snapshot AMI manifest + if: matrix.target.arch == 'amd64' + env: + AMI_ID: ${{ steps.build-ami.outputs.stage2_ami_id }} + RUN_ID: ${{ github.run_id }} + run: | + ssh-keygen -t ed25519 -N '' -f /tmp/manifest-key -q + + VPC_ID=$(aws ec2 describe-vpcs --region "$AWS_REGION" --filters Name=is-default,Values=true --query 'Vpcs[0].VpcId' --output text) + SUBNET_ID=$(aws ec2 describe-subnets --region "$AWS_REGION" --filters Name=vpc-id,Values="$VPC_ID" --query 'Subnets[0].SubnetId' --output text) + SG_ID=$(aws ec2 create-security-group --region "$AWS_REGION" --group-name "manifest-snapshot-$RUN_ID" --description "temp sg for ami manifest snapshot" --vpc-id "$VPC_ID" --query GroupId --output text) + RUNNER_IP=$(curl -s https://checkip.amazonaws.com) + aws ec2 authorize-security-group-ingress --region "$AWS_REGION" --group-id "$SG_ID" --protocol tcp --port 22 --cidr "$RUNNER_IP/32" + + cleanup() { + [ -n "${INSTANCE_ID:-}" ] && aws ec2 terminate-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" && aws ec2 wait instance-terminated --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" + aws ec2 delete-security-group --region "$AWS_REGION" --group-id "$SG_ID" + } + trap cleanup EXIT + + INSTANCE_ID=$(aws ec2 run-instances --region "$AWS_REGION" --image-id "$AMI_ID" --instance-type t3.small --subnet-id "$SUBNET_ID" --security-group-ids "$SG_ID" --associate-public-ip-address --count 1 --query 'Instances[0].InstanceId' --output text) + aws ec2 wait instance-running --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" + IP=$(aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) + + for i in $(seq 1 30); do + aws ec2-instance-connect send-ssh-public-key --region "$AWS_REGION" --instance-id "$INSTANCE_ID" --instance-os-user ubuntu --ssh-public-key file:///tmp/manifest-key.pub + ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i /tmp/manifest-key "ubuntu@$IP" true && break + sleep 5 + done + + scp -o StrictHostKeyChecking=no -i /tmp/manifest-key testinfra/manifest-snapshot.sh "ubuntu@$IP:/tmp/manifest-snapshot.sh" + ssh -o StrictHostKeyChecking=no -i /tmp/manifest-key "ubuntu@$IP" "chmod +x /tmp/manifest-snapshot.sh && sudo /tmp/manifest-snapshot.sh" > ami-manifest.txt + + aws s3 cp ami-manifest.txt "s3://${{ secrets.ARTIFACTS_BUCKET }}/ami-manifests/${{ matrix.postgres_version }}/latest.txt" + - name: Setup post build env vars run: | POSTGRES_SUPABASE_VERSION=${{ steps.build-ami.outputs.postgres_release_version }} diff --git a/.github/workflows/testinfra-ami-build.yml b/.github/workflows/testinfra-ami-build.yml index 7288358399..948875b344 100644 --- a/.github/workflows/testinfra-ami-build.yml +++ b/.github/workflows/testinfra-ami-build.yml @@ -126,11 +126,20 @@ jobs: env: AMI_ID: ${{ steps.build-ami.outputs.stage2_ami_id }} EXECUTION_ID: ${{ steps.build-ami.outputs.execution_id }} + MANIFEST_OUTPUT: ami-manifest-${{ matrix.postgres_version }}-${{ matrix.target.arch }}.txt run: | # TODO: use uv for pkg mgmt pip3 install boto3 'boto3-stubs[essential]' ec2instanceconnectcli pytest 'pytest-testinfra[paramiko]' requests pytest -vv -s testinfra/test_ami_nix.py + - name: Upload AMI manifest + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ami-manifest-${{ matrix.postgres_version }}-${{ matrix.target.arch }} + path: ami-manifest-*.txt + overwrite: true + retention-days: 1 + - name: Cleanup resources on build cancellation if: ${{ cancelled() }} run: | diff --git a/testinfra/manifest-snapshot.sh b/testinfra/manifest-snapshot.sh new file mode 100755 index 0000000000..0f5a949f91 --- /dev/null +++ b/testinfra/manifest-snapshot.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -eu + +EXCLUDE='^/(proc|run|tmp|var/log|data)(/|$)' + +find / -xdev \( -type f -o -type l \) 2>/dev/null | grep -Ev "$EXCLUDE" | sort | while read -r f; do + if [ -L "$f" ]; then + printf '%s\tlink\t%s\n' "$f" "$(readlink "$f")" + else + printf '%s\t%s\t%s\n' "$f" "$(stat -c '%a:%U:%G' "$f" 2>/dev/null || echo '?')" "$(sha256sum "$f" 2>/dev/null | cut -d' ' -f1)" + fi +done + +echo '--- units ---' +systemctl list-unit-files --no-pager 2>/dev/null | sort +echo '--- users ---' +getent passwd | sort +echo '--- groups ---' +getent group | sort +echo '--- nft ---' +nft list ruleset 2>/dev/null || true +echo '--- sysctl ---' +sysctl -a 2>/dev/null | sort diff --git a/testinfra/test_ami_nix.py b/testinfra/test_ami_nix.py index cfd77554d6..ed0e31bf52 100644 --- a/testinfra/test_ami_nix.py +++ b/testinfra/test_ami_nix.py @@ -1407,3 +1407,14 @@ def test_apparmor_denies_access_to_sensitive_paths(host): f"to have succeeded.\nstdout: {result['stdout']}\nstderr: {result['stderr']}" ) print(f"Confirmed: access to {test_file} denied by AppArmor") + + +def test_manifest_snapshot(host): + ssh = host["ssh"] + script = os.path.join(os.path.dirname(__file__), "manifest-snapshot.sh") + upload_file_via_sftp(ssh, script, "/tmp/manifest-snapshot.sh") + run_ssh_command(ssh, "chmod +x /tmp/manifest-snapshot.sh") + result = run_ssh_command(ssh, "sudo /tmp/manifest-snapshot.sh") + assert result["succeeded"], result["stderr"] + with open(os.environ.get("MANIFEST_OUTPUT", "ami-manifest.txt"), "w") as f: + f.write(result["stdout"])