在 NuGet 还原期间使用带有 "unable to get local issuer certificate" 的自签名 SSL 证书时,Azure DevOps Server 管道构建失败

Azure DevOps Server pipeline build fails when using self-signed SSL certificate with "unable to get local issuer certificate" during NuGet restore

升级到 Azure DevOps Server 2019 后,自动管道构建在 NuGet 还原步骤失败,显示:

Error: Error: unable to get local issuer certificate

Packages failed to restore

Microsoft's documentation 指出 Windows 上的构建代理 运行 使用 Windows 证书存储区,因此我已检查构建中是否正确安装了所需的证书服务器,但是它仍然失败。

有很多症状相似但原因不同的问题。经过调查,我找到了解决方案,但我没有发现任何关于这个确切问题的信息,所以我将 post 一个答案,希望能为其他人节省一些时间!

事实证明,Azure DevOps 构建代理使用的 Node.js 版本未使用 Windows 证书存储。

所需的解决方案是将 Node.js 指向 self-signed SSL 证书的根 CA 证书的导出副本(*.cer 文件),使用名为 NODE_EXTRA_CA_CERTS 或使用名为 NODE.EXTRA.CA.CERTS 的任务变量,其值指向证书。

Developer Community Issue Link

我使用带有以下脚本的 PowerShell 代理作业。 有效地 为管道提供 Node.JS 的 "Use the Windows Machine Certificate Store" 选项。

一些注意事项:

  • 使用 ProcMon 监控 node.exe 表明每次管道 运行 时都会读取 NODE_EXTRA_CA_CERTS 中引用的文件。但是,其他人建议 运行ning Restart-Service vstsagent* -Force 才能获得更改。这不是我的经验,但环境之间的差异可能会导致此行为。

  • 这增加了大约 1 秒的管道执行时间。 "set and forget certificate management for Node in Pipelines on Windows" 可能是一个可以接受的价格,但仍然值得注意。

# 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 的 PowerShell 脚本。下面的脚本可以在构建代理本身上执行。它接受目标路径的参数并在服务器上设置环境变量。

感谢@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")