-
Notifications
You must be signed in to change notification settings - Fork 16
Feature: [Ubuntu, no-CVM] CVM machines to not get UEFI and kek certificate updates #356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
25e0af9
627bb93
ef8d554
6bdead9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| #!/usr/bin/env bash | ||
| # Copyright 2020 Microsoft Corporation | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| HOSTNAME=$(hostname) | ||
|
|
||
| ROOT_SRC=$(findmnt -n -o SOURCE /) | ||
| ROOT_DEV=$(readlink -f "$ROOT_SRC" || echo "$ROOT_SRC") | ||
|
|
||
| FDE="false" | ||
| DETAILS="" | ||
|
|
||
| check_device() { | ||
| local dev="$1" | ||
|
|
||
| if blkid "$dev" 2>/dev/null | grep -qi 'crypto_LUKS'; then | ||
| FDE="true" | ||
| DETAILS="LUKS:$dev" | ||
| return | ||
| fi | ||
|
|
||
| local type | ||
| type=$(lsblk -dn -o TYPE "$dev" 2>/dev/null || true) | ||
|
|
||
| if [[ "$type" == "crypt" ]]; then | ||
| FDE="true" | ||
| DETAILS="CRYPT:$dev" | ||
| return | ||
| fi | ||
| } | ||
|
|
||
| walk_parents() { | ||
| local dev="$1" | ||
|
|
||
| while [[ -n "$dev" ]]; do | ||
| check_device "$dev" | ||
|
|
||
| if [[ "$FDE" == "true" ]]; then | ||
| return | ||
| fi | ||
|
|
||
| local parent | ||
| parent=$(lsblk -ndo PKNAME "$dev" 2>/dev/null | head -1 || true) | ||
|
|
||
| if [[ -z "$parent" ]]; then | ||
| break | ||
| fi | ||
|
|
||
| dev="/dev/$parent" | ||
| done | ||
| } | ||
|
|
||
| walk_parents "$ROOT_DEV" | ||
|
|
||
| if [[ "$FDE" != "true" ]]; then | ||
| while read -r name type; do | ||
| if [[ "$type" == "crypt" ]]; then | ||
| mapper="/dev/mapper/$name" | ||
|
|
||
| if mount | grep -q "^$mapper on / "; then | ||
| FDE="true" | ||
| DETAILS="DMCRYPT_ROOT:$mapper" | ||
| break | ||
| fi | ||
| fi | ||
| done < <(dmsetup ls --target crypt 2>/dev/null || true) | ||
| fi | ||
|
|
||
| if [[ "$FDE" != "true" ]]; then | ||
| if systemctl list-units 2>/dev/null | grep -qi azure; then | ||
| if ls /var/lib/waagent/*Encryption* >/dev/null 2>&1; then | ||
| FDE="true" | ||
| DETAILS="AZURE_ADE_ARTIFACTS" | ||
| fi | ||
| fi | ||
| fi | ||
|
|
||
| echo "$HOSTNAME,$ROOT_DEV,FDE=$FDE,$DETAILS" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -192,6 +192,63 @@ def get_env_var(self, var_name, raise_if_not_success=False): | |
| raise | ||
| return None | ||
|
|
||
| def detect_confidential_vm(self): | ||
| # type: () -> tuple | ||
| """Returns whether the current VM is a Confidential VM and the detection details.""" | ||
| if self.platform.os_type() == 'Windows': | ||
| return False, str() | ||
|
|
||
| is_confidential_vm, detection_details = self.detect_confidential_vm_by_imds() | ||
| if is_confidential_vm: | ||
| return is_confidential_vm, detection_details | ||
|
|
||
| is_confidential_vm, detection_details = self.detect_confidential_vm_by_fde() | ||
| if is_confidential_vm: | ||
| return is_confidential_vm, detection_details | ||
|
|
||
| return False, str() | ||
|
|
||
| def detect_confidential_vm_by_fde(self): | ||
| # type: () -> tuple | ||
| """Runs the FDE-based CVM detection script and returns whether it detected a Confidential VM.""" | ||
| command_output = str() | ||
|
|
||
| try: | ||
| detection_shim_path = self.__get_fde_detection_shim_path() | ||
| detection_script = self.file_system.read_with_retry(detection_shim_path) | ||
| if detection_script is None or str(detection_script).strip() == str(): | ||
| raise Exception("FDE_DETECTION_SHIM_EMPTY:{0}".format(str(detection_shim_path))) | ||
|
|
||
| code, out = self.run_command_output('bash "{0}"'.format(detection_shim_path), False, False) | ||
| command_output = str(out).strip() if out is not None else str() | ||
| return code == 0 and re.search(r'\bFDE\s*=\s*true\b', command_output, re.IGNORECASE) is not None, command_output | ||
| except Exception as e: | ||
| raise Exception("FDE_DETECTION_ERROR:{0}; OUTPUT:{1}".format(str(e), command_output)) | ||
|
|
||
| @staticmethod | ||
| def __get_fde_detection_shim_path(): | ||
| # type: () -> str | ||
| """Resolves the packaged FDE detection shim path.""" | ||
| current_dir = os.path.dirname(os.path.realpath(__file__)) | ||
| shim_path = os.path.join(current_dir, Constants.DETECT_CVM_SHIM_FILE_NAME) | ||
|
|
||
| if os.path.isfile(shim_path): | ||
| return shim_path | ||
|
|
||
| raise Exception("FDE_DETECTION_SHIM_NOT_FOUND:{0}".format(str(shim_path))) | ||
|
|
||
| def detect_confidential_vm_by_imds(self): | ||
| # type: () -> tuple | ||
| """Queries Azure IMDS and returns whether the VM reports ConfidentialVM security type.""" | ||
| command = 'curl -s --connect-timeout 2 --max-time 2 -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2025-04-07"' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a chance this IP address changes and could be spoofed/taken by a malicious actor?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have much insight on this one, simply used the command that was shared by partners to fetch from imds. Here's Copilot's response: ✅ Short answer No, the IP does not change 🔎 Why this is safe
169.254.169.254 is a hard-coded Azure IMDS endpoint, not assigned from your VNet or subnet. [learn.microsoft.com] ➡️ This means: It is not customer-controlled
The endpoint is non-routable and only accessible from within the VM [learn.microsoft.com] ➡️ So: No external attacker can “impersonate” this endpoint over the network
Azure automatically injects a route to 169.254.169.254 via the VM’s primary NIC (not via your VNet config) [simonpainter.com] ➡️ This prevents: DNS spoofing
If an attacker already has root/admin access inside the VM, they could: Intercept traffic locally ✅ But: At that point, the VM is already compromised 💡 Best-practice statement The IMDS endpoint (169.254.169.254) is a well-known, platform-defined link-local address that is not part of the customer-controlled network and does not change across deployments. It is non-routable and only accessible from within the VM, with traffic handled entirely within the Azure host fabric. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. Thanks! |
||
|
|
||
| code, out = self.run_command_output(command, False, False) | ||
| command_output = str(out).strip() if out is not None else str() | ||
| if code == 0 and re.search(r'"securityType"\s*:\s*"ConfidentialVM"', command_output, re.IGNORECASE) is not None: | ||
| return True, 'IMDS:ConfidentialVM' | ||
|
|
||
| return False, str() | ||
|
|
||
| def run_command_output(self, cmd, no_output=False, chk_err=True): | ||
| # type: (str, bool, bool) -> (int, any) | ||
| """ Wrapper for subprocess.check_output. Execute 'cmd'. Returns return code and STDOUT, trapping expected exceptions. Reports exceptions to Error if chk_err parameter is True """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -238,6 +238,15 @@ def main(argv): | |
| external_dependencies_source_code_path = os.path.join(source_code_path, 'external_dependencies') | ||
| add_external_dependencies(external_dependencies_destination, external_dependencies_source_code_path) | ||
|
|
||
| # Copy core shim files + enforce UNIX style line endings | ||
| print('\n========== Copying core shim files + enforcing UNIX style line endings.\n') | ||
| core_shim_files = ['DetectConfidentialVmShim.sh'] | ||
| for core_shim_file in core_shim_files: | ||
| core_shim_src = os.path.join(working_directory, 'core', 'src', 'bootstrap', core_shim_file) | ||
| core_shim_destination = os.path.join(working_directory, 'out', core_shim_file) | ||
| shutil.copyfile(core_shim_src, core_shim_destination) | ||
| replace_text_in_file(core_shim_destination, '\r\n', '\n') | ||
|
Comment on lines
+241
to
+248
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, use external_dependencies for this file (and have it automatically covered). Merge over this -- |
||
|
|
||
| except Exception as error: | ||
| print('Exception during packaging all python modules in core: ' + repr(error)) | ||
| raise | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move to external dependencies and note the original source of the file (for future updates, responsible owners, etc). Ensure an internal escalation path exists if there are issues with this in the future.