Azure / trusted-signing-action

MIT License
21 stars 8 forks source link

Azure DevOps Workload Identity Support? #21

Closed JeffBrownTech closed 1 month ago

JeffBrownTech commented 1 month ago

I've been trying to get the TrustedSigning@0 task to work with my service connection using workload identity federation with a user-managed identity. I am using the AzureCLI task to log in using the service connection and exporting environment variables to the pipeline (much like you can with Terraform).

I am curious if this scenario is supported, and if so, can more documentation be provided with an example?

- task: AzureCLI@2
  inputs:
    azureSubscription: '<service connection>'
    scriptType: 'pscore'
    scriptLocation: 'inlineScript'
    inlineScript: |
      Write-Host "##vso[task.setvariable variable=AZURE_CLIENT_ID]$env:servicePrincipalId"
      Write-Host "##vso[task.setvariable variable=AZURE_TENANT_ID]$env:tenantId"
      Write-Host "##vso[task.setvariable variable=AZURE_USE_OIDC]true"
      Write-Host "##vso[task.setvariable variable=AZURE_OIDC_TOKEN]$env:idToken"
    addSpnToEnvironment: true
    useGlobalConfig: true
  env:
    ARM_USE_AZUREAD: true

- task: TrustedSigning@0
  inputs:
    ExcludeEnvironmentCredential: true
    ExcludeSharedTokenCacheCredential: true
    ExcludeVisualStudioCredential: true
    ExcludeVisualStudioCodeCredential: true
    ExcludeAzureCliCredential: true
    ExcludeAzurePowerShellCredential: true
    Endpoint: 'https://eus.codesigning.azure.net/'
    CodeSigningAccountName: '<name>'
    CertificateProfileName: '<profile>'
    FilesFolder: '$(System.DefaultWorkingDirectory)'
    FilesFolderFilter: 'ps1'
    FileDigest: 'SHA256'

However, the TrustedSigning fails:

"Metadata": {
  "Endpoint": "https://eus.codesigning.azure.net/"
  "CodeSigningAccountName": "<name>",
  "CertificateProfileName": "<profile>",
  "ExcludeCredentials": [
    "EnvironmentCredential",
    "SharedTokenCacheCredential",
    "VisualStudioCredential",
    "VisualStudioCodeCredential",
    "AzureCliCredential",
    "AzurePowerShellCredential",
    "InteractiveBrowserCredential"
  ]
}

