Script Hangs in Windows 10 While Accessing Azure Key Vault Secrets, Works in Windows 11: Need Assistance

Hi, I’m working on a script where it fetches the details of secrets from an Azure Key Vault and recreates the secrets into another Key Vault of a different tenant. I’m using a Windows Form to provide a GUI to select the required secret.
The script works fine in Windows 11, but when attempting to execute it in Windows 10, the script sometimes gets stuck. It appears to be hanging at line number 183 in the Get-AzKeyvaultSecret cmdlet. This issue occurs occasionally, and I couldn’t understand why it’s happening.
Below are the PowerShell versions:

  1. Windows 10:
    • PowerShell version: 5.1.19041.3031
  2. Windows 11:
    • PowerShell version: 5.1.22621.2506
    Picture1

Code:

<#
Author: NathCorp
Version: V02.1
Created on: 01/05/2024
Last Modified: 02/20/2024
Description: This script is created to migrate specific secrets from Azure Key Vault soruce tenant to a target tenant.

Limitations:
- This script only migrates the secrets (not keys and certificates).

Pre-requisites:
1. Permission to read secret in Tenant A.
2. Permission to create Secret in Tenant B.
3. Azure Key Vault in Tenant B.
4. Machine where execute the script must be reachable to both key vault.

Note: The migrating secret must not already exist in the target tenant's Key Vault.
#>

# Load Windows Forms
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# Load configuration from JSON file
Try {
    $configFilePath = "D:\AxKVMigrationInfo\config.json"
    $config = Get-Content $configFilePath -ErrorAction Stop | ConvertFrom-Json 
}
Catch {
    Write-Host "[ERROR] Config file not found: $_"
    exit 1
}

# Set log file path from the configuration
$logFilePath = $config.FilePaths.Log

# Subscription details for Source Tenant
$sourceSubscriptionId = $config.SourceTenant.SubscriptionId
$sourceVaultName = $config.SourceTenant.VaultName

# Subscription details for Target Tenant
$targetSubscriptionId = $config.TargetTenant.SubscriptionId
$targetVaultName = $config.TargetTenant.VaultName

# CSV file path where migrated secrets will get exported
$logDirectory = Split-Path -Path $LogFilePath -Parent
$CSVFilePath = Join-Path -Path $logDirectory -ChildPath "Migrated_Secrets.csv"


# Function to log messages
function Write-Log {
    param (
        [string]$message,
        [string]$logType
    )

    $logMessage = "$logType - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $message"
    Add-Content -Path $logFilePath -Value $logMessage
}

# Function to check and install requried modules
function Import-RequiredModule {
    Try {
        Import-Module -Name Az.KeyVault -ErrorAction Stop
        Import-Module -Name Az.Accounts -ErrorAction Stop
    }
    catch { 
        Write-Host "The required PowerShell module is not found on the machine. Installing the modules Az.KeyVault and Az.Accounts." 
        Write-Log "The required PowerShell module is not found on the machine. Installing the modules Az.KeyVault and Az.Accounts." "[INFO]"

        Install-Module -Name Az.KeyVault
        Install-Module -Name Az.Accounts
        
        Import-Module -Name Az.KeyVault
        Import-Module -Name Az.Accounts
        Write-Host "Az.KeyVault and Az.Accounts successfully installed and imported" 
        Write-Log "Az.KeyVault and Az.Accounts successfully installed and imported" "[INFO]"
    }
}

