問題描述
Viper (本文環境是Viper 1.1.0)是Go應用程序的完整配置解決方案,在很多項目中都有應用。etcd是一個分佈式KV存儲,最直接的應用是配置中心。
Viper除了支持從文件中讀取配置,還支持從遠程的配置中心讀取配置,使用下面的代碼進行配置。
viper.AddRemoteProvider("etcd",
"http://127.0.0.1:2379",
"conf.toml")
viper.SetConfigType("toml")
err := viper.ReadRemoteConfig()
if err != nil {
panic(err)
}
運行後報錯panic: Remote Configurations Error: No Files Found
,檢查後發現etcd開啓了tls,所以需要用https協議訪問etcd的API,更新代碼如下。
viper.AddSecureRemoteProvider("etcd",
"https://127.0.0.1:2379",
"conf.toml",
"key_path")
viper.SetConfigType("toml")
err := viper.ReadRemoteConfig()
if err != nil {
panic(err)
}
使用AddSecureRemoteProvider
方法替換AddRemoteProvider
方法,問題依舊。
定位問題
跟蹤源碼發現,最終像etcd發送請求的是go-etcd包(目前go-etcd已經不維護),在go-etcd的requests.go文件中找到了相關的源碼,go-etcd調用了net/http包向etcd發送請求。
這個時候忽然想到etcd的證書是自簽名的,訪問自簽名證書的https接口應該會報錯啊,怎麼會請求到內容呢?如下圖,在Chrome中訪問etcd的自簽名https接口,會提示證書無效。
我們自己使用go實現一段請求etcd https接口的代碼,看看到底是什麼回事,代碼如下。
resp, err := http.Get("https://127.0.0.1:2379/v2/keys/conf.toml?quorum=false&recursive=false&sorted=false")
if err != nil {
// handle error
fmt.Println(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(body))
果然不一樣,我們的代碼會報錯x509: certificate signed by unknown authority
,因爲是自簽名證書,客戶端無法驗證證書真假,所以這個報錯是可以理解的,go-etcd代碼和我們的測試代碼表現不一致,一定是我們落下了什麼,重新梳理go-etcd源碼終於發現了原因。
在client.go文件的initHTTPSClient方法中,發現允許繞過證書驗證,這就可以解釋爲什麼證書無效也能獲取到數據了,繞過了證書的驗證環節,相當於不管證書真假都拿來用。現在可以解釋使用AddRemoteProvider
方法訪問https接口爲什麼可以獲取到內容,但是無法解釋AddSecureRemoteProvider
方法爲什麼無法從https接口獲取內容,因爲兩個方法在發送http請求階段的代碼是一致的,都忽略了證書驗證。
查看AddSecureRemoteProvider
的註釋,發現了原因。
原來...AddSecureRemoteProvider
這個Secure指的並不是使用安全鏈接https,而是在請求到內容後加了一個解密的步驟(Secure指請求的是加密過的內容,而不是使用加密鏈接請求),最後一個參數接收的也並不是客戶端證書,而是解密的gpg key... 根據viper的文檔,這個gpg key是可選的,我們這個例子中,如果給gpg key傳入一個空字符串,也是可以正常執行的...
必須吐槽一下viper的命名,哪裏是AddSecureRemoteProvider
,明明應該叫AddEncryptedRemoteProvider
總結
出現這個問題,主要是誤會了AddSecureRemoteProvider
接口表達的意思,並且go-etcd允許忽略證書驗證,也讓問題變得更加離奇。
當然go-etcd的這種配置是非常合理的,內部系統使用自簽名證書是一個很正常的行爲。