5

After upgrading to Azure DevOps Server 2019, automated pipeline builds are failing at the NuGet restore step showing:

Error: Error: unable to get local issuer certificate

Packages failed to restore

Microsoft's documentation states that the build agent running on Windows uses the Windows certificate store, so I have checked that the required certificates are installed correctly on the build server, however it is still failing.

There are many questions with similar symptoms but not the same cause. After investigation, I have found the solution to this but I didn't spot anything on this exact issue so I will post an answer that will hopefully save somebody else some time!

Andy Hames
  • 481
  • 3
  • 15

3 Answers3

4

It turns out that the Azure DevOps build agent is using a version of Node.js that doesn't use the Windows Certificate Store.

The solution required is to point Node.js at an exported copy (*.cer file) of your self-signed SSL certificate's root CA certificate, using either a system environment variable called NODE_EXTRA_CA_CERTS or by using a Task Variable called NODE.EXTRA.CA.CERTS, with a value pointing to the certificate.

Developer Community Issue Link

Andy Hames
  • 481
  • 3
  • 15
  • I used the powershell command: `[Environment]::SetEnvironmentVariable("NODE_EXTRA_CA_CERTS", "C:\Path\To\Certificates\our_certificate.cer", "Machine")`, then `Get-service vstsagent* | stop-service -force` and finally Get-service `vstsagent* | stop-service` – reckface Nov 27 '19 at 17:00
  • Not sure why, but I needed the ".pem" file. – Robert Muehsig May 20 '20 at 10:12
3

I use a PowerShell agent job with the following script. This effectively gives a "Use the Windows Machine Certificate Store" option to Node.JS for the pipeline.

Some notes:

  • Monitoring node.exe with ProcMon suggests that the file referenced in NODE_EXTRA_CA_CERTS is read every time the pipeline is run. However, others have suggested running Restart-Service vstsagent* -Force is required for the change to be picked up. This isn't my experience but perhaps something different between environments causes this behaviour.

  • This adds an additional ~1s pipeline execution time. Probably an acceptable price for a "set and forget certificate management for Node in Pipelines on Windows" but worth noting nonetheless.

# If running in a pipeline then use the Agent Home directory,
# otherwise use the machine temp folder which is useful for testing
if ($env:AGENT_HOMEDIRECTORY -ne $null) { $TargetFolder = $env:AGENT_HOMEDIRECTORY }
else { $TargetFolder = [System.Environment]::GetEnvironmentVariable('TEMP','Machine') }

# Loop through each CA in the machine store
Get-ChildItem -Path Cert:\LocalMachine\CA | ForEach-Object {

    # Convert cert's bytes to Base64-encoded text and add begin/end markers
    $Cert = "-----BEGIN CERTIFICATE-----`n"
    $Cert+= $([System.Convert]::ToBase64String($_.export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert),'InsertLineBreaks'))
    $Cert+= "`n-----END CERTIFICATE-----`n"

    # Append cert to chain
    $Chain+= $Cert
}

# Build target path
$CertFile = "$TargetFolder\TrustedRootCAs.pem"

# Write to file system
$Chain | Out-File $CertFile -Force -Encoding ASCII

# Clean-up
$Chain = $null

# Let Node (running later in the pipeline) know from where to read certs
Write-Host "##vso[task.setvariable variable=NODE.EXTRA.CA.CERTS]$CertFile"
alifen
  • 51
  • 4
0

I formatted the PowerShell script from @alifen. The script below can be executed on the build agent itself. It takes a parameter for the target path and sets the environment variable on the server.

Credit to @alifen


[CmdletBinding()]
param (
    [Parameter()]
    [string]
    $TargetFolder = "$env:SystemDrive\Certs"
)
If (-not(Test-Path $TargetFolder))
{
    $null = New-Item -ItemType Directory -Path $TargetFolder -Force
}
# Loop through each CA in the machine store
Get-ChildItem -Path Cert:\LocalMachine\CA | ForEach-Object {

    # Convert cert's bytes to Base64-encoded text and add begin/end markers
    $Cert = "-----BEGIN CERTIFICATE-----`n"
    $Cert += $([System.Convert]::ToBase64String($_.export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert), 'InsertLineBreaks'))
    $Cert += "`n-----END CERTIFICATE-----`n"

    # Append cert to chain
    $Chain += $Cert
}

# Build target path
$CertFile = "$TargetFolder\TrustedRootCAs.pem"

# Write to file system
Write-Host "[$($MyInvocation.MyCommand.Name)]: Exporting certs to: [$CertFile]"
$Chain | Out-File $CertFile -Force -Encoding ASCII

# Set Environment variable
Write-Host "[$($MyInvocation.MyCommand.Name)]: Setting environment variable [NODE_EXTRA_CA_CERTS] to [$CertFile]"
[Environment]::SetEnvironmentVariable("NODE_EXTRA_CA_CERTS", "$CertFile", "Machine")

Dejulia489
  • 551
  • 3
  • 9