# Function to prompt the user to enter tenant credentials
function Prompt-ForCredentials {
    param (
        [string]$account
    )

    $title = "Enter $account Tenant Credentials"
    $message = "Please enter the credentials for $account Tenant."

    [System.Windows.Forms.MessageBox]::Show($message, $title, [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information)
}


# Function to connect to Azure account and log connection event
function Connect-ToAzureAccount {
    param (
        [string]$SubscriptionId,
        [string]$Account
    )

    Write-Host "Enter $Account Tenant credentials"
    # Prompt user for credentials
    Prompt-ForCredentials -account $Account

    # Connect to Azure account
    try {
        Connect-AzAccount -Subscription $SubscriptionId -ErrorAction Stop
        
        # Get the current user
        $currentUser = (Get-AzContext).Account.Id
        Write-Log "Connected to $Account Azure account. User: $currentUser" "[INFO]"
    }
    catch {
        Write-Log "Error connecting to $Account Azure account: $_" "[ERROR]"
        throw "Error connecting to $Account Azure account."
    }
}

# Function to disconnect from Azure account and log disconnection event
function Disconnect-FromAzureAccount {
    param (
        [string]$Account
    )

    # Disconnect from Azure account
    try {
        Disconnect-AzAccount -ErrorAction Stop
        Write-Log "Disconnected from $Account Azure account" "[INFO]"
    }
    catch {
        Write-Log "Error disconnecting from $Account Azure account: $_" "[ERROR]"
        throw "Error disconnecting from $Account Azure account."
    }
}

# Function to display a confirmation message box
function Show-ConfirmationBox {
    param (
        [string]$message
    )

    return [System.Windows.Forms.MessageBox]::Show(
        $message,
        "Confirmation",
        [System.Windows.Forms.MessageBoxButtons]::OKCancel,
        [System.Windows.Forms.MessageBoxIcon]::Warning
    )
}


# Function to export specific secrets from source Key Vault based on CSV file
function Export-SpecificSecretsFromSourceKeyVault {
    param (
        [string]$SourceVaultName,
        [string]$SourceSubscriptionId,
        [string[]]$ArrSelectedSecretName
    )

    # Update the minimum and maximum of the progress bar.
    $ProgressBar.Minimum = 0
    $ProgressBar.Maximum = $ArrSelectedSecretName.Count

    # Update the text of the Progress label.
    $progressLable.Text = "Exporting....."

    Write-Host "Exporting specific secrets from source Key Vault ($SourceVaultName) in Subscription ($SourceSubscriptionId)"
    Write-Log "Exporting specific secrets from source Key Vault ($SourceVaultName) in Subscription ($SourceSubscriptionId)" "[INFO]"

    # Export specific secrets based on Selected Items
    $SpecificSecretDetailsExport = foreach ($secretName in $ArrSelectedSecretName) {
        
        # update progress status:
        $ProgressBar.Value = $ProgressBarValue
        
        try {
            Get-AzKeyVaultSecret -VaultName $SourceVaultName -Name $secretName -ErrorAction Stop
        }
        catch {
            Write-Log "Error exporting secret '$secretName' from source Key Vault: $_" "[ERROR]"
        }
        $ProgressBarValue++
    }

    # Update the text of the Progress label.
    $progressLable.Text = "Exported"

    Write-Host "Exporting Successful."
    Write-Log "Exporting Successful." "[INFO]"

    Disconnect-FromAzureAccount -Account "Source"

    return $SpecificSecretDetailsExport
}


# Function to export migrated or imported secrets to CSV
function Export-SecretsToCSV {
    param (
        [string]$CSVFilePath,
        [PSCustomObject]$SecretDetail
    )

    # Extract specific details for each secret
    $exportData = [PSCustomObject]@{
        SecretName     = $SecretDetail.Name
        ContentType    = $SecretDetail.ContentType
        ActivationDate = $SecretDetail.NotBefore
        ExpiryDate     = $SecretDetail.Expires
        State          = $SecretDetail.Enabled
    }

    $exportData | Export-Csv -Path $CSVFilePath -NoTypeInformation -Append
    Write-Log "Secrets exported to CSV file: $CSVFilePath" "[INFO]"
}



# Function to import secrets into target Key Vault
function Import-SecretsIntoTargetKeyVault {
    param (
        [string]$TargetVaultName,
        [string]$TargetSubscriptionId,
        [Array]$SecretDetails
    )

    Connect-ToAzureAccount -SubscriptionId $TargetSubscriptionId -Account "Target"

    # Update the minimum and maximum of the progress bar.
    $ProgressBar.Minimum = 0
    $ProgressBar.Maximum = $SecretDetails.Count

    # Update the text of the Progress label.
    $progressLable.Text = "Importing....."

    Write-Log "Importing secrets into target Key Vault ($TargetVaultName) in Subscription ($TargetSubscriptionId)." "[INFO]"

    # Core logic
    foreach ($SD in $SecretDetails) {
        $SecretName = $SD.Name

        # update progress status:
        $ProgressBar.Value = $ProgressBarValue

        if ($SecretName -ne $null) {
            # Check if the secret already exists in the target Key Vault
            $existingSecret = Get-AzKeyVaultSecret -VaultName $TargetVaultName -Name $SecretName -ErrorAction SilentlyContinue
            if ($existingSecret -eq $null) {
                # Create secret into the Target Tenant
                try {
                    Set-AzKeyVaultSecret -VaultName $TargetVaultName -Name $SecretName -SecretValue $SD.SecretValue -Expires $SD.Expires -NotBefore $SD.NotBefore -ContentType $SD.ContentType -Tag $SD.Tags -ErrorAction Stop
                    
                    Write-Log "Secret '$SecretName' imported successfully." "[SUCCESS]"

                    # Call Export-SecretsToCSV function after importing secrets into the target Key Vault
                    Export-SecretsToCSV -CSVFilePath $CSVFilePath -SecretDetail $SD
                    
                }
                catch {
                    Write-Log "Error importing secret '$SecretName' into target Key Vault: $_" "[ERROR]"
                }
            }
            else {
                Write-Log "Secret '$SecretName' already exists in the target Key Vault. Skipping." "[WARNING]"
            }
        }
    
        $ProgressBarValue++
    
    }

    # Update the text of the Progress label.
    $progressLable.Text = "Imported"

    Disconnect-FromAzureAccount -Account "Target"

    Write-Log "Import completed successfully." "[INFO]"
}


# Define minimum form size
$minimumFormSize = New-Object System.Drawing.Size(500, 550)

# Function to dynamically adjust the size of $checkedListBox based on form size
function Resize-CheckedListBox {
    $checkedListBox.Size = New-Object System.Drawing.Size(($form.ClientSize.Width - 30), ($form.ClientSize.Height - 150))
    $buttonSelectAll.Location = New-Object System.Drawing.Point(10, ($form.ClientSize.Height - 120))
    $buttonDeselectAll.Location = New-Object System.Drawing.Point(170, ($form.ClientSize.Height - 120))
    $buttonMigrate.Location = New-Object System.Drawing.Point(($form.ClientSize.Width - $buttonMigrate.Width - 30), ($form.ClientSize.Height - 120))
    $progressBar.Location = New-Object System.Drawing.Point(10, ($form.ClientSize.Height - 60))
    $progressLable.Location = New-Object System.Drawing.Point(350, ($form.ClientSize.Height - 60))
}

# Function to enforce minimum form size
function Enforce-MinimumFormSize {
    if ($form.ClientSize.Width -lt $minimumFormSize.Width -or $form.ClientSize.Height -lt $minimumFormSize.Height) {
        $form.ClientSize = $minimumFormSize
    }
}

# Function to enable or disable the migration button based on selection
function Update-MigrationButton {
    if ($checkedListBox.CheckedItems.Count -gt 0) {
        $buttonMigrate.Enabled = $true
    }
    else {
        $buttonMigrate.Enabled = $false
    }
}

# Create Form
$form = New-Object System.Windows.Forms.Form
$form.Text = "AzKV Secret Migration Tool"
$form.Size = $minimumFormSize  # Set initial form size to minimum
$form.StartPosition = "CenterScreen"

# Attach Resize event handler to form
$form.add_Resize({
        Resize-CheckedListBox
        Enforce-MinimumFormSize
    })

# Attach Load event handler to form to ensure minimum size is enforced initially
$form.add_Load({
        Enforce-MinimumFormSize
    })

# Create CheckedListBox
$checkedListBox = New-Object System.Windows.Forms.CheckedListBox
$checkedListBox.Location = New-Object System.Drawing.Point(10, 10)
$checkedListBox.Font = New-Object System.Drawing.Font("Arial", 12)  # Adjust the font size as needed
$checkedListBox.ItemHeight = 30  # Adjust the height as needed

# Create Button for Select All
$buttonSelectAll = New-Object System.Windows.Forms.Button
$buttonSelectAll.Size = New-Object System.Drawing.Size(150, 30)
$buttonSelectAll.Location = New-Object System.Drawing.Point(10, ($form.ClientSize.Height - 120))
$buttonSelectAll.Text = "Select All"

# Add Click event to the Select All Button
$buttonSelectAll.Add_Click({
        # Check all checkboxes
        for ($i = 0; $i -lt $checkedListBox.Items.Count; $i++) {
            $checkedListBox.SetItemChecked($i, $true)

            # Update migration button status
            Update-MigrationButton
        }
    })

# Create Button for Deselect All
$buttonDeselectAll = New-Object System.Windows.Forms.Button
$buttonDeselectAll.Size = New-Object System.Drawing.Size(150, 30)
$buttonDeselectAll.Location = New-Object System.Drawing.Point(170, ($form.ClientSize.Height - 120))
$buttonDeselectAll.Text = "Deselect All"

# Add Click event to the Deselect All Button
$buttonDeselectAll.Add_Click({
        # Uncheck all checkboxes
        for ($i = 0; $i -lt $checkedListBox.Items.Count; $i++) {
            $checkedListBox.SetItemChecked($i, $false)

            # Update migration button status
            Update-MigrationButton
        }
    })

# Create Button for Migrate
$buttonMigrate = New-Object System.Windows.Forms.Button
$buttonMigrate.Size = New-Object System.Drawing.Size(150, 30)
$buttonMigrate.Text = "Migrate Secrets"
$buttonMigrate.Enabled = $false  # Initially disable the button


# Import required Module
Import-RequiredModule


# Get secrets from source Key Vault
Connect-ToAzureAccount -SubscriptionId $SourceSubscriptionId -Account "Source"
Try {
    $secrets = Get-AzKeyVaultSecret -VaultName $sourceVaultName
}
catch {
    Write-Log "Error Listing secret '$secretName' from source Key Vault: $_" "[ERROR]"
    exit 1
}


# Populate CheckedListBox with secret names
$secrets | ForEach-Object {
    [void]$checkedListBox.Items.Add($_.Name)
}

# Progress Bar
$progressBar = New-Object System.Windows.Forms.ProgressBar
$progressBar.Style = "Continuous"
$progressBar.Width = 310
$progressBar.Height = 25
$progressBar.Location = New-Object System.Drawing.Point(10, ($form.ClientSize.Height - 60))
$ProgressBarValue = 1

# Progress bar label
$progressLable = New-Object System.Windows.Forms.Label
$progressLable.AutoSize = $true
$progressLable.Width = 100
$progressLable.Height = 25
$progressLable.Location = New-Object System.Drawing.Point(350, ($form.ClientSize.Height - 60))




# Add Click event to the Button
$buttonMigrate.Add_Click({
        # Display confirmation message box
        $confirmation = Show-ConfirmationBox "The selected secrets will be Imported into the target tenant. If you want to migrate click on OK"
        

        # Check if the user clicked OK
        if ($confirmation -eq [System.Windows.Forms.DialogResult]::OK) {
            # Get selected secrets
            $selectedSecrets = Export-SpecificSecretsFromSourceKeyVault -SourceVaultName $sourceVaultName -SourceSubscriptionId $sourceSubscriptionId -ArrSelectedSecretName $checkedListBox.CheckedItems
            
            # Proceed with migration
            Import-SecretsIntoTargetKeyVault -TargetVaultName $targetVaultName -TargetSubscriptionId $targetSubscriptionId -SecretDetails $selectedSecrets
            
            # Show Import successful message
            [System.Windows.Forms.MessageBox]::Show("Imported successfully! For more details, check the logs file.", "Import Successful", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information)

            $form.Close()
        }
        
    })


# Add controls to the form
$form.Controls.AddRange(@($checkedListBox, $buttonSelectAll, $buttonDeselectAll, $buttonMigrate, $progressBar, $progressLable))

# Add Change event to the checkedListBox to update migration button status
$checkedListBox.Add_SelectedIndexChanged({ Update-MigrationButton })

# Call Resize-CheckedListBox to set initial sizes and positions
Resize-CheckedListBox

# Show the form
[void]$form.ShowDialog()

# Display a message indicating the completion of the script
Write-Log "Key Vault migration script completed successfully." "[INFO]"

# Display the log file path
Write-Host "Log file exported to: $logFilePath"

# Display the log file path
Write-Host "CSV file exported to: $CSVFilePath"

# Remove all variables for no longer access.
Remove-Variable * -ErrorAction SilentlyContinue

Any help will be highly appreciated.

Hi, welcome to the forum :wave:

Hard to help with no code, and the tiny screenshot is no help :eyeglasses:.

If I had to hazard a guess, it could be the TLS version. Try forcing it to version 1.2 at the start of the script (or at least before you attempt any web connections).

[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12

Thank you for your response, I have uploaded the code, and I have tired enabling TLS1.2 but didn’t work.

Can you define what ‘get stuck’ means? Does it run forever and not stop? No error? You are 100% positive it’s getting stuck at that point?. May try throwing on -verbose and seeing if anything more spits out that may hint at the issue.

EDIT: also is this reproducible on other win10 boxes? Any chance it’s just something odd with that one box?

Hi, First thank you for your response. Getting stuck means “The script stops further execution in a particular line without any error”. I have tried this with different Windows 10 boxes. In all the boxes I’m getting the same result. Further, I have tried with Powershell 7 and it works fine with PowerShell 7.4.1.

I didn’t try with -verbose switch. thank you for your suggestion.