Public/Connect-AzureLocalServicePrincipal.ps1
|
function Connect-AzureLocalServicePrincipal { <# .SYNOPSIS Authenticates to Azure using a Service Principal or Managed Identity for CI/CD automation. .DESCRIPTION Logs into Azure CLI using Service Principal credentials or Managed Identity (MSI), enabling automated operations in GitHub Actions, Azure DevOps Pipelines, or other CI/CD systems. Authentication methods: 1. Managed Identity (-UseManagedIdentity): For Azure-hosted runners/agents with assigned identity 2. Service Principal (default): Using credentials from parameters or environment variables Service Principal credentials can be provided via: - Parameters: -ServicePrincipalId, -ServicePrincipalSecret, -TenantId - Environment variables: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID If already authenticated (interactively or via SP/MSI), this function will skip login unless -Force is specified. .PARAMETER UseManagedIdentity Use Managed Identity (MSI) authentication instead of Service Principal. This is useful for Azure-hosted runners, VMs, or Azure Container Instances that have a system-assigned or user-assigned managed identity. .PARAMETER ManagedIdentityClientId Optional. The client ID of a user-assigned managed identity to use. If not specified, the system-assigned managed identity will be used. .PARAMETER ServicePrincipalId The Application (client) ID of the Service Principal. Can also be set via AZURE_CLIENT_ID environment variable. Not used when -UseManagedIdentity is specified. .PARAMETER ServicePrincipalSecret The client secret for the Service Principal. Can also be set via AZURE_CLIENT_SECRET environment variable. For security, prefer a [SecureString] or the AZURE_CLIENT_SECRET environment variable. Accepts both [string] (plaintext, logs a security warning) and [SecureString]. Plaintext passing via command line is discouraged because process command-line arguments may be visible to other users/EDR on the host. Not used when -UseManagedIdentity is specified. .PARAMETER TenantId The Azure AD tenant ID. Can also be set via AZURE_TENANT_ID environment variable. Not used when -UseManagedIdentity is specified. .PARAMETER Force Force re-authentication even if already logged in. .OUTPUTS Returns $true if authentication succeeded, $false otherwise. .EXAMPLE # Using Managed Identity (system-assigned) - recommended for Azure-hosted agents Connect-AzureLocalServicePrincipal -UseManagedIdentity .EXAMPLE # Using Managed Identity (user-assigned) with specific client ID Connect-AzureLocalServicePrincipal -UseManagedIdentity -ManagedIdentityClientId "12345678-1234-1234-1234-123456789012" .EXAMPLE # Using Service Principal with SecureString (preferred when not using env vars) $secret = Read-Host -AsSecureString -Prompt 'Service Principal Secret' Connect-AzureLocalServicePrincipal -ServicePrincipalId $appId -ServicePrincipalSecret $secret -TenantId $tenant .EXAMPLE # Using environment variables (recommended for CI/CD with Service Principal) $env:AZURE_CLIENT_ID = 'your-app-id' $env:AZURE_CLIENT_SECRET = 'your-secret' $env:AZURE_TENANT_ID = 'your-tenant-id' Connect-AzureLocalServicePrincipal .EXAMPLE # GitHub Actions workflow - credentials from secrets # env: # AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} # AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} # AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} Connect-AzureLocalServicePrincipal .NOTES The Service Principal or Managed Identity requires the following permissions: - Microsoft.AzureStackHCI/clusters/read - Microsoft.AzureStackHCI/clusters/updates/read - Microsoft.AzureStackHCI/clusters/updates/apply/action - Microsoft.AzureStackHCI/clusters/updateSummaries/read - Microsoft.AzureStackHCI/clusters/updates/updateRuns/read - Microsoft.Resources/subscriptions/resources/read (for Azure Resource Graph queries) - Tag Contributor role (for Set-AzureLocalClusterUpdateRingTag) #> [CmdletBinding(DefaultParameterSetName = 'ServicePrincipal')] [OutputType([bool])] param( [Parameter(Mandatory = $true, ParameterSetName = 'ManagedIdentity')] [switch]$UseManagedIdentity, [Parameter(Mandatory = $false, ParameterSetName = 'ManagedIdentity')] [string]$ManagedIdentityClientId, [Parameter(Mandatory = $false, ParameterSetName = 'ServicePrincipal')] [string]$ServicePrincipalId, [Parameter(Mandatory = $false, ParameterSetName = 'ServicePrincipal')] # Accept either [string] (plaintext - backward compatible, warns) or [SecureString]. [object]$ServicePrincipalSecret, [Parameter(Mandatory = $false, ParameterSetName = 'ServicePrincipal')] [string]$TenantId, [Parameter(Mandatory = $false)] [switch]$Force ) # Check for existing authentication unless Force is specified if (-not $Force) { try { $accountInfo = az account show 2>$null | ConvertFrom-Json if ($LASTEXITCODE -eq 0 -and $accountInfo) { Write-Verbose "Already authenticated as: $($accountInfo.user.name) (Type: $($accountInfo.user.type))" return $true } } catch { # Not authenticated, continue with login - this is expected behavior Write-Verbose "No existing Azure CLI session, proceeding with authentication" } } # Managed Identity authentication if ($UseManagedIdentity) { Write-Log -Message "Authenticating with Managed Identity..." -Level Warning try { if ($ManagedIdentityClientId) { # User-assigned managed identity Write-Log -Message "Using user-assigned managed identity: $ManagedIdentityClientId" -Level Verbose $loginResult = az login --identity --username $ManagedIdentityClientId --output none 2>&1 } else { # System-assigned managed identity Write-Log -Message "Using system-assigned managed identity" -Level Verbose $loginResult = az login --identity --output none 2>&1 } if ($LASTEXITCODE -ne 0) { Write-Error "Managed Identity authentication failed: $loginResult" Write-Error "Ensure this environment has a managed identity assigned and it has the required permissions." return $false } # Verify authentication $accountInfo = az account show 2>$null | ConvertFrom-Json if ($LASTEXITCODE -eq 0 -and $accountInfo) { Write-Log -Message "Successfully authenticated with Managed Identity" -Level Success Write-Log -Message "Subscription: $($accountInfo.name) ($($accountInfo.id))" -Level Verbose $script:ManagedIdentityAuthenticated = $true return $true } else { Write-Error "Authentication succeeded but account verification failed." return $false } } catch { Write-Error "Managed Identity authentication error: $($_.Exception.Message)" return $false } } # Service Principal authentication (default) # Get credentials from parameters or environment variables $clientId = if ($ServicePrincipalId) { $ServicePrincipalId } else { $env:AZURE_CLIENT_ID } # Resolve secret: [SecureString] preferred, [string] accepted for backward compat (with warning) $clientSecretPlain = $null $secretBstr = [IntPtr]::Zero try { if ($ServicePrincipalSecret -is [System.Security.SecureString]) { $secretBstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ServicePrincipalSecret) $clientSecretPlain = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($secretBstr) } elseif ($ServicePrincipalSecret -is [string] -and $ServicePrincipalSecret) { Write-Log -Message "SECURITY: -ServicePrincipalSecret was supplied as plaintext [string]. Secret may be visible in process command line to other users on this host. Prefer [SecureString] or the AZURE_CLIENT_SECRET environment variable for CI/CD." -Level Warning $clientSecretPlain = $ServicePrincipalSecret } elseif ($null -ne $ServicePrincipalSecret) { throw "-ServicePrincipalSecret must be a [string] or [SecureString]. Got: $($ServicePrincipalSecret.GetType().FullName)" } else { $clientSecretPlain = $env:AZURE_CLIENT_SECRET } $tenant = if ($TenantId) { $TenantId } else { $env:AZURE_TENANT_ID } # Validate required credentials if (-not $clientId) { throw "Service Principal ID not provided. Set -ServicePrincipalId parameter or AZURE_CLIENT_ID environment variable." } if (-not $clientSecretPlain) { throw "Service Principal Secret not provided. Set -ServicePrincipalSecret parameter or AZURE_CLIENT_SECRET environment variable." } if (-not $tenant) { throw "Tenant ID not provided. Set -TenantId parameter or AZURE_TENANT_ID environment variable." } Write-Log -Message "Authenticating with Service Principal..." -Level Warning try { # Login using Service Principal $loginResult = az login --service-principal ` --username $clientId ` --password $clientSecretPlain ` --tenant $tenant ` --output none 2>&1 if ($LASTEXITCODE -ne 0) { Write-Error "Service Principal authentication failed: $loginResult" return $false } # Verify authentication $accountInfo = az account show 2>$null | ConvertFrom-Json if ($LASTEXITCODE -eq 0 -and $accountInfo) { Write-Log -Message "Successfully authenticated as Service Principal: $($accountInfo.user.name)" -Level Success Write-Log -Message "Subscription: $($accountInfo.name) ($($accountInfo.id))" -Level Verbose $script:ServicePrincipalAuthenticated = $true return $true } else { Write-Error "Authentication succeeded but account verification failed." return $false } } catch { Write-Error "Service Principal authentication error: $($_.Exception.Message)" return $false } } finally { # Scrub plaintext secret from memory as soon as az login returns if ($secretBstr -ne [IntPtr]::Zero) { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($secretBstr) } $clientSecretPlain = $null } } |