diff --git a/helpers/GenerateResourcesAndImage.ps1 b/helpers/GenerateResourcesAndImage.ps1 index 53cb9afbb..2b30911d0 100644 --- a/helpers/GenerateResourcesAndImage.ps1 +++ b/helpers/GenerateResourcesAndImage.ps1 @@ -66,6 +66,35 @@ Function Show-LatestCommit { } } +function Get-GitHubActionsOidcIdToken { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True)] + [string] $RequestUrl, + [Parameter(Mandatory = $True)] + [string] $RequestToken, + [Parameter(Mandatory = $False)] + [string] $Audience = 'api://AzureADTokenExchange' + ) + + $separator = if ($RequestUrl -match '\?') { '&' } else { '?' } + $urlWithAudience = "${RequestUrl}${separator}audience=$([System.Uri]::EscapeDataString($Audience))" + $headers = @{ Authorization = "Bearer $RequestToken" } + + try { + $response = Invoke-RestMethod -Method Get -Uri $urlWithAudience -Headers $headers + } + catch { + throw "Failed to request GitHub Actions OIDC ID token. Ensure workflow permissions include 'id-token: write'. Details: $($_.Exception.Message)" + } + + if ([string]::IsNullOrEmpty($response.value)) { + throw "GitHub Actions OIDC token response did not contain a 'value' field." + } + + return $response.value +} + function Start-Sleep($seconds) { $doneDT = (Get-Date).AddSeconds($seconds) while ($doneDT -gt (Get-Date)) { @@ -103,6 +132,13 @@ Function GenerateResourcesAndImage { The Azure client secret to use to authenticate with Azure. If not specified, the current user's credentials will be used. .PARAMETER AzureTenantId The Azure tenant id to use to authenticate with Azure. If not specified, the current user's credentials will be used. + .PARAMETER UseOidc + If set, authenticate using GitHub Actions OIDC (federated credentials) instead of a client secret. + Requires AzureClientId and AzureTenantId, and OidcRequestToken/OidcRequestUrl parameters. + .PARAMETER OidcRequestToken + GitHub Actions OIDC request token. + .PARAMETER OidcRequestUrl + GitHub Actions OIDC request URL. .PARAMETER RestrictToAgentIpAddress If set, access to the VM used by packer to generate the image is restricted to the public IP address this script is run from. This parameter cannot be used in combination with the virtual_network_name packer parameter. @@ -143,6 +179,14 @@ Function GenerateResourcesAndImage { [Parameter(Mandatory = $False)] [string] $AzureTenantId, [Parameter(Mandatory = $False)] + [switch] $UseOidc, + [Parameter(Mandatory = $False)] + [ValidateNotNullOrEmpty()] + [string] $OidcRequestToken, + [Parameter(Mandatory = $False)] + [ValidateNotNullOrEmpty()] + [string] $OidcRequestUrl, + [Parameter(Mandatory = $False)] [string] $PluginVersion = "2.2.1", [Parameter(Mandatory = $False)] [switch] $RestrictToAgentIpAddress, @@ -214,10 +258,17 @@ Function GenerateResourcesAndImage { } Write-Host "Validating packer template..." + $validateClientSecret = "fake" + if ($UseOidc) { + $validateClientSecret = "" + } + & $PackerBinary validate ` "-only=$($PackerTemplate.BuildName).*" ` "-var=client_id=fake" ` - "-var=client_secret=fake" ` + "-var=client_secret=$($validateClientSecret)" ` + "-var=oidc_request_token=fake" ` + "-var=oidc_request_url=fake" ` "-var=subscription_id=$($SubscriptionId)" ` "-var=tenant_id=fake" ` "-var=location=$($AzureLocation)" ` @@ -239,10 +290,23 @@ Function GenerateResourcesAndImage { Write-Verbose "No AzureClientId was provided, will use interactive login." az login --output none } + elseif ($UseOidc) { + if ([string]::IsNullOrEmpty($AzureTenantId)) { + throw "AzureTenantId is required for OIDC authentication." + } + + Write-Verbose "Using OIDC service principal login (federated credentials)." + $idToken = Get-GitHubActionsOidcIdToken -RequestUrl $OidcRequestUrl -RequestToken $OidcRequestToken + az login --service-principal --username $AzureClientId --tenant $AzureTenantId --federated-token $idToken --output none + } else { - Write-Verbose "AzureClientId was provided, will use service principal login." + if ([string]::IsNullOrEmpty($AzureClientSecret) -or [string]::IsNullOrEmpty($AzureTenantId)) { + throw "AzureClientSecret and AzureTenantId are required for service principal login unless -UseOidc is specified." + } + Write-Verbose "AzureClientId was provided, will use service principal login (client secret)." az login --service-principal --username $AzureClientId --password=$AzureClientSecret --tenant $AzureTenantId --output none } + az account set --subscription $SubscriptionId if ($LastExitCode -ne 0) { throw "Failed to login to Azure subscription '$SubscriptionId'." @@ -257,7 +321,7 @@ Function GenerateResourcesAndImage { throw "Resource group '$ResourceGroupName' does not exist." } - # Create service principal + # Create / choose authentication for packer if ([string]::IsNullOrEmpty($AzureClientId)) { Write-Host "Creating service principal for packer..." $ADCleanupRequired = $true @@ -277,9 +341,26 @@ Function GenerateResourcesAndImage { Write-Host "Service principal created with id '$ServicePrincipalAppId'. It will be deleted after the build." } else { - $ServicePrincipalAppId = $AzureClientId - $ServicePrincipalPassword = $AzureClientSecret - $TenantId = $AzureTenantId + if ($UseOidc) { + if ([string]::IsNullOrEmpty($AzureTenantId)) { + throw "AzureTenantId is required for OIDC authentication." + } + + $ServicePrincipalAppId = $AzureClientId + $ServicePrincipalPassword = "" + $TenantId = $AzureTenantId + # Avoid leaking OIDC request values via command line arguments. + $env:PKR_VAR_oidc_request_token = $OidcRequestToken + $env:PKR_VAR_oidc_request_url = $OidcRequestUrl + } + else { + if ([string]::IsNullOrEmpty($AzureClientSecret) -or [string]::IsNullOrEmpty($AzureTenantId)) { + throw "AzureClientSecret and AzureTenantId are required for service principal authentication unless -UseOidc is specified." + } + $ServicePrincipalAppId = $AzureClientId + $ServicePrincipalPassword = $AzureClientSecret + $TenantId = $AzureTenantId + } } Write-Debug "Service principal app id: $ServicePrincipalAppId." Write-Debug "Tenant id: $TenantId." @@ -288,6 +369,8 @@ Function GenerateResourcesAndImage { -only "$($PackerTemplate.BuildName).*" ` -var "client_id=$($ServicePrincipalAppId)" ` -var "client_secret=$($ServicePrincipalPassword)" ` + -var "oidc_request_token=$($env:PKR_VAR_oidc_request_token)" ` + -var "oidc_request_url=$($env:PKR_VAR_oidc_request_url)" ` -var "subscription_id=$($SubscriptionId)" ` -var "tenant_id=$($TenantId)" ` -var "location=$($AzureLocation)" `