diff --git a/RunCommand/Linux/Linux_IMDSValidation/Linux_IMDSValidation.sh b/RunCommand/Linux/Linux_IMDSValidation/Linux_IMDSValidation.sh new file mode 100644 index 0000000..9e59a09 --- /dev/null +++ b/RunCommand/Linux/Linux_IMDSValidation/Linux_IMDSValidation.sh @@ -0,0 +1,512 @@ +#!/bin/bash +# Disclaimer: +# The sample scripts are not supported under any Microsoft standard support +# program or service. The sample scripts are provided AS IS without warranty +# of any kind. Microsoft further disclaims all implied warranties including, +# without limitation, any implied warranties of merchantability or of fitness +# for a particular purpose. The entire risk arising out of the use or +# performance of the sample scripts and documentation remains with you. +# +# Synopsis: +# Validates Azure IMDS attestation certificate chain on Linux VMs. +# +# Description: +# This script performs the following checks: +# Phase 1 - Verifies the IMDS endpoint (169.254.169.254) is reachable +# Phase 2 - Fetches the attested document and extracts the signing certificate +# Phase 3 - Validates the certificate chain against the system trust store +# Phase 4 - Detects which OCSP intermediate the VM is using +# Phase 5 - Checks the trust store for known IMDS certificates +# Phase 6 - Tests connectivity to AIA, CRL, and OCSP endpoints +# Phase 7 - Detects the distribution and shows the correct fix commands +# +# Notes: +# Requires root/sudo privileges. +# Requires openssl and python3. +# Tested on Ubuntu 22.04, RHEL 9, SUSE 15. +# Reference: https://aka.ms/AzVmIMDSValidation +# +# Usage: +# ./Linux_IMDSValidation.sh # Diagnostic only (default) +# ./Linux_IMDSValidation.sh --autofix # Download, install, re-validate + +AUTOFIX=false +if [ "${1:-}" = "--autofix" ]; then + AUTOFIX=true +fi + +set -euo pipefail + +echo "=====================================================" +echo " Azure IMDS Attestation Certificate Chain Validator" +echo " Reference: https://aka.ms/AzVmIMDSValidation" +echo "=====================================================" + +# ---- Phase 1: IMDS Reachability ---- +echo "" +echo "[Phase 1] IMDS Endpoint Reachability" +echo "-------------------------------------" +if curl -s --connect-timeout 5 -H "Metadata:true" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" -o /dev/null; then + echo " [PASS] 169.254.169.254 is reachable" +else + echo " [FAIL] 169.254.169.254 is NOT reachable" + echo " Action: Check firewall rules and network configuration." + echo " Note: IMDS uses a link-local address handled by the hypervisor." + echo " Guest OS route changes cannot block IMDS; only firewall rules can." + exit 1 +fi + +# ---- Phase 2: Attestation Fetch ---- +echo "" +echo "[Phase 2] IMDS Attested Document" +echo "--------------------------------" +ATTESTED_JSON=$(curl -s -H "Metadata:true" "http://169.254.169.254/metadata/attested/document?api-version=2018-10-01") +if [ -z "$ATTESTED_JSON" ]; then + echo " [FAIL] Cannot retrieve attested document" + exit 1 +fi + +python3 -c " +import json, sys, base64 +doc = json.loads(sys.argv[1]) +sig = base64.b64decode(doc['signature']) +with open('/tmp/imds_sig.der', 'wb') as f: + f.write(sig) +print(' [PASS] Attested document retrieved ({} bytes signature)'.format(len(sig))) +" "$ATTESTED_JSON" + +# Extract cert from PKCS#7 envelope +openssl pkcs7 -in /tmp/imds_sig.der -inform DER -print_certs -out /tmp/imds_cert.pem 2>/dev/null + +LEAF_SUBJECT=$(openssl x509 -in /tmp/imds_cert.pem -noout -subject 2>/dev/null) +LEAF_ISSUER=$(openssl x509 -in /tmp/imds_cert.pem -noout -issuer 2>/dev/null) +LEAF_DATES=$(openssl x509 -in /tmp/imds_cert.pem -noout -dates 2>/dev/null) + +echo " Leaf: $LEAF_SUBJECT" +echo " Issuer: $LEAF_ISSUER" +echo " $LEAF_DATES" + +# ---- Phase 3: Chain Validation ---- +echo "" +echo "[Phase 3] Certificate Chain Validation" +echo "--------------------------------------" +VERIFY_RESULT=$(openssl verify /tmp/imds_cert.pem 2>&1 || true) +if echo "$VERIFY_RESULT" | grep -q ": OK"; then + echo " [PASS] Certificate chain validates successfully" + CHAIN_OK=true +else + echo " [FAIL] Certificate chain validation failed" + echo " $VERIFY_RESULT" + CHAIN_OK=false +fi + +# ---- Phase 4: OCSP Detection ---- +echo "" +echo "[Phase 4] OCSP Intermediate Detection" +echo "--------------------------------------" +OCSP_NUM=$(echo "$LEAF_ISSUER" | grep -oP 'OCSP \K[0-9]+' || echo "unknown") +echo " Your VM uses OCSP intermediate: $OCSP_NUM" +if [ "$OCSP_NUM" = "unknown" ]; then + echo " [WARN] Could not detect OCSP number from issuer" + echo " Issuer: $LEAF_ISSUER" +else + echo " Download URL: https://www.microsoft.com/pkiops/certs/Microsoft%20TLS%20G2%20RSA%20CA%20OCSP%20${OCSP_NUM}.crt" +fi + +# ---- Phase 5: Trust Store Check ---- +echo "" +echo "[Phase 5] Trust Store Inventory" +echo "-------------------------------" + +# Detect trust store location +if [ -f /etc/ssl/certs/ca-certificates.crt ]; then + TRUST_STORE="/etc/ssl/certs/ca-certificates.crt" +elif [ -f /etc/pki/tls/certs/ca-bundle.crt ]; then + TRUST_STORE="/etc/pki/tls/certs/ca-bundle.crt" +elif [ -f /var/lib/ca-certificates/ca-bundle.pem ]; then + TRUST_STORE="/var/lib/ca-certificates/ca-bundle.pem" +elif [ -f /etc/ssl/ca-bundle.pem ]; then + TRUST_STORE="/etc/ssl/ca-bundle.pem" +else + TRUST_STORE="unknown" +fi +echo " Trust store: $TRUST_STORE" + +# Check for DigiCert Global Root G2 +if [ "$TRUST_STORE" != "unknown" ]; then + if awk -v cmd='openssl x509 -noout -subject' '/BEGIN/{close(cmd)};{print | cmd}' "$TRUST_STORE" 2>/dev/null | grep -qi "DigiCert Global Root G2"; then + echo " [OK] DigiCert Global Root G2" + else + echo " [MISS] DigiCert Global Root G2" + fi + + if awk -v cmd='openssl x509 -noout -subject' '/BEGIN/{close(cmd)};{print | cmd}' "$TRUST_STORE" 2>/dev/null | grep -qi "Microsoft TLS RSA Root G2"; then + echo " [OK] Microsoft TLS RSA Root G2 (B5EE)" + else + echo " [MISS] Microsoft TLS RSA Root G2 (B5EE) - cross-signed intermediate" + echo " Download: http://caissuers.microsoft.com/pkiops/certs/Microsoft%20TLS%20RSA%20Root%20G2%20-%20xsign.crt" + fi + + if [ "$OCSP_NUM" != "unknown" ]; then + if awk -v cmd='openssl x509 -noout -subject' '/BEGIN/{close(cmd)};{print | cmd}' "$TRUST_STORE" 2>/dev/null | grep -qi "Microsoft TLS G2 RSA CA OCSP $OCSP_NUM"; then + echo " [OK] Microsoft TLS G2 RSA CA OCSP $OCSP_NUM" + else + echo " [MISS] Microsoft TLS G2 RSA CA OCSP $OCSP_NUM - OCSP responder intermediate" + echo " Download: https://www.microsoft.com/pkiops/certs/Microsoft%20TLS%20G2%20RSA%20CA%20OCSP%20${OCSP_NUM}.crt" + fi + fi +fi + +# ---- Phase 6: Connectivity Check ---- +echo "" +echo "[Phase 6] AIA / CRL / OCSP Endpoint Connectivity" +echo "-------------------------------------------------" + +declare -A TARGETS +TARGETS=( + ["AIA: cacerts.digicert.com"]="cacerts.digicert.com" + ["AIA: caissuers.microsoft.com"]="caissuers.microsoft.com" + ["AIA: www.microsoft.com"]="www.microsoft.com" + ["CRL: crl3.digicert.com"]="crl3.digicert.com" + ["CRL: crl4.digicert.com"]="crl4.digicert.com" + ["OCSP: ocsp.digicert.com"]="ocsp.digicert.com" + ["OCSP: oneocsp.microsoft.com"]="oneocsp.microsoft.com" +) + +BLOCKED=0 +for label in "${!TARGETS[@]}"; do + host="${TARGETS[$label]}" + if curl -s --connect-timeout 5 -o /dev/null "http://$host" 2>/dev/null; then + echo " [+] $label" + else + echo " [-] $label - BLOCKED" + BLOCKED=$((BLOCKED + 1)) + fi +done + +# ---- Phase 7: Clock Skew Detection ---- +echo "" +echo "[Phase 7] System Clock Validation" +echo "----------------------------------" +CLOCK_SKEW=false + +# Try chrony first, then ntpd, then manual date comparison +if command -v chronyc &>/dev/null; then + OFFSET=$(chronyc tracking 2>/dev/null | grep "System time" | sed -n 's/.*: \([0-9.]*\) seconds.*/\1/p' || true) + if [ -n "$OFFSET" ]; then + OFFSET_INT=$(echo "$OFFSET" | cut -d. -f1) + if [ "${OFFSET_INT:-0}" -gt 300 ]; then + echo " [FAIL] Clock offset: ${OFFSET}s (chrony)" + echo " Certs may appear expired or not-yet-valid." + echo " Fix: sudo chronyc makestep" + CLOCK_SKEW=true + elif [ "${OFFSET_INT:-0}" -gt 60 ]; then + echo " [WARN] Clock offset: ${OFFSET}s (chrony)" + else + echo " [PASS] Clock offset: ${OFFSET}s (within tolerance)" + fi + else + echo " [WARN] Could not parse chrony output" + fi +elif command -v ntpq &>/dev/null; then + OFFSET=$(ntpq -p 2>/dev/null | tail -1 | awk '{print $9}') + if [ -n "$OFFSET" ]; then + OFFSET_ABS=${OFFSET#-} + if [ "${OFFSET_ABS%%.*}" -gt 300000 ] 2>/dev/null; then + echo " [FAIL] Clock offset: ${OFFSET}ms (ntpd)" + echo " Fix: sudo ntpdate time.windows.com" + CLOCK_SKEW=true + else + echo " [PASS] Clock offset: ${OFFSET}ms (ntpd)" + fi + else + echo " [WARN] Could not parse ntpq output" + fi +else + echo " [WARN] No NTP client found (chrony/ntpd). Cannot check clock accuracy." +fi + +# ---- Phase 8: Certificate Expiry Check ---- +echo "" +echo "[Phase 8] Certificate Expiry Check" +echo "-----------------------------------" +EXPIRED_CERTS=0 +EXPIRING_CERTS=0 +NOW_EPOCH=$(date +%s) +WARN_DAYS=60 +WARN_SECS=$((WARN_DAYS * 86400)) + +check_cert_expiry() { + local certfile="$1" + local label="$2" + if [ -f "$certfile" ]; then + local enddate + enddate=$(openssl x509 -in "$certfile" -noout -enddate 2>/dev/null | cut -d= -f2) + if [ -n "$enddate" ]; then + local end_epoch + end_epoch=$(date -d "$enddate" +%s 2>/dev/null) + if [ -n "$end_epoch" ]; then + local remaining=$((end_epoch - NOW_EPOCH)) + local days_left=$((remaining / 86400)) + if [ "$remaining" -lt 0 ]; then + echo " [FAIL] $label - EXPIRED ($enddate)" + EXPIRED_CERTS=$((EXPIRED_CERTS + 1)) + elif [ "$remaining" -lt "$WARN_SECS" ]; then + echo " [WARN] $label - expires in $days_left days ($enddate)" + EXPIRING_CERTS=$((EXPIRING_CERTS + 1)) + else + echo " [OK] $label - valid until $enddate ($days_left days)" + fi + fi + fi + fi +} + +# Check installed Microsoft TLS certs across distro paths +for certdir in /usr/local/share/ca-certificates /etc/pki/ca-trust/source/anchors /usr/share/pki/trust/anchors; do + if [ -d "$certdir" ]; then + for cert in "$certdir"/microsoft-*.crt; do + [ -f "$cert" ] && check_cert_expiry "$cert" "$(basename "$cert")" + done + fi +done + +# Also check the leaf cert from IMDS +if [ -f /tmp/imds_cert.pem ]; then + check_cert_expiry /tmp/imds_cert.pem "IMDS leaf cert (metadata.azure.com)" +fi + +if [ "$BLOCKED" -gt 0 ] && [ "$((EXPIRED_CERTS + EXPIRING_CERTS))" -gt 0 ]; then + echo "" + echo " [ALERT] AIA is blocked AND certificates are expiring/expired." + echo " This VM cannot auto-download replacements." +fi + +# ---- Phase 9: Proxy Check ---- +echo "" +echo "[Phase 9] Proxy Configuration" +echo "------------------------------" +PROXY_DETECTED=false +if [ -n "${http_proxy:-}" ] || [ -n "${https_proxy:-}" ] || [ -n "${HTTP_PROXY:-}" ] || [ -n "${HTTPS_PROXY:-}" ]; then + echo " [INFO] Proxy detected:" + [ -n "${http_proxy:-}" ] && echo " http_proxy=$http_proxy" + [ -n "${https_proxy:-}" ] && echo " https_proxy=$https_proxy" + [ -n "${HTTP_PROXY:-}" ] && echo " HTTP_PROXY=$HTTP_PROXY" + [ -n "${HTTPS_PROXY:-}" ] && echo " HTTPS_PROXY=$HTTPS_PROXY" + echo " If AIA endpoints are blocked, add them to no_proxy." + PROXY_DETECTED=true +else + echo " [OK] No proxy environment variables set" +fi + +# ---- Phase 10: Summary & Fix Commands ---- +echo "" +echo "=====================================================" +echo "[Summary]" +echo "=====================================================" + +if [ "$CHAIN_OK" = true ] && [ "$BLOCKED" -eq 0 ] && [ "$CLOCK_SKEW" = false ] && [ "$EXPIRED_CERTS" -eq 0 ]; then + echo " ALL CHECKS PASSED" + echo " IMDS attestation certificate chain is healthy." +else + if [ "$CHAIN_OK" = false ]; then + echo "" + echo " CERTIFICATE CHAIN FAILED - Install missing certificates:" + echo "" + + # Detect distro + if [ -f /etc/os-release ]; then + . /etc/os-release + fi + + case "${ID:-unknown}" in + ubuntu|debian) + echo " Distribution: $ID (use update-ca-certificates)" + echo "" + echo " # Install cross-signed intermediate (B5EE):" + echo " sudo curl -s -o /tmp/b5ee.der \\" + echo " 'http://caissuers.microsoft.com/pkiops/certs/Microsoft%20TLS%20RSA%20Root%20G2%20-%20xsign.crt'" + echo " sudo openssl x509 -in /tmp/b5ee.der -inform DER \\" + echo " -out /usr/local/share/ca-certificates/microsoft-tls-rsa-root-g2.crt" + echo "" + echo " # Install OCSP intermediate (replace $OCSP_NUM if different):" + echo " sudo curl -s -o /tmp/ocsp.der \\" + echo " 'https://www.microsoft.com/pkiops/certs/Microsoft%20TLS%20G2%20RSA%20CA%20OCSP%20${OCSP_NUM}.crt'" + echo " sudo openssl x509 -in /tmp/ocsp.der -inform DER \\" + echo " -out /usr/local/share/ca-certificates/microsoft-tls-g2-rsa-ca-ocsp.crt" + echo "" + echo " sudo update-ca-certificates" + ;; + rhel|centos|ol|almalinux|rocky|mariner|azurelinux) + echo " Distribution: $ID (use update-ca-trust)" + echo "" + echo " # Install cross-signed intermediate (B5EE):" + echo " sudo curl -s -o /tmp/b5ee.der \\" + echo " 'http://caissuers.microsoft.com/pkiops/certs/Microsoft%20TLS%20RSA%20Root%20G2%20-%20xsign.crt'" + echo " sudo openssl x509 -in /tmp/b5ee.der -inform DER \\" + echo " -out /etc/pki/ca-trust/source/anchors/microsoft-tls-rsa-root-g2.crt" + echo "" + echo " # Install OCSP intermediate (replace $OCSP_NUM if different):" + echo " sudo curl -s -o /tmp/ocsp.der \\" + echo " 'https://www.microsoft.com/pkiops/certs/Microsoft%20TLS%20G2%20RSA%20CA%20OCSP%20${OCSP_NUM}.crt'" + echo " sudo openssl x509 -in /tmp/ocsp.der -inform DER \\" + echo " -out /etc/pki/ca-trust/source/anchors/microsoft-tls-g2-rsa-ca-ocsp.crt" + echo "" + echo " sudo update-ca-trust" + ;; + sles|opensuse*) + echo " Distribution: $ID (use update-ca-certificates)" + echo "" + echo " # Install cross-signed intermediate (B5EE):" + echo " sudo curl -s -o /tmp/b5ee.der \\" + echo " 'http://caissuers.microsoft.com/pkiops/certs/Microsoft%20TLS%20RSA%20Root%20G2%20-%20xsign.crt'" + echo " sudo openssl x509 -in /tmp/b5ee.der -inform DER \\" + echo " -out /usr/share/pki/trust/anchors/microsoft-tls-rsa-root-g2.crt" + echo "" + echo " # Install OCSP intermediate (replace $OCSP_NUM if different):" + echo " sudo curl -s -o /tmp/ocsp.der \\" + echo " 'https://www.microsoft.com/pkiops/certs/Microsoft%20TLS%20G2%20RSA%20CA%20OCSP%20${OCSP_NUM}.crt'" + echo " sudo openssl x509 -in /tmp/ocsp.der -inform DER \\" + echo " -out /usr/share/pki/trust/anchors/microsoft-tls-g2-rsa-ca-ocsp.crt" + echo "" + echo " sudo update-ca-certificates" + ;; + *) + echo " Distribution: $ID (unknown - check your distro's cert management docs)" + echo " Download certs from: https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details" + ;; + esac + fi + + if [ "$BLOCKED" -gt 0 ]; then + echo "" + echo " CONNECTIVITY ISSUES ($BLOCKED endpoint(s) blocked):" + echo " Configure firewall to allow port 80 outbound to" + echo " AIA, CRL, and OCSP endpoints listed above." + echo " Reference: https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details#certificate-downloads-and-revocation-lists" + fi +fi + +echo "" +echo "Chain: DigiCert Global Root G2 > Microsoft TLS RSA Root G2 (cross-sign) > OCSP Intermediate > Leaf" +echo "Additional Information: https://aka.ms/AzVmIMDSValidation" + +# ---- AutoFix Phase ---- +if [ "$AUTOFIX" = true ] && [ "$CHAIN_OK" = false ]; then + echo "" + echo "=============================================" + echo " AutoFix: Attempting certificate remediation" + echo "=============================================" + + # Fix clock skew first + if [ "$CLOCK_SKEW" = true ]; then + echo "" + echo " Fixing clock skew..." + if command -v chronyc &>/dev/null; then + chronyc makestep 2>/dev/null && echo " [OK] Clock resynced (chrony)" || echo " [FAIL] chronyc makestep failed" + elif command -v ntpdate &>/dev/null; then + ntpdate time.windows.com 2>/dev/null && echo " [OK] Clock resynced (ntpdate)" || echo " [FAIL] ntpdate failed" + else + echo " [WARN] No NTP client available to fix clock" + fi + fi + + # Detect distro for cert install path + if [ -f /etc/os-release ]; then + . /etc/os-release + fi + + FIXED=0 + FAILED=0 + + install_cert() { + local url="$1" + local name="$2" + local tmpfile="/tmp/imds_autofix_${name}.der" + + echo "" + echo " Downloading: $name" + echo " URL: $url" + if ! curl -s --connect-timeout 10 -o "$tmpfile" "$url"; then + echo " [FAIL] Download failed (AIA may be blocked)" + FAILED=$((FAILED + 1)) + return 1 + fi + echo " [OK] Downloaded" + + # Convert DER to PEM + local pemfile="/tmp/imds_autofix_${name}.pem" + if ! openssl x509 -in "$tmpfile" -inform DER -out "$pemfile" 2>/dev/null; then + echo " [FAIL] DER to PEM conversion failed" + FAILED=$((FAILED + 1)) + return 1 + fi + + # Install based on distro + case "${ID:-unknown}" in + ubuntu|debian) + cp "$pemfile" "/usr/local/share/ca-certificates/${name}.crt" + ;; + rhel|centos|ol|almalinux|rocky|mariner|azurelinux) + cp "$pemfile" "/etc/pki/ca-trust/source/anchors/${name}.crt" + ;; + sles|opensuse*) + cp "$pemfile" "/usr/share/pki/trust/anchors/${name}.crt" + ;; + *) + echo " [FAIL] Unknown distro: ${ID:-unknown}" + FAILED=$((FAILED + 1)) + return 1 + ;; + esac + echo " [OK] Installed to trust store" + FIXED=$((FIXED + 1)) + + rm -f "$tmpfile" "$pemfile" 2>/dev/null + return 0 + } + + # Install cross-signed intermediate (B5EE) + install_cert "http://caissuers.microsoft.com/pkiops/certs/Microsoft%20TLS%20RSA%20Root%20G2%20-%20xsign.crt" "microsoft-tls-rsa-root-g2" + + # Install OCSP intermediate + if [ "$OCSP_NUM" != "unknown" ]; then + install_cert "https://www.microsoft.com/pkiops/certs/Microsoft%20TLS%20G2%20RSA%20CA%20OCSP%20${OCSP_NUM}.crt" "microsoft-tls-g2-rsa-ca-ocsp-${OCSP_NUM}" + fi + + # Update trust store + echo "" + echo " Updating trust store..." + case "${ID:-unknown}" in + ubuntu|debian) + update-ca-certificates 2>/dev/null + ;; + rhel|centos|ol|almalinux|rocky|mariner|azurelinux) + update-ca-trust 2>/dev/null + ;; + sles|opensuse*) + update-ca-certificates 2>/dev/null + ;; + esac + echo " [OK] Trust store updated" + + # Re-validate + echo "" + echo " Re-validating certificate chain..." + VERIFY2=$(openssl verify /tmp/imds_cert.pem 2>&1) + if echo "$VERIFY2" | grep -q ": OK"; then + echo " [PASS] Certificate chain now validates successfully!" + else + echo " [FAIL] Chain still fails after remediation." + echo " $VERIFY2" + fi + + echo "" + echo " AutoFix Summary: $FIXED installed, $FAILED failed" +elif [ "$AUTOFIX" = true ] && [ "$CHAIN_OK" = true ]; then + echo "" + echo " [INFO] AutoFix: Chain already validates. No action needed." +fi + +echo "Script completed." + diff --git a/RunCommand/Linux/Linux_IMDSValidation/README.md b/RunCommand/Linux/Linux_IMDSValidation/README.md new file mode 100644 index 0000000..b47f9ef --- /dev/null +++ b/RunCommand/Linux/Linux_IMDSValidation/README.md @@ -0,0 +1,112 @@ + +# Azure VM IMDS Attestation Certificate Chain Validator (Linux) + +This bash script validates the Azure Instance Metadata Service (IMDS) attestation certificate chain on Linux VMs. It identifies exactly which certificate is missing, detects the OCSP intermediate your VM uses, checks the trust store, tests connectivity to certificate download endpoints, and provides distro-specific fix commands. + +## How It Works + +The script runs 7 phases: + +1. **IMDS Reachability** — Confirms `169.254.169.254` is reachable from the guest OS +2. **Attestation Fetch** — Retrieves the attested document from IMDS and extracts the signing certificate from the PKCS#7 envelope +3. **Chain Validation** — Validates the certificate chain against the system trust store using `openssl verify` +4. **OCSP Detection** — Automatically detects which OCSP intermediate (02-16) your VM is using +5. **Store Inventory** — Checks the trust store for DigiCert Root G2, Microsoft TLS RSA Root G2, and the detected OCSP intermediate +6. **Connectivity** — Tests TCP connectivity to AIA, CRL, and OCSP endpoints +7. **Summary** — Provides distro-specific fix commands (Ubuntu/Debian, RHEL/CentOS/Mariner, SUSE) + +## Certificate Chain + +IMDS attestation uses a 4-level certificate chain: + +``` +DigiCert Global Root G2 (Root CA) + └── Microsoft TLS RSA Root G2 (cross-signed intermediate, NOT a root) + └── Microsoft TLS G2 RSA CA OCSP xx (OCSP responder intermediate) + └── CN=metadata.azure.com (leaf — from IMDS attested endpoint) +``` + +> **Note:** Azure rotates OCSP intermediates (numbered 02 through 16). The script automatically detects which one your VM is using. + +## Supported Distributions + +| Distribution | Cert Path | Update Command | Tested | +|---|---|---|---| +| Ubuntu / Debian | `/usr/local/share/ca-certificates/` | `update-ca-certificates` | Ubuntu 22.04 ✅ | +| RHEL / CentOS / Oracle Linux / Azure Linux | `/etc/pki/ca-trust/source/anchors/` | `update-ca-trust` | RHEL 9.7 ✅ | +| SUSE / openSUSE | `/usr/share/pki/trust/anchors/` | `update-ca-certificates` | SUSE 15 SP6 ✅ | + +## Prerequisites + +- Root/sudo privileges +- `openssl` (installed by default on all Azure Linux images) +- `python3` (for PKCS#7 signature extraction) +- `curl` (for IMDS and connectivity checks) +- Must be executed within an Azure VM + +## Usage + +Run the script via Azure Run Command: + +### Azure CLI + +```bash +az vm run-command invoke \ + --resource-group \ + --name \ + --command-id RunShellScript \ + --scripts @Linux_IMDSValidation.sh +``` + +With auto-fix (downloads missing certs, installs, updates trust store, re-validates): + +```bash +az vm run-command invoke \\ + --resource-group \\ + --name \\ + --command-id RunShellScript \\ + --scripts @Linux_IMDSValidation.sh \\ + --parameters "--autofix" +``` + +### Download and run locally + +```bash +# Diagnostic only (default) +curl -sL https://raw.githubusercontent.com/Azure/azure-support-scripts/master/RunCommand/Linux/Linux_IMDSValidation/Linux_IMDSValidation.sh | sudo bash + +# With auto-fix +curl -sL https://raw.githubusercontent.com/Azure/azure-support-scripts/master/RunCommand/Linux/Linux_IMDSValidation/Linux_IMDSValidation.sh | sudo bash -s -- --autofix +``` + +### Run from within the VM + +```bash +chmod +x Linux_IMDSValidation.sh +sudo ./Linux_IMDSValidation.sh +``` + +## Important: DER to PEM Conversion + +Certificate downloads from Microsoft are in **DER format**. Linux trust stores require **PEM format**. The script's fix commands include the conversion step (`openssl x509 -inform DER`). If you install certificates manually, always convert first: + +```bash +openssl x509 -in certificate.der -inform DER -out certificate.crt +``` + +## References + +- [Azure Instance Metadata Service (IMDS)](https://learn.microsoft.com/azure/virtual-machines/instance-metadata-service) +- [IMDS Certificate Chain Issues (Linux)](https://learn.microsoft.com/troubleshoot/azure/virtual-machines/linux/linux-vm-imds-certissues) +- [IMDS Certificate Chain Issues (Windows)](https://learn.microsoft.com/troubleshoot/azure/virtual-machines/windows/windows-vm-imds-certissues) +- [Azure Certificate Authority Details](https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details?tabs=certificate-authority-chains) + +## Liability + +As described in the [MIT license](../../../LICENSE.txt), these scripts are provided as-is with no warranty or liability associated with their use. + +## Provide Feedback + +We value your input. If you encounter problems with the scripts or ideas on how they can be improved please file an issue in the [Issues](https://github.com/Azure/azure-support-scripts/issues) section of the project. + +## Known Issues diff --git a/RunCommand/Windows/Windows_IMDSValidation/README.md b/RunCommand/Windows/Windows_IMDSValidation/README.md index 18e649b..a0613c5 100644 --- a/RunCommand/Windows/Windows_IMDSValidation/README.md +++ b/RunCommand/Windows/Windows_IMDSValidation/README.md @@ -1,19 +1,46 @@ -# Azure VM Attested Metadata Verification Script +# Azure VM IMDS Attestation Certificate Chain Validator -This PowerShell script is used to verify the attestation signature provided by the Azure Instance Metadata Service (IMDS). It helps ensure that the certificate used in the attestation is valid and trusted by attempting to build a certificate chain. This can be useful in verifying the integrity and authenticity of an Azure VM's identity. +This PowerShell script validates the Azure Instance Metadata Service (IMDS) attestation certificate chain on Azure VMs. It identifies exactly which certificate is missing or misconfigured, checks certificate stores, and tests connectivity to certificate download endpoints. + +## How It Works + +The script runs 6 phases: + +1. **IMDS Reachability** — Confirms `169.254.169.254:80` is reachable from the guest OS +2. **Attestation Fetch** — Retrieves the attested document from IMDS and extracts the signing certificate +3. **Chain Validation** — Builds the X509 certificate chain and walks **each element** to identify the exact failure point (not just the leaf cert's issuer) +4. **Store Inventory** — Checks all IMDS-relevant certificates are in the correct stores, flags misplaced or disallowed certs +5. **Connectivity** — Tests TCP port 80 to AIA, CRL, and OCSP endpoints required for certificate validation +6. **Summary** — Provides actionable fix steps with specific download URLs for missing certificates + +## Certificate Chain (as of Jan 2026) + +IMDS attestation uses a 4-level certificate chain: + +``` +DigiCert Global Root G2 (DF3C24F9...) + └── Microsoft TLS RSA Root G2 (B5EE89E7...) ← cross-signed intermediate, NOT a root + └── Microsoft TLS G2 RSA CA OCSP 04 (DA6D0400...) ← OCSP responder intermediate + └── CN=metadata.azure.com (leaf — from IMDS attested endpoint) +``` + +> **Note:** "Microsoft TLS RSA Root G2" is a cross-signed intermediate issued by DigiCert Global Root G2, despite its name suggesting it is a root CA. The `-xsign` suffix in its download URL confirms this. ## Features -- Fetches attested metadata from the Azure Instance Metadata Service. -- Extracts and decodes the signature. -- Attempts to build a certificate chain for verification. -- Warns if any certificates in the chain are missing and provides a link to Microsoft’s documentation. +- **Per-element chain walk** — Identifies exactly which certificate in the chain is causing the failure +- **Known cert lookup** — Maps each cert to its expected store, type, and download URL +- **Store inventory** — Checks correct store placement and warns about certs in wrong stores or the Disallowed store +- **AIA/CRL/OCSP connectivity** — Tests all endpoints needed for certificate download and validation +- **Actionable output** — Specific download URLs and fix steps, not just generic "go to this page" +- **AutoFix mode** — Optional `-AutoFix` switch downloads and installs missing certificates, re-validates, and clears the activation watermark ## Prerequisites -- PowerShell 5.1 or later (earlier versions may not support `-NoProxy`). -- Must be executed within an Azure VM (as it accesses the instance metadata endpoint). +- PowerShell 5.1 or later +- Administrator privileges +- Must be executed within an Azure VM (accesses the instance metadata endpoint) ## Usage @@ -21,25 +48,44 @@ Run the script in PowerShell **within an Azure VM**: ```powershell Set-ExecutionPolicy Bypass -Force -.\ Windows_IMDSValidation.ps1 +.\Windows_IMDSValidation.ps1 ``` -## Troubleshooting +With auto-fix (downloads missing certs, installs, re-validates, runs fclip.exe): -If you see the message: - -``` -Certificate not found: 'CN=Microsoft Azure XXXX, ...' -Please refer to the following link to download missing certificates: -https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details?tabs=certificate-authority-chains +```powershell +.\Windows_IMDSValidation.ps1 -AutoFix ``` -Visit the provided Microsoft documentation to download and install the necessary root/intermediate certificates. +Or via Azure Run Command: +- Azure Portal → VM → Operations → Run Command → Select `Windows_IMDSValidation` + +## Interpreting Results + +| Phase 4 Output | Meaning | Action | +|---|---|---| +| `[OK]` | Certificate found in correct store | None needed | +| `[MISS]` | Certificate not found | Download and install from the URL shown, or use `-AutoFix` | +| `[WARN] Found in WRONG store` | Cert exists but in incorrect store | Move to the correct store | +| `[WARN] DISALLOWED store` | Cert is explicitly blocked | Remove from Disallowed store | + +### Common Scenario: "Certificate not found" but cert was installed + +If you installed a certificate but the script still reports a chain failure, the **next certificate up the chain** may be missing. For example, installing the OCSP intermediate (DA6D) without the cross-signed intermediate above it (B5EE) will still fail. + +## Important: Auto-Download Behavior + +Windows `X509Chain.Build()` will automatically attempt to download missing intermediate certificates via AIA (Authority Information Access) extensions. This means: +- A **PASS** result doesn't guarantee certificates are permanently installed — they may have been fetched at runtime +- If AIA endpoints are blocked, the chain build will fail even if the cert was previously auto-downloaded ## References -- [Azure Instance Metadata Service](https://learn.microsoft.com/azure/virtual-machines/windows/instance-metadata-service) -- [Azure Certificate Authority Details](https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details) +- [Azure Instance Metadata Service (IMDS)](https://learn.microsoft.com/azure/virtual-machines/instance-metadata-service) +- [Azure Certificate Authority Details](https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details?tabs=certificate-authority-chains) +- [IMDS Cert Issues Troubleshooting](https://learn.microsoft.com/troubleshoot/azure/virtual-machines/windows/windows-vm-imds-certissues) +- [IMDS Connection Issues Troubleshooting](https://learn.microsoft.com/troubleshoot/azure/virtual-machines/windows/windows-vm-imds-connection) +- [IMDS Verification Tool](https://aka.ms/AzVmIMDSValidation) ## Liability As described in the [MIT license](..\..\..\LICENSE.txt), these scripts are provided as-is with no warranty or liability associated with their use. diff --git a/RunCommand/Windows/Windows_IMDSValidation/Windows_IMDSValidation.ps1 b/RunCommand/Windows/Windows_IMDSValidation/Windows_IMDSValidation.ps1 index 759829a..b7dfe84 100644 --- a/RunCommand/Windows/Windows_IMDSValidation/Windows_IMDSValidation.ps1 +++ b/RunCommand/Windows/Windows_IMDSValidation/Windows_IMDSValidation.ps1 @@ -18,123 +18,644 @@ Disclaimer: .DESCRIPTION This script performs the following checks: - - Verifies the attestation signature provided by IMDS - - Validates that the certificate used in attestation is trusted by building a certificate chain - - Tests TCP connectivity to AIA, CRL, and OCSP endpoints required for certificate validation - - Provides troubleshooting guidance for missing certificates or connectivity issues + Phase 1 - Verifies the IMDS endpoint (169.254.169.254) is reachable + Phase 2 - Fetches the attested document and extracts the signing certificate + Phase 3 - Builds the certificate chain and identifies the EXACT certificate + that is missing or invalid (not just the leaf cert issuer) + Phase 4 - Inventories the local certificate stores for all IMDS-relevant certs + Phase 5 - Tests TCP connectivity to AIA, CRL, and OCSP endpoints + Phase 6 - Provides an actionable summary with specific download URLs .NOTES Requires administrator privileges. - Tested on Windows Server 2016+. + Tested on Windows Server 2016, 2019, 2022, 2025. + Run via Azure Run Command or locally in an elevated PowerShell session. .EXAMPLE Run as administrator: PS> .\Windows_IMDSValidation.ps1 + +.EXAMPLE + Run with auto-fix (downloads and installs missing certificates, then re-validates): + PS> .\Windows_IMDSValidation.ps1 -AutoFix + +.PARAMETER AutoFix + When specified, the script attempts to download and install missing intermediate + certificates, then re-validates the chain. Default is diagnostic-only (no changes). #> -# Display description on screen -Write-Host "---------------------------------------------------------------------------------------------------------------------" -ForegroundColor Cyan -Write-Host "This script is used to verify the attestation signature provided by the Azure Instance Metadata Service (IMDS)." -ForegroundColor Cyan -Write-Host "It helps ensure that the certificate used in the attestation is valid and trusted by attempting to build a certificate chain." -ForegroundColor Cyan -Write-Host "This can be useful in verifying the integrity and authenticity of an Azure VM's identity." -ForegroundColor Cyan -Write-Host "Reference: https://aka.ms/AzVmIMDSValidation" -ForegroundColor Cyan -Write-Host "---------------------------------------------------------------------------------------------------------------------`n" -ForegroundColor Cyan +param( + [switch]$AutoFix = $false +) + +# ---- Display banner ---------------------------------------------------------- +Write-Host "=====================================================" -ForegroundColor Cyan +Write-Host " Azure IMDS Attestation Certificate Chain Validator" -ForegroundColor Cyan +Write-Host " Reference: https://aka.ms/AzVmIMDSValidation" -ForegroundColor Cyan +Write-Host "=====================================================" -ForegroundColor Cyan # ---- Safety checks ----------------------------------------------------------- function Assert-Admin { $isAdmin = ([Security.Principal.WindowsPrincipal] ` [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - if (-not $isAdmin) { - Write-Host "Please run this script as Administrator." -ForegroundColor Red + Write-Host "[FAIL] Please run this script as Administrator." -ForegroundColor Red exit 1 } } Assert-Admin -# ---- Main Logic -------------------------------------------------------------- +# ---- Known IMDS Attestation Certificate Chain -------------------------------- +# As of Jan 2026: IMDS uses OCSP responder certs chained through a cross-signed +# intermediate to the DigiCert Global Root G2. +# Chain: Leaf -> OCSP Intermediate -> Cross-signed Intermediate -> Root +$KnownCerts = @( + [PSCustomObject]@{ + CN = "DigiCert Global Root G2" + Thumbprint = "DF3C24F9BFD666761B268073FE06D1CC8D4F82A4" + Type = "Root CA" + Store = "Root" + Location = "LocalMachine" + DownloadUrl = "https://cacerts.digicert.com/DigiCertGlobalRootG2.crt" + }, + [PSCustomObject]@{ + CN = "Microsoft TLS RSA Root G2" + Thumbprint = "B5EE89E77326AB2BF1775BD99C19A28947FF8184" + Type = "Cross-signed Intermediate (NOT a root despite its name)" + Store = "CA" + Location = "LocalMachine" + DownloadUrl = "http://caissuers.microsoft.com/pkiops/certs/Microsoft%20TLS%20RSA%20Root%20G2%20-%20xsign.crt" + }, + [PSCustomObject]@{ + CN = "Microsoft TLS G2 RSA CA OCSP 04" + Thumbprint = "DA6D0400641B45AECC595D24E5037AA6BC09C358" + Type = "OCSP Responder Intermediate" + Store = "CA" + Location = "LocalMachine" + DownloadUrl = "https://www.microsoft.com/pkiops/certs/Microsoft%20TLS%20G2%20RSA%20CA%20OCSP%2004.crt" + } +) +# ---- Phase 1: IMDS Reachability --------------------------------------------- +Write-Host "`n[Phase 1] IMDS Endpoint Reachability" -ForegroundColor Cyan +Write-Host "-------------------------------------" -ForegroundColor Cyan try { - # Get the attested document from IMDS - $attestedDoc = Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Uri http://169.254.169.254/metadata/attested/document?api-version=2018-10-01 - - # Decode the signature + $tcp = Test-NetConnection -ComputerName 169.254.169.254 -Port 80 -WarningAction SilentlyContinue + if ($tcp.TcpTestSucceeded) { + Write-Host " [PASS] 169.254.169.254:80 is reachable" -ForegroundColor Green + } else { + Write-Host " [FAIL] 169.254.169.254:80 is NOT reachable" -ForegroundColor Red + Write-Host " Action: Check routing table, firewall, and proxy settings." -ForegroundColor Yellow + Write-Host " Reference: https://learn.microsoft.com/troubleshoot/azure/virtual-machines/windows/windows-vm-imds-connection" -ForegroundColor Yellow + Write-Host "`nScript cannot continue without IMDS connectivity." -ForegroundColor Red + exit 1 + } +} catch { + Write-Host " [FAIL] Network test error: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} + +# ---- Phase 2: Attestation Fetch --------------------------------------------- +Write-Host "`n[Phase 2] IMDS Attested Document" -ForegroundColor Cyan +Write-Host "--------------------------------" -ForegroundColor Cyan +try { + $attestedDoc = Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET ` + -Uri http://169.254.169.254/metadata/attested/document?api-version=2018-10-01 $signature = [System.Convert]::FromBase64String($attestedDoc.signature) - - # Create certificate object from signature $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]($signature) - - # Build the chain from the default store - $chain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain - if (-not $chain.Build($cert)) { - # Certificate not found in the default store - Write-Host "Certificate not found in default store: '$($cert.Issuer)'" -ForegroundColor Red - - # Check alternate store (LocalMachine\Root) - $altStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "LocalMachine") - $altStore.Open("ReadOnly") - $foundInAltStore = $altStore.Certificates | Where-Object { $_.Thumbprint -eq $cert.Thumbprint } - $altStore.Close() - - if ($foundInAltStore) { - Write-Host "Warning: Certificate found in LocalMachine\Root. It may be in the incorrect store." -ForegroundColor Yellow + + Write-Host " [PASS] Attested document retrieved successfully" -ForegroundColor Green + Write-Host " Leaf Subject : $($cert.Subject)" + Write-Host " Leaf Issuer : $($cert.Issuer)" + Write-Host " Leaf Thumbprint: $($cert.Thumbprint)" + Write-Host " Valid : $($cert.NotBefore.ToString('yyyy-MM-dd')) to $($cert.NotAfter.ToString('yyyy-MM-dd'))" +} catch { + Write-Host " [FAIL] Cannot retrieve attested document: $($_.Exception.Message)" -ForegroundColor Red + Write-Host " Action: Verify IMDS endpoint connectivity and VM configuration." -ForegroundColor Yellow + Write-Host " Reference: https://aka.ms/AzVmIMDSValidation" -ForegroundColor Yellow + exit 1 +} + +# ---- Phase 3: Certificate Chain Validation ----------------------------------- +Write-Host "`n[Phase 3] Certificate Chain Validation" -ForegroundColor Cyan +Write-Host "--------------------------------------" -ForegroundColor Cyan + +$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain +$chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online +$chainBuilt = $chain.Build($cert) + +$chainErrors = @() + +Write-Host "`n Chain Elements ($($chain.ChainElements.Count) certificates found):" +for ($i = 0; $i -lt $chain.ChainElements.Count; $i++) { + $element = $chain.ChainElements[$i] + $elCert = $element.Certificate + $elStatus = $element.ChainElementStatus + + if ($elStatus.Count -eq 0) { + Write-Host " [$i] [OK] $($elCert.Subject)" -ForegroundColor Green + Write-Host " Thumbprint: $($elCert.Thumbprint)" + } else { + Write-Host " [$i] [FAIL] $($elCert.Subject)" -ForegroundColor Red + Write-Host " Thumbprint: $($elCert.Thumbprint)" + Write-Host " Issuer : $($elCert.Issuer)" + foreach ($s in $elStatus) { + $statusInfo = "$($s.StatusInformation)".Trim() + Write-Host " Status : $($s.Status) - $statusInfo" -ForegroundColor Yellow + } + + # Identify which cert is ACTUALLY missing by checking the issuer + $issuerCN = ($elCert.Issuer -replace 'CN=','').Split(',')[0].Trim() + $knownIssuer = $KnownCerts | Where-Object { $_.CN -eq $issuerCN } + if ($knownIssuer) { + Write-Host " >> The issuer certificate may be missing:" -ForegroundColor Red + Write-Host " Name : $($knownIssuer.CN)" -ForegroundColor Yellow + Write-Host " Type : $($knownIssuer.Type)" -ForegroundColor Yellow + Write-Host " Download: $($knownIssuer.DownloadUrl)" -ForegroundColor Yellow + Write-Host " Store : $($knownIssuer.Location)\$($knownIssuer.Store)" -ForegroundColor Yellow + } + + $chainErrors += [PSCustomObject]@{ + Index = $i + Subject = $elCert.Subject + Issuer = $elCert.Issuer + Status = ($elStatus | ForEach-Object { $_.Status }) -join ', ' + } + } +} + +if ($chain.ChainStatus.Count -gt 0) { + Write-Host "`n Overall chain status:" -ForegroundColor Yellow + foreach ($s in $chain.ChainStatus) { + $statusInfo = "$($s.StatusInformation)".Trim() + Write-Host " $($s.Status): $statusInfo" -ForegroundColor Yellow + } +} + +if ($chainBuilt -and $chainErrors.Count -eq 0) { + Write-Host "`n [PASS] Certificate chain validated successfully." -ForegroundColor Green +} else { + Write-Host "`n [FAIL] Certificate chain validation failed." -ForegroundColor Red +} + +# ---- Phase 4: Certificate Store Inventory ------------------------------------ +Write-Host "`n[Phase 4] Certificate Store Inventory" -ForegroundColor Cyan +Write-Host "-------------------------------------" -ForegroundColor Cyan + +# Build the check list from ACTUAL chain certs (dynamic) merged with known certs. +# IMDS rotates OCSP intermediates (02, 04, 06, 08, 10, 12, 14, 16), so we check +# whatever the VM is actually using, not just hardcoded thumbprints. +$certsToCheck = @() + +# Add certs discovered in the actual chain (skip the leaf at index 0) +if ($chain.ChainElements.Count -gt 1) { + for ($ci = 1; $ci -lt $chain.ChainElements.Count; $ci++) { + $ec = $chain.ChainElements[$ci].Certificate + $cn = ($ec.Subject -replace 'CN=','').Split(',')[0].Trim() + + # Determine expected store + $expectedStore = "CA" + if ($ec.Subject -eq $ec.Issuer -or $cn -match "Root G[23]?$" -and $cn -notmatch "TLS RSA Root") { + $expectedStore = "Root" + } + $knownMatch = $KnownCerts | Where-Object { $_.Thumbprint -eq $ec.Thumbprint } + if ($knownMatch) { + $expectedStore = $knownMatch.Store + } + + $certsToCheck += [PSCustomObject]@{ + CN = $cn + Thumbprint = $ec.Thumbprint + Type = if ($knownMatch) { $knownMatch.Type } else { "Chain intermediate (detected)" } + Store = $expectedStore + Location = "LocalMachine" + DownloadUrl = if ($knownMatch) { $knownMatch.DownloadUrl } else { "See https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details" } + Source = "chain" + } + } +} + +# Add any known certs not already in the list (covers certs that weren't in this chain) +foreach ($known in $KnownCerts) { + if (-not ($certsToCheck | Where-Object { $_.Thumbprint -eq $known.Thumbprint })) { + $certsToCheck += [PSCustomObject]@{ + CN = $known.CN + Thumbprint = $known.Thumbprint + Type = $known.Type + Store = $known.Store + Location = $known.Location + DownloadUrl = $known.DownloadUrl + Source = "known" + } + } +} + +Write-Host " Checking $($certsToCheck.Count) certificates (from chain + known list):`n" + +$missingCerts = @() + +foreach ($chk in $certsToCheck) { + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($chk.Store, $chk.Location) + $store.Open("ReadOnly") + $found = $store.Certificates | Where-Object { $_.Thumbprint -eq $chk.Thumbprint } + $store.Close() + + $wrongStores = @() + foreach ($checkStore in @("Root","CA","My","AuthRoot")) { + if ($checkStore -eq $chk.Store) { continue } + $s2 = New-Object System.Security.Cryptography.X509Certificates.X509Store($checkStore, $chk.Location) + $s2.Open("ReadOnly") + $inWrong = $s2.Certificates | Where-Object { $_.Thumbprint -eq $chk.Thumbprint } + $s2.Close() + if ($inWrong) { $wrongStores += "$($chk.Location)\$checkStore" } + } + + $disStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("Disallowed", $chk.Location) + $disStore.Open("ReadOnly") + $inDisallowed = $disStore.Certificates | Where-Object { $_.Thumbprint -eq $chk.Thumbprint } + $disStore.Close() + + $label = if ($chk.Source -eq "chain") { "(active chain)" } else { "(known)" } + + if ($found) { + Write-Host " [OK] $($chk.CN) $label" -ForegroundColor Green + Write-Host " Store: $($chk.Location)\$($chk.Store) (correct)" + Write-Host " Type : $($chk.Type)" + if ($wrongStores.Count -gt 0) { + Write-Host " [WARN] Also found in: $($wrongStores -join ', ')" -ForegroundColor Yellow + } + if ($inDisallowed) { + Write-Host " [WARN] Certificate is in the DISALLOWED store!" -ForegroundColor Red + } + } else { + $autoNote = "" + if ($chainBuilt -and $chk.Source -eq "chain") { + $autoNote = " (was auto-downloaded via AIA during chain build)" + } + Write-Host " [MISS] $($chk.CN) $label$autoNote" -ForegroundColor $(if ($chk.Source -eq "chain") { "Yellow" } else { "Red" }) + Write-Host " Expected store: $($chk.Location)\$($chk.Store)" + Write-Host " Type : $($chk.Type)" + Write-Host " Download : $($chk.DownloadUrl)" -ForegroundColor Yellow + if ($wrongStores.Count -gt 0) { + Write-Host " [WARN] Found in WRONG store: $($wrongStores -join ', ')" -ForegroundColor Yellow + } + if ($inDisallowed) { + Write-Host " [WARN] Certificate is in the DISALLOWED store!" -ForegroundColor Red + } + $missingCerts += $chk + } +} + +# Note about auto-download if chain passed but certs missing +if ($chainBuilt -and $missingCerts.Count -gt 0) { + $autoDownloaded = $missingCerts | Where-Object { $_.Source -eq "chain" } + if ($autoDownloaded.Count -gt 0) { + Write-Host "`n [NOTE] Chain validation PASSED but $($autoDownloaded.Count) cert(s) are not permanently" -ForegroundColor Yellow + Write-Host " installed. They were auto-downloaded via AIA at runtime." -ForegroundColor Yellow + Write-Host " Install them permanently to avoid failures if AIA is blocked." -ForegroundColor Yellow + } +} + +# ---- Phase 5: Connectivity Check -------------------------------------------- +Write-Host "`n[Phase 5] AIA / CRL / OCSP Endpoint Connectivity" -ForegroundColor Cyan +Write-Host "-------------------------------------------------" -ForegroundColor Cyan + +$tcpTargets = [ordered]@{ + "AIA (certificate download)" = @( + "cacerts.digicert.com", + "cacerts.digicert.cn", + "cacerts.geotrust.com", + "caissuers.microsoft.com", + "www.microsoft.com" + ) + "CRL (revocation lists)" = @( + "crl3.digicert.com", + "crl4.digicert.com", + "crl.digicert.cn", + "www.microsoft.com" + ) + "OCSP (online validation)" = @( + "ocsp.digicert.com", + "ocsp.digicert.cn", + "oneocsp.microsoft.com" + ) +} + +$unreachableCount = 0 +foreach ($category in $tcpTargets.Keys) { + Write-Host "`n $category" -ForegroundColor Magenta + foreach ($targetHost in $tcpTargets[$category]) { + try { + $result = Test-NetConnection -ComputerName $targetHost -Port 80 -WarningAction SilentlyContinue + if ($result.TcpTestSucceeded) { + Write-Host " [+] $targetHost" -ForegroundColor Green + } else { + $unreachableCount++ + Write-Host " [-] $targetHost - BLOCKED" -ForegroundColor Red + } + } catch { + $unreachableCount++ + Write-Host " [!] $targetHost - ERROR: $($_.Exception.Message)" -ForegroundColor Yellow + } + } +} + +# ---- Phase 6: Clock Skew Detection ------------------------------------------ +Write-Host "`n[Phase 6] System Clock Validation" -ForegroundColor Cyan +Write-Host "---------------------------------" -ForegroundColor Cyan +$clockSkewDetected = $false +try { + $w32tmOutput = w32tm /stripchart /computer:time.windows.com /dataonly /samples:1 2>&1 + $offsetLine = $w32tmOutput | Where-Object { $_ -match '[+-]\d+\.\d+s' } | Select-Object -Last 1 + if ($offsetLine -match '([+-]?\d+\.\d+)s') { + $offsetSeconds = [math]::Abs([double]$Matches[1]) + if ($offsetSeconds -gt 300) { + Write-Host " [FAIL] Clock offset: $([math]::Round($offsetSeconds))s from time.windows.com" -ForegroundColor Red + Write-Host " Certs may appear expired or not-yet-valid due to clock skew." -ForegroundColor Yellow + Write-Host " Fix: w32tm /resync /force" -ForegroundColor Yellow + $clockSkewDetected = $true + } elseif ($offsetSeconds -gt 60) { + Write-Host " [WARN] Clock offset: $([math]::Round($offsetSeconds))s from time.windows.com" -ForegroundColor Yellow } else { - Write-Host "Please refer to the following link to download missing certificates:" -ForegroundColor Yellow - Write-Host "https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details?tabs=certificate-authority-chains" -ForegroundColor Yellow - Write-Host "https://learn.microsoft.com/azure/virtual-machines/instance-metadata-service" -ForegroundColor Yellow + Write-Host " [PASS] Clock offset: $([math]::Round($offsetSeconds))s (within tolerance)" -ForegroundColor Green } } else { - Write-Host "No missing certificate has been found." -ForegroundColor Green - } - - # --- TCP Connectivity Check Section --- - Write-Host "`nPerforming TCP Port 80 Connectivity Check..." -ForegroundColor Cyan - - $tcpTargets = @{ - "AIA" = @( - "cacerts.digicert.com", - "cacerts.digicert.cn", - "cacerts.geotrust.com", - "caissuers.microsoft.com", - "www.microsoft.com" - ) - "CRL" = @( - "crl3.digicert.com", - "crl4.digicert.com", - "crl.digicert.cn", - "www.microsoft.com" - ) - "OCSP" = @( - "ocsp.digicert.com", - "ocsp.digicert.cn", - "oneocsp.microsoft.com" - ) - } - - foreach ($category in $tcpTargets.Keys) { - Write-Host "`nChecking $category endpoints..." -ForegroundColor Magenta - foreach ($targetHost in $tcpTargets[$category]) { + Write-Host " [WARN] Could not parse w32tm output" -ForegroundColor Yellow + } +} catch { + Write-Host " [WARN] Clock check failed: $($_.Exception.Message)" -ForegroundColor Yellow +} + +# ---- Phase 7: Certificate Expiry Check -------------------------------------- +Write-Host "`n[Phase 7] Certificate Expiry Check" -ForegroundColor Cyan +Write-Host "----------------------------------" -ForegroundColor Cyan +$expiringCerts = @() +$expiredCerts = @() +$now = [DateTime]::UtcNow +$warningDays = 60 + +foreach ($chk in $certsToCheck) { + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($chk.Store, $chk.Location) + $store.Open("ReadOnly") + $found = $store.Certificates | Where-Object { $_.Thumbprint -eq $chk.Thumbprint } + $store.Close() + + if ($found) { + $daysLeft = ($found.NotAfter - $now).Days + if ($daysLeft -lt 0) { + Write-Host " [FAIL] $($chk.CN) - EXPIRED ($($found.NotAfter.ToString('yyyy-MM-dd')))" -ForegroundColor Red + Write-Host " Download fresh: $($chk.DownloadUrl)" -ForegroundColor Yellow + $expiredCerts += $chk + } elseif ($daysLeft -lt $warningDays) { + Write-Host " [WARN] $($chk.CN) - expires in $daysLeft days - $($found.NotAfter.ToString('yyyy-MM-dd'))" -ForegroundColor Yellow + Write-Host " Download fresh: $($chk.DownloadUrl)" -ForegroundColor Yellow + $expiringCerts += $chk + } else { + Write-Host " [OK] $($chk.CN) - valid until $($found.NotAfter.ToString('yyyy-MM-dd')) - $daysLeft days remaining" -ForegroundColor Green + } + } +} + +if ($unreachableCount -gt 0 -and ($expiringCerts.Count -gt 0 -or $expiredCerts.Count -gt 0)) { + Write-Host "`n [ALERT] AIA is blocked AND certificates are expiring/expired." -ForegroundColor Red + Write-Host " This VM cannot auto-download replacements." -ForegroundColor Red + Write-Host " Manually download from the URLs above." -ForegroundColor Yellow +} + +# ---- Phase 8: TLS 1.2 Check (Windows-specific) ------------------------------ +Write-Host "`n[Phase 8] TLS 1.2 Configuration" -ForegroundColor Cyan +Write-Host "-------------------------------" -ForegroundColor Cyan +$tls12Issue = $false + +$currentProtocol = [Net.ServicePointManager]::SecurityProtocol +Write-Host " Current SecurityProtocol: $currentProtocol" + +if ($currentProtocol -notmatch 'Tls12') { + Write-Host " [WARN] TLS 1.2 not in SecurityProtocol - HTTPS cert downloads may fail" -ForegroundColor Yellow + $tls12Issue = $true +} + +$regPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' +if (Test-Path $regPath) { + $enabled = (Get-ItemProperty -Path $regPath -Name 'Enabled' -ErrorAction SilentlyContinue).Enabled + $disabled = (Get-ItemProperty -Path $regPath -Name 'DisabledByDefault' -ErrorAction SilentlyContinue).DisabledByDefault + if ($enabled -eq 0 -or $disabled -eq 1) { + Write-Host " [FAIL] TLS 1.2 is disabled in SCHANNEL registry" -ForegroundColor Red + Write-Host " HTTPS cert downloads (OCSP intermediates) will fail." -ForegroundColor Yellow + Write-Host " Fix: Enable TLS 1.2 in registry and restart." -ForegroundColor Yellow + $tls12Issue = $true + } else { + Write-Host " [PASS] TLS 1.2 enabled in SCHANNEL registry" -ForegroundColor Green + } +} else { + # No explicit registry setting = OS default (usually enabled on 2016+) + $osVersion = [System.Environment]::OSVersion.Version + if ($osVersion.Major -eq 6 -and $osVersion.Minor -le 3) { + # Windows Server 2012 R2 or older - TLS 1.2 may not be default + Write-Host " [WARN] No explicit TLS 1.2 registry setting on $([System.Environment]::OSVersion.VersionString)" -ForegroundColor Yellow + Write-Host " Older OS versions may not have TLS 1.2 enabled by default." -ForegroundColor Yellow + $tls12Issue = $true + } else { + Write-Host " [PASS] TLS 1.2 (OS default - no explicit override)" -ForegroundColor Green + } +} + +# ---- Phase 9: Proxy & CryptoAPI Cache Check ---------------------------------- +Write-Host "`n[Phase 9] Proxy & Cache Check" -ForegroundColor Cyan +Write-Host "-----------------------------" -ForegroundColor Cyan + +# Proxy detection +$proxyDetected = $false +try { + $proxyOutput = netsh winhttp show proxy 2>&1 + if ($proxyOutput -match 'Proxy Server') { + $proxyLine = ($proxyOutput | Where-Object { $_ -match 'Proxy Server' }) -join '' + Write-Host " [INFO] WinHTTP proxy configured: $($proxyLine.Trim())" -ForegroundColor Yellow + Write-Host " If AIA endpoints are blocked, add them to bypass list." -ForegroundColor Yellow + $proxyDetected = $true + } else { + Write-Host " [OK] No WinHTTP proxy configured" -ForegroundColor Green + } +} catch { + Write-Host " [WARN] Could not check proxy: $($_.Exception.Message)" -ForegroundColor Yellow +} + +# CryptoAPI cache check - look for OCSP certs in CurrentUser\CA that differ from chain +if ($chain.ChainElements.Count -gt 1) { + $cuStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("CA", "CurrentUser") + $cuStore.Open("ReadOnly") + $staleCerts = @() + foreach ($el in $chain.ChainElements) { + $chainCert = $el.Certificate + if ($chainCert.Subject -match "OCSP") { + $cachedOCSP = $cuStore.Certificates | Where-Object { + $_.Subject -match "OCSP" -and $_.Thumbprint -ne $chainCert.Thumbprint + } + foreach ($stale in $cachedOCSP) { + $staleCerts += $stale + Write-Host " [WARN] Stale OCSP in CurrentUser\CA: $($stale.Subject)" -ForegroundColor Yellow + Write-Host " Thumbprint: $($stale.Thumbprint) (chain uses $($chainCert.Thumbprint))" -ForegroundColor Yellow + } + } + } + $cuStore.Close() + if ($staleCerts.Count -eq 0) { + Write-Host " [OK] No stale OCSP certs in CryptoAPI cache" -ForegroundColor Green + } +} + +# ---- Phase 10: Summary & Recommendations ------------------------------------ +Write-Host "`n=====================================================" -ForegroundColor Cyan +Write-Host "[Summary]" -ForegroundColor Cyan +Write-Host "=====================================================" -ForegroundColor Cyan + +if ($chainBuilt -and $chainErrors.Count -eq 0 -and $missingCerts.Count -eq 0 -and $unreachableCount -eq 0 -and -not $clockSkewDetected -and $expiredCerts.Count -eq 0 -and -not $tls12Issue) { + Write-Host " ALL CHECKS PASSED" -ForegroundColor Green + Write-Host " IMDS attestation certificate chain is healthy." -ForegroundColor Green +} else { + if ($missingCerts.Count -gt 0) { + Write-Host "`n MISSING CERTIFICATES ($($missingCerts.Count)):" -ForegroundColor Red + foreach ($mc in $missingCerts) { + Write-Host " - $($mc.CN) ($($mc.Type))" -ForegroundColor Yellow + Write-Host " Download : $($mc.DownloadUrl)" + Write-Host " Install : $($mc.Location)\$($mc.Store)" + } + Write-Host "`n After installing certificates:" -ForegroundColor Yellow + Write-Host " 1. Run: fclip.exe (from C:\Windows\System32)" -ForegroundColor Yellow + Write-Host " 2. Restart the VM or sign out and sign back in" -ForegroundColor Yellow + Write-Host " Reference: https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details?tabs=certificate-authority-chains" -ForegroundColor Yellow + } + + if (-not $chainBuilt -and $missingCerts.Count -eq 0) { + Write-Host "`n CHAIN VALIDATION FAILED (all known certs present):" -ForegroundColor Red + Write-Host " Possible causes:" -ForegroundColor Yellow + Write-Host " - Certificate in the wrong store (check Phase 4 warnings)" -ForegroundColor Yellow + Write-Host " - Certificate is expired" -ForegroundColor Yellow + Write-Host " - Certificate is in the Disallowed store" -ForegroundColor Yellow + Write-Host " - A newer OCSP intermediate is in use (cert rotation)" -ForegroundColor Yellow + Write-Host " Reference: https://aka.ms/AzVmIMDSValidation" -ForegroundColor Yellow + } + + if ($unreachableCount -gt 0) { + Write-Host "`n CONNECTIVITY ISSUES ($unreachableCount endpoint(s) blocked):" -ForegroundColor Red + Write-Host " Configure firewall/proxy to allow port 80 outbound to" -ForegroundColor Yellow + Write-Host " AIA, CRL, and OCSP endpoints listed above." -ForegroundColor Yellow + Write-Host " Reference: https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details#certificate-downloads-and-revocation-lists" -ForegroundColor Yellow + } +} + +Write-Host "`nChain: DigiCert Global Root G2 > Microsoft TLS RSA Root G2 (cross-sign) > OCSP Intermediate > Leaf" -ForegroundColor Cyan +Write-Host "Additional Information: https://aka.ms/AzVmIMDSValidation" -ForegroundColor Cyan +# ---- AutoFix Phase: Download, Install, Re-validate -------------------------- +if ($AutoFix) { + Write-Host "`n=============================================" -ForegroundColor Magenta + Write-Host " AutoFix: Attempting remediation" -ForegroundColor Magenta + Write-Host "=============================================" -ForegroundColor Magenta + + $fixedCount = 0 + $failedCount = 0 + + # Fix clock skew first (affects all cert validation) + if ($clockSkewDetected) { + Write-Host "`n Fixing clock skew..." -ForegroundColor Cyan + try { + $resyncOutput = w32tm /resync /force 2>&1 + Write-Host " [OK] Clock resynced" -ForegroundColor Green + $fixedCount++ + } catch { + Write-Host " [FAIL] Clock resync failed: $($_.Exception.Message)" -ForegroundColor Red + $failedCount++ + } + } + + # Fix expired certs (re-download from AIA) + if ($expiredCerts.Count -gt 0) { + Write-Host "`n Replacing $($expiredCerts.Count) expired certificate(s)..." -ForegroundColor Cyan + foreach ($ec in $expiredCerts) { try { - $result = Test-NetConnection -ComputerName $targetHost -Port 80 -WarningAction SilentlyContinue - if ($result.TcpTestSucceeded) { - Write-Host " [+] $targetHost : Port 80 reachable" -ForegroundColor Green - } else { - Write-Host " [-] $targetHost : Port 80 unreachable" -ForegroundColor Red - } + $tmpPath = "$env:TEMP\imds_cert_expired_$($ec.Thumbprint).crt" + Invoke-WebRequest -Uri $ec.DownloadUrl -OutFile $tmpPath -UseBasicParsing -TimeoutSec 15 + $targetStore = New-Object System.Security.Cryptography.X509Certificates.X509Store($ec.Store, $ec.Location) + $targetStore.Open("ReadWrite") + $newCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tmpPath) + $targetStore.Add($newCert) + $targetStore.Close() + Write-Host " [OK] Replaced expired $($ec.CN)" -ForegroundColor Green + $fixedCount++ + Remove-Item $tmpPath -Force -ErrorAction SilentlyContinue } catch { - Write-Host " [!] Error testing $targetHost : $($_.Exception.Message)" -ForegroundColor Yellow + Write-Host " [FAIL] $($ec.CN): $($_.Exception.Message)" -ForegroundColor Red + $failedCount++ } } } -} catch { - Write-Host "Unable to connect to the metadata server: $($_.Exception.Message)" -ForegroundColor Red - Write-Host "Please refer to the following link for details about IMDS endpoint connection:" -ForegroundColor Yellow - Write-Host "For more information, visit: https://aka.ms/AzVmIMDSValidation" -ForegroundColor Cyan - Write-Host "https://learn.microsoft.com/azure/virtual-machines/instance-metadata-service" -ForegroundColor Yellow -} + # Fix missing certs + if ($missingCerts.Count -gt 0) { + Write-Host "`n Installing $($missingCerts.Count) missing certificate(s)..." -ForegroundColor Cyan + foreach ($mc in $missingCerts) { + # Skip certs in the Disallowed store + $disStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("Disallowed", $mc.Location) + $disStore.Open("ReadOnly") + $inDisallowed = $disStore.Certificates | Where-Object { $_.Thumbprint -eq $mc.Thumbprint } + $disStore.Close() + if ($inDisallowed) { + Write-Host "`n [SKIP] $($mc.CN) - in Disallowed store (policy decision, cannot auto-fix)" -ForegroundColor Yellow + $failedCount++ + continue + } + + Write-Host "`n Downloading: $($mc.CN)" -ForegroundColor Cyan + Write-Host " URL: $($mc.DownloadUrl)" + try { + $tmpPath = "$env:TEMP\imds_cert_$($mc.Thumbprint).crt" + Invoke-WebRequest -Uri $mc.DownloadUrl -OutFile $tmpPath -UseBasicParsing -TimeoutSec 15 + Write-Host " [OK] Downloaded" -ForegroundColor Green + + $targetStore = New-Object System.Security.Cryptography.X509Certificates.X509Store($mc.Store, $mc.Location) + $targetStore.Open("ReadWrite") + $newCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tmpPath) + $targetStore.Add($newCert) + $targetStore.Close() + + Write-Host " [OK] Installed to $($mc.Location)\$($mc.Store)" -ForegroundColor Green + $fixedCount++ + Remove-Item $tmpPath -Force -ErrorAction SilentlyContinue + } catch { + Write-Host " [FAIL] $($_.Exception.Message)" -ForegroundColor Red + $failedCount++ + } + } + } + + # Re-validate the chain + Write-Host "`n Re-validating certificate chain..." -ForegroundColor Cyan + $chain2 = New-Object System.Security.Cryptography.X509Certificates.X509Chain + $chain2.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online + $chain2Built = $chain2.Build($cert) + + if ($chain2Built) { + Write-Host " [PASS] Certificate chain now validates successfully!" -ForegroundColor Green + } else { + Write-Host " [FAIL] Certificate chain still fails after remediation." -ForegroundColor Red + Write-Host " Review Phase 3 output above for remaining issues." -ForegroundColor Yellow + } + # Run fclip.exe if chain now passes + if ($chain2Built -and (Test-Path "$env:SystemRoot\System32\fclip.exe")) { + Write-Host "`n Running fclip.exe to clear activation watermark..." -ForegroundColor Cyan + try { + & "$env:SystemRoot\System32\fclip.exe" 2>$null + Write-Host " [OK] fclip.exe completed. Sign out and sign back in to clear the watermark." -ForegroundColor Green + } catch { + Write-Host " [WARN] fclip.exe failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + Write-Host "`n AutoFix Summary: $fixedCount fixed, $failedCount failed/skipped" -ForegroundColor Cyan +} elseif ($AutoFix) { + Write-Host "`n [INFO] AutoFix: No issues to fix." -ForegroundColor Green +} +Write-Host "Script completed.`n" -ForegroundColor Cyan -Write-Host "`r`nAdditional Information: https://aka.ms/AzVmIMDSValidation" -ForegroundColor Cyan -Write-Host "`r`nScript completed successfully." -ForegroundColor Cyan \ No newline at end of file