﻿# 在高度網路管制的企業內部如何設定 Git 連接 Azure Repos 上的儲存庫

我每年都會有好幾場 [Azure DevOps](https://dev.azure.com/) 的企業內訓，最近的一場是在一家金融業的企業，他們的網路管制非常嚴格，但是有特別開放 Azure DevOps Services 的雲端服務，所以基本上連線是正常的，唯獨 Git 連接 Azure Repos 上的儲存庫時，就會遇到連線問題。今天這篇文章我就來說說解決方案。

![Secure Git connection in a highly restricted corporate network](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/9655661c06e81dddfa08-4b17b241-b66c-4da3-9e71-a5101e320cc4.jpg)

## 問題描述

這個問題主要出在金融業對於內部上網的管制，他們的網路安全設定非常嚴格，所以連接網站時的 TLS/SSL 憑證，並不是 Azure DevOps Services 官方網站的憑證，而是公司內部自簽的憑證。為了解決 TLS 連線信任問題，一般的解法通常都是透過 AD 的**群組原則**(Group Policy)自動安裝公司核准的根憑證，因此使用者不會感覺到憑證被換過。

不過，這招對 [Git for Windows](https://gitforwindows.org/) 是行不通的，因為 [Git for Windows](https://gitforwindows.org/) 擁有獨立的根憑證儲存區，並沒有共用 Windows 的憑證儲存區，所以即使你在 Windows 上安裝了公司的根憑證，Git for Windows 也無法正確的連接 Azure Repos 上的儲存庫。

## 解決方案

解決方案其實很簡單，就是讓 [Git for Windows](https://gitforwindows.org/) 信任公司自簽的憑證，這樣就可以正確的連接 Azure Repos 上的儲存庫了。

為了要能信任公司自簽的憑證，Git 可以透過設定 `http.sslBackend` 組態設定來選擇不同的受信任根憑證來源：

1.  使用獨立的根憑證檔案來驗證 SSL/TLS 連線 (預設值)
    
    ```bash
    git config --global http.sslBackend openssl
    ```
    
    這個部分還需要搭配 `http.sslCAInfo` 組態設定，指定根憑證檔案的路徑，例如：
    
    ```powershell
    git config --global http.sslCAInfo "$env:USERPROFILE\ca-bundle.crt"
    ```
    
2.  使用 Windows 的內建網絡層 Secure Channel (SChannel) 來處理 HTTPS 連接
    
    如果企業有透過 AD 的**群組原則**(Group Policy)自動安裝公司核准的根憑證，這個選項就非常適合，因為 Git for Windows 會自動使用 Windows 的憑證儲存區來驗證 SSL/TLS 連線。
    
    ```bash
    git config --global http.sslBackend schannel
    ```
    
    > 這個設定應該是最簡單的方法，但前提就是你已經有安裝好受信任的根憑證到 Windows 的憑證儲存區。
    

若你是全新安裝 Git for Windows 的話，安裝過程會看到 `Choosing HTTPS transport backend` 這個步驟，你也可以在這邊選擇 `Use the native Windows Secure Channel library` 自動完成上述設定，不用安裝好之後才打指令變更設定！

![Choosing HTTPS transport backend](https://stwillblogassets.blob.core.windows.net/files/images/external/github.com/b41867fb931b616dbabe-9075bc52-a818-429d-92cc-6e4747e9def6.jpg)

若要使用獨立的根憑證檔案來驗證 SSL/TLS 連線，以下是完整的設定步驟：

1.  先取得 Azure DevOps Services 的網站憑證
    
    我以 `https://dev.azure.com/orgName/` 為例，你必須取得 `dev.azure.com` 的憑證，取得的方法有很多種，詳細作法請見下一個段落。
    
    不過，以企業內部網路環境來說，最正確的作法，應該是取得公司核發的根憑證，這樣才能保證所有公司發出的憑證你都能夠正確的連接。
    
2.  複製 Git for Windows 的**根憑證定義檔**到你的**家目錄** (Home Directory)
    
    Git 的**根憑證定義檔**可以透過以下命令查到，通常檔名為 `ca-bundle.crt`：
    
    ```powershell
    git config --system http.sslCAInfo
    ```
    
    先將該檔案複製到你的家目錄，通常是 `C:\Users\YourName\`，以下是我常用的 PowerShell 命令：
    
    ```powershell
    $CABundlePath=$(git config --system http.sslCAInfo)
    Copy-Item -Path $CABundlePath -Destination "$env:USERPROFILE\ca-bundle.crt"
    ```
    
3.  將公司自簽憑證加入到 `ca-bundle.crt` 檔案中
    
    該檔案是一個文字檔，且斷行符號為 `LF` ( `\n` ) 喔，寫入時需要特別注意一下！
    
    你可以使用以下 PowerShell 指令將公司自簽憑證加入到 `ca-bundle.crt` 檔案中：
    
    ```powershell
    Get-Content C:\YourCert.pem | Out-File -Append -FilePath "$env:USERPROFILE\ca-bundle.crt" -Encoding ascii
    ```
    
    > 請將 `C:\YourCert.pem` 改成你的公司自簽憑證的檔案路徑，通常是 `.pem` 或 `.crt` 副檔名，且為「文字格式」。
    
4.  設定 Git 的 `http.sslCAInfo` 設定，指向家目錄的 `ca-bundle.crt` 檔案
    
    若使用 PowerShell 的命令如下：
    
    ```powershell
    git config --global http.sslCAInfo "$env:USERPROFILE\ca-bundle.crt"
    ```
    

只要上面 4 個步驟，就可以設定好了！

無論是透過 Git 命令列工具，或是透過 Visual Studio 2022 的 Git 工具，都可以正確的連接 Azure Repos 上的儲存庫了！

### 取得 Azure DevOps Services 的網站憑證

以下是我提供兩個簡單的方法取得 Azure DevOps Services 的網站憑證：

1.  透過 `Linux` 或 `WSL` 可以執行以下命令
    
    ```bash
    echo | openssl s_client -showcerts -servername dev.azure.com -connect dev.azure.com:443 2>/dev/null | openssl x509 -outform PEM > dev.azure.com.pem
    ```
    
2.  透過 `PowerShell` 可以執行以下命令
    
    ```powershell
    $domain = "dev.azure.com"
    $cert = [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
    $tcpClient = New-Object System.Net.Sockets.TcpClient($domain, 443)
    $sslStream = New-Object System.Net.Security.SslStream($tcpClient.GetStream(), $false, { $true }, $null)
    $sslStream.AuthenticateAsClient($domain)
    $cert = $sslStream.RemoteCertificate
    $certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
    $pem = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($certBytes, "InsertLineBreaks") + "`n-----END CERTIFICATE-----"
    $certPath = "dev.azure.com.pem"
    Set-Content -Path $certPath -Value $pem
    $sslStream.Close()
    $tcpClient.Close()
    ```
    

上述兩個命令都會產生一個 `dev.azure.com.pem` 檔案，內容稍有不同，但其實是同一張憑證。

我有發現 PowerShell 使用到 [\[System.Convert\]::ToBase64String](https://learn.microsoft.com/en-us/dotnet/api/system.convert.tobase64string) 方法，這裡設定的 `InsertLineBreaks` 選項，會產生每行最寬 76 字元的 Base64 編碼文字。然而 `openssl x509 -outform PEM` 則會產生每行最寬 64 字元的 Base64 編碼文字。只有這點差別而已，兩個命令產生的憑證檔其實是一樣的。

### 最後我整理最無腦的 PowerShell 設定腳本

理論上只要修改第一行的變數，你就可以在第一次設定的時候自動設定到好：

```powershell
$domain = "dev.azure.com"

$cert = [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$tcpClient = New-Object System.Net.Sockets.TcpClient($domain, 443)
$sslStream = New-Object System.Net.Security.SslStream($tcpClient.GetStream(), $false, { $true }, $null)
$sslStream.AuthenticateAsClient($domain)
$cert = $sslStream.RemoteCertificate
$certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
$pem = "-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($certBytes, "InsertLineBreaks") + "`n-----END CERTIFICATE-----"
$certPath = "server.pem"
Set-Content -Path $certPath -Value $pem
$sslStream.Close()
$tcpClient.Close()

$CABundlePath = $(git config --system http.sslCAInfo)
Copy-Item -Path $CABundlePath -Destination "$env:USERPROFILE\ca-bundle.crt"

Get-Content $certPath | Out-File -Append -FilePath "$env:USERPROFILE\ca-bundle.crt" -Encoding ascii
Remove-Item -LiteralPath $certPath

git config --global http.sslCAInfo "$env:USERPROFILE\ca-bundle.crt"
```

清除上述設定的 PowerShell 命令如下：

```powershell
git config --global --unset http.sslCAInfo
Remove-Item -LiteralPath "$env:USERPROFILE\ca-bundle.crt"
```

### 相關連結

-   [Convert.ToBase64String Method (System) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.convert.tobase64string?WT.mc_id=DT-MVP-4015686)
-   [git - Adding self-signed SSL certificate without disabling authority-signed ones - Stack Overflow](https://stackoverflow.com/q/23807313/910074)