Submitting digest for signing...
Unhandled managed exception
Azure.Identity.CredentialUnavailableException: DefaultAzureCredential failed to retrieve a token from the included credentials. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/defaultazurecredential/troubleshoot
- WorkloadIdentityCredential authentication unavailable. The workload options are not fully configured. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/workloadidentitycredential/troubleshoot
- ManagedIdentityCredential authentication unavailable. Multiple attempts failed to obtain a token from the managed identity endpoint.
- Azure Developer CLI could not be found.
 ---> System.AggregateException: Multiple exceptions were encountered while attempting to authenticate. (WorkloadIdentityCredential authentication unavailable. The workload options are not fully configured. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/workloadidentitycredential/troubleshoot) (ManagedIdentityCredential authentication unavailable. Multiple attempts failed to obtain a token from the managed identity endpoint.) (Azure Developer CLI could not be found.)
 ---> Azure.Identity.CredentialUnavailableException: WorkloadIdentityCredential authentication unavailable. The workload options are not fully configured. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/workloadidentitycredential/troubleshoot
   at Azure.Identity.WorkloadIdentityCredential.GetTokenCoreAsync(Boolean async, TokenRequestContext requestContext, CancellationToken cancellationToken)
   at Azure.Identity.CredentialDiagnosticScope.FailWrapAndThrow(Exception ex, String additionalMessage, Boolean isCredentialUnavailable)
   at Azure.Identity.WorkloadIdentityCredential.GetTokenCoreAsync(Boolean async, TokenRequestContext requestContext, CancellationToken cancellationToken)
   at Azure.Identity.WorkloadIdentityCredential.GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
   at Azure.Identity.DefaultAzureCredential.GetTokenFromSourcesAsync(TokenCredential[] sources, TokenRequestContext requestContext, Boolean async, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
japarson commented 1 month ago

Hi @JeffBrownTech, I'm also going through the process of trying to get this to work for Azure Pipelines. What happens if you remove all of the credential exclusion inputs? In particular, "AzureCliCredential".

JeffBrownTech commented 1 month ago

@japarson The task still fails, just with more auth options. I should have noted this is on a self-hosted Windows agent. For now I'm going to revert to a client ID and secret and but would like to find out if this is scenario is possible.

japarson commented 1 month ago

@JeffBrownTech I was able to authenticate with AzureCliCredential (App Registration + Federated Credential) using the strategy outlined in this post: https://stackoverflow.com/a/78297452/15081913

I'm not sure if this will also work with your managed identity as I'm still new to this. Might be worth trying.

steps:
- task: AzureCLI@2
  displayName: 'Azure CLI'
  inputs:
    azureSubscription: '<service_connection>'
    addSpnToEnvironment: true
    scriptType: bash
    scriptLocation: inlineScript
    inlineScript: |
     echo "##vso[task.setvariable variable=ARM_CLIENT_ID;issecret=true]$servicePrincipalId" 
     echo "##vso[task.setvariable variable=ARM_ID_TOKEN;issecret=true]$idToken"
     echo "##vso[task.setvariable variable=ARM_TENANT_ID;issecret=true]$tenantId"

- bash: |
   az login --service-principal -u $(ARM_CLIENT_ID) --tenant $(ARM_TENANT_ID) --allow-no-subscriptions --federated-token $(ARM_ID_TOKEN)
  displayName: 'Login Azure'

P.S. Make sure to use 'bash' for the script types.

JeffBrownTech commented 1 month ago

@japarson That works! Below is my final code, it work in PowerShell for me and using a managed identity in the service connection. Interesting that a second az login is what did it. AFAIK, the AzureCLI does a log into Azure, but maybe it only persists for that task's duration and not the rest of the pipeline.

I think an improvement to the TrustedSigning task would be to follow what the TerraformTask does. You can specify a service connection, and the task auto-logins in with that service connection's authentication mechanism (secret, workload identity, etc.).

- task: AzureCLI@2
  inputs:
    azureSubscription: '<service connection>'
    scriptType: 'pscore'
    scriptLocation: 'inlineScript'
    inlineScript: |
      Write-Host "##vso[task.setvariable variable=ARM_CLIENT_ID]$env:servicePrincipalId"
      Write-Host "##vso[task.setvariable variable=ARM_TENANT_ID]$env:tenantId"
      Write-Host "##vso[task.setvariable variable=ARM_ID_TOKEN]$env:idToken"
    addSpnToEnvironment: true

- task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      az login --service-principal -u $(ARM_CLIENT_ID) --tenant $(ARM_TENANT_ID) --allow-no-subscriptions --federated-token $(ARM_ID_TOKEN)

- task: TrustedSigning@0
  inputs:
    ExcludeSharedTokenCacheCredential: true
    ExcludeVisualStudioCredential: true
    ExcludeVisualStudioCodeCredential: true
    Endpoint: 'https://eus.codesigning.azure.net/'
    CodeSigningAccountName: '<name>'
    CertificateProfileName: '<profile>'
    FilesFolder: '$(System.DefaultWorkingDirectory)'
    FileDigest: 'SHA256'
japarson commented 1 month ago

I'll follow up on your suggested improvement in the new issue I created. Thank you!

japarson commented 1 month ago

@JeffBrownTech Hi Jeff, I wanted to provide an important modification to the guidance I provided earlier. When setting the variables in the AzureCLI task, make sure to use ;issecret=true to protect your secrets in the pipeline.

Before

inlineScript: |
      Write-Host "##vso[task.setvariable variable=ARM_CLIENT_ID]$env:servicePrincipalId"
      Write-Host "##vso[task.setvariable variable=ARM_TENANT_ID]$env:tenantId"
      Write-Host "##vso[task.setvariable variable=ARM_ID_TOKEN]$env:idToken"

After

inlineScript: |
      Write-Host "##vso[task.setvariable variable=ARM_CLIENT_ID;issecret=true]$env:servicePrincipalId"
      Write-Host "##vso[task.setvariable variable=ARM_TENANT_ID;issecret=true]$env:tenantId"
      Write-Host "##vso[task.setvariable variable=ARM_ID_TOKEN;issecret=true]$env:idToken"