每個人都在討論微服務,每個人也都希望能夠實現微服務架構,而微服務安全也日漸成為大家關注的重要問題。今天與大家分享的文章,就從應用層面深入探討了應對微服務安全挑戰的方案,為微服務安全提供了新的思路。
面向服務架構(簡稱 SOA)引入了一類設計規范,其核心思路在於采用高度解耦式服務部署,其中各項服務可通過一套標准信息格式經由網絡實現彼此通信。這套方案與具體技術無關,即不考慮各項服務具體是如何實現的。每項服務都擁有一個明確定義,用於發布服務描述或者服務接口。在實踐當中,這類信息格式通過 SOAP 實現標准化——即由 W3C 於 2000 年初推出的一項標准——同時亦基於 XML——其中服務描述由 WSDL(另一項 W3C 標准)進行標准化,而服務發現標准由 UDDI(同樣為 W3C 標准)實現。這一切正是基於 SOAP 的 Web 服務的實現基礎,甚至使得 Web 服務在一定程度上成了 SOA 的代名詞。不過這種實現方式在架構模式層面也有著自己的缺陷。SOA 的基本原則正被時代所逐步淘汰,如今由 OASIS 提供的 WS-* 堆棧(包括 WS-Security, WS-Policy, WS-Security Policy,WS-Trust, WS-Federation, WS-Secure Conversation, WS-Reliable Messaging, WS-Atomic Transactions, WS-BPEL 等等)令 SOA 的復雜性不斷提高,這也直接導致很多普通開發者發現自己很難對其加以駕馭。
多年之後,如今我們得以再次開啟這段通往 SOA 基本原則的旅途——但這一次它有了新的名號,即微服務(microservice)。微服務能夠為應用程序設計提供一種更具針對性、范圍性與模塊性的實現方案。
微服務可謂當下一大熱門詞匯之一,與之並駕齊驅的則包括物聯網、容器化與區塊鏈。“微服務”一詞最初於 2011 年 5 月亮相於威尼斯軟件架構師研討會。這個詞匯用於解釋一類常見的架構類型。
大家已經意識到微服務並不僅僅是做對了的 SOA,它也不只是一種架構模式——而是一種圍繞架構模式展開的全新文化。其由主要目標作為驅動力,旨在實現快速部署與快速生產。
在保護微服務安全時,需要從以下幾個角度入手:
保護開發生命周期與測試自動化機制:微服務背後的核心驅動力在於提升投付生產的速度。我們需要向服務當中引入變更,加以測試而後立即將成果部署至生產環境。為了確保在代碼層面中不存在安全漏洞,我們需要制定規劃以進行靜態代碼分析與動態測試——更重要的是,這些測試應當成為持續交付流程的組成部分。任何安全漏洞都需要在早期開發周期內被發現,另外反饋周期也必須盡可能得到縮短。
DevOps 安全:微服務部署模式可謂多種多樣——但其中使用最為廣泛的當數每主機服務模式。其中的主機指定的並不一定是物理設備——也很可能屬於容器(Docker)。我們需要對容器層面的安全進行關注。我們該如何確保各容器之間得到有效隔離,又該在容器與主機操作系統之間采取怎樣的隔離水平?
應用級別安全:我們該如何驗證用戶身份並對其微服務訪問操作進行控制,又要怎樣保障不同微服務之間的通信安全?
在今天的文章中,我們將提供一整套安全模式,旨在解決應用層級所面臨的各類微服務安全保護挑戰。
在 Java EE 環境下,攔截器可以由 servlet 過濾器充當。該 servlet 過濾器會攔截全部來自其已注冊上下文的請求,並強制進行驗證。該服務調用要麼攜帶有效的憑證,要麼擁有能夠映射至某個用戶的會話令牌。一旦 servlet 過濾器找到該用戶,則會創建登錄上下文,並將其傳遞給下游組件。每個下游組件都能夠從該登錄上下文內識別出用戶以完成授權。
在微服務環境下,安全性往往成為最大的挑戰。在微服務架構當中,各服務分布及部署在分布式設置當中的多套容器之內。各服務接口不再存在於本地,而是通過 HTTP 進行遠程接入。以下示意圖顯示了不同微服務之間的交互方式。
這裡的挑戰在於,我們要如何驗證用戶並在不同微服務之間以對稱方式完成登錄上下文傳遞,隨後還要想辦法讓微服務完成對用戶的授權。
JWT(即 JSON Web 令牌)負責定義一套容器,旨在完成各方之間的數據傳輸。其可用於:
在各方之間傳播其中一方的身份。
在各方之間傳播用戶權利。
通過非安全通道在各方之間安全實現數據傳輸。
根據JWT受信指標判斷用戶身份。
已簽名 JWT 被稱為 JWS(即 JSON Web 簽名),而加密 JWT 則被稱為 JWE(即 JSON Web 加密)。事實上,JWT 並不會以自身原始方式存在——其要麼作為 JWS,要麼作為 JWE,它像是一種抽象類——JWS 與 JWE 為其具體實現方式。
來自某一微服務並將被傳遞至另一微服務的用戶上下文可伴隨 JWS 一同傳遞。由於 JWS 由上游微服務的某一已知密鑰進行簽名,因此 JWS 會同時包含有最終用戶身份(在 JWT 中聲明)以及上游微服務身份(通過簽名實現)。為了接收 JWS,下游微服務首先需要根據 JWS 本身中的嵌入公鑰對 JWS 的簽名進行驗證。這還不夠,我們還需要檢查該密鑰是否受信。不同微服務之間可通過多種方式建立受信關系。其一為由服務為各服務配置受信證書。很明顯,這種方式在規模化微服務部署環境中並不可行。因此我建議大家建立一套專有證書中心(簡稱 CA),同時可以為不同微服務組設置中介證書中心。現在,相較於互相信任及各自分配不同的證書,下游微服務將只需要信任根證書授權或者中介機制即可。這能夠顯著降低證書配置所帶來的管理負擔。
而 aud 參數同樣存在於 JWT 聲明集內,負責指定令牌的目標受眾。其可以是單個接收者或者是一組接收者。在執行任何驗證檢查之前,該令牌接收者都必須首先查看是否發布了特定 JWT 供其使用,如果沒有則立即拒絕。令牌發送方需要在發出令牌之前,確定該令牌實際接收者的身份,同時 aud 參數值必須屬於令牌發送方與接收方間預先約定的值。在微服務環境中,我們可以利用正規表達式來驗證令牌受眾。舉例來說,令牌中的 aud 值可以為*.facilelogin.com,意味著 facilelogin.com 域名下的每個接收方(例如foo.facilelogin.com、bar.facilelogin.com 等)都能夠擁有自己的 aud 值。
CRL (證書吊銷列表 / RFC 2459)
OCSP (在線證書狀態協議 / RFC 2560)
OCSP Stapling (RFC 6066)
OCSP Stapling Required (尚處於草案階段)
CRL 的使用頻率並不高。客戶端在發起 TLS 握手時,必須從對應的證書頒發中心處獲取一份長長的吊銷證書列表,而後檢查服務器證書是否被列入該列表。相較於每一次進行列表獲取,客戶端可以在本地對 CRL 進行緩存。在此之後,大家還需要考慮如何避免以陳舊數據為基礎做出判斷的問題。當 TLS 相互驗證機制被使用時,服務器也需要針對客戶端進行同樣的證書驗證。最終,人們發現 CRL 的實際效果其實並不理想,因此新的解決方案也應運而生——這就是 OCSP。
在 OCSP 當中,一切元素的實際效果都要比 CRL 好上那麼一點。TLS 客戶端能夠檢查特定證書的狀態,且無需從證書中心處下載完整的吊銷證書列表。換句話來說,當客戶端每次與新的下游微服務進行通信時,其都必須同對應的 OCSP 響應方溝通以驗證當前服務器(或者服務)的證書狀態——而服務器則必須面向客戶端證書執行同樣的操作。如此一來,OCSP 響應方同樣面臨著巨大的流量壓力。基於同樣的考慮,客戶端仍然可以對 OCSP 決策進行緩存,但這無疑繼續帶來同樣的、基於陳舊數據進行決策的可能性。
而 OCSP stapling 的出現令客戶端不再需要每次同下游微服務進行通信時,都與 OCSP 響應方“打招呼”。該下游微服務將從對應的 OCSP 響應方處獲取 OCSP 響應,以及 staple,或者將響應附加到證書本身當中。由於 OCSP 響應得到了對應證書中心的簽名,因此該客戶端能夠驗證通過其簽名並接收此響應。這種方法令事情有了轉機,事實上如今是由服務而非客戶端與 OCSP 響應方進行通信。不過在 TLS 相互驗證模式下,OCSP stapling 相較於原始 OCSP 無法帶來任何額外優勢。
由於 OCSP 必須配合 stapling,該服務(即下游服務)需要向客戶端(即上游服務)提供保證,證明 OCSP 響應被附加到了該服務在 TLS 握手時接收到的證書中。如果 OCSP 響應未被附加至該證書中,那麼結果並非出現軟錯誤,而是客戶端必須立即拒絕該連接。
臨時證書帶來的最大挑戰在於其部署與維護工作。自動則正是解決這些難題的靈丹妙藥。Netflix 公司建議使用分層方案以構建臨時證書部署機制。大家可以在 TPM(即受信平台模塊)或者 SGX(軟件保護擴展)當中獲得系統身份或者長期證書,從而顯著提升安全性。在此之後,再使用這些憑證作為臨時證書。最後,在微服務中使用臨時證書——這些證書亦可由其它微服務使用。每項微服務都能夠利用自身長期證書對臨時證書進行定期刷新。當然,僅僅擁有臨時證書還不夠——托管該服務(或者 TLS 終止器)的主機應當支持對服務器證書的動態更新。目前存在大量能夠運行服務器證書動態重載的 TLS 終止器,但其中大多數可能會導致短暫的服務停機。
最終用戶對微服務的訪問(通過 API 實現)應當在邊界或者 API 網關處進行驗證。目前最為常見的 API 安全保護模式為 OAuth 2.0。
要想通過 API 網關訪問某項微服務,請求發起方必須首先獲得有效的 OAuth 令牌。系統能夠以自身角色訪問微服務,也可以作為其他用戶實現訪問。對於後一種情況,假設用戶登錄至某 Web 應用,那麼此後該 Web 應用即可以所登錄用戶的身份進行微服務訪問。
下面來看端到端通信的具體實現方式,如上圖所顯示:
用戶通過 Identity Provider 登錄至 Web 應用/移動應用,而 Web 應用/移動應用則通過 OpenID Connect(也可以是 SAML 2.0)信任該 Provider。
該 Web 應用獲取一條 OAuth 2.0 access_token 與一條 id_token。其中 id_token 將驗證訪問該 Web 應用的最終用戶。如果使用 SAML 2.0,則該 Web 應用需要與其信任的 OAuth 驗證服務器的 token 端點進行通信,同時將 SAML 令牌交換為一條 OAuth acess_token,隨後交換 OAuth 2.0 的 SAML 2.0 grant type。
該 Web 應用會作為最終用戶調用一個 API——並隨同 API 請求發送 access_token。
API 網關會攔截來自該 Web 應用的請求,提取 access_token,與令牌交換端點(或者 STS)進行通信,並由後者驗證該 access_token,而後向該 API 網關提供 JWT(由其簽名)。此 JWT 還攜帶有用戶上下文。在 STS 對 acess_token 進行驗證時,其還將通過 introspection API 與對應的 OAuth 授權服務器進行通信。
API 網關向下游微服務將同時發出請求與 JWT。
每項微服務都會驗證其接收到的 JWT,而後作為下游服務調用,其能夠創建新的自簽名 JWT 並將其與該請求一同發送。在其它方案中,亦會用到嵌套 JWT——即由新的 JWT 攜帶上一 JWT。
在上述流程當中,來自外部客戶端的 API 請求將經由該 API 網關。當某項微服務與其它微服務通信時,其將不再需要經過該網關。另外,從特定微服務的角度來看,無論大家是從外部客戶端還是其它微服務處獲取請求,獲得的都是 JWT——也就是說,這是一種對稱安全模式。
上圖所示為 XACML 組件架構。首先,策略管理員需要通過 PAP(即策略管理點)定義 XACML 策略,而這些策略將被保存在策略存儲內。要檢查特定實體是否擁有訪問某種資源的權限,PEP(即策略執行點)需要攔截該訪問請求、創建一條 XACML 請求並將其發送至 XACML PDP(即策略決策點)。該 XACML 請求能夠攜帶任何有助於在 PDP 上執行決策流程的屬性。舉例來說,其能夠包含拒絕標識符、資源標識符以及特定對象將對目標資源執行的操作。需要進行用戶授權的微服務則需要與該 PDP 通信並從 JWT 中提取相關屬性,從而建立 XACML 請求。PIP(即策略信息點)會在 PDP 發現 XACML 請求中不存在策略評估所要求的特定屬性時介入。在此之後,PDP 會與 PIP 通信以找到缺失的對應屬性。PIP 能夠接入相關數據存儲,找到該屬性而後將其返回至 PDP。
性能成本:每一次被要求執行訪問控制檢查時,對應微服務都需要通過線纜與 PDP 進行通信。當該決策被緩存在客戶端時,此類傳輸成本與策略評估成本將得到有效降低。不過在使用緩存機制時,我們亦有可能根據陳舊數據進行安全決策。
策略信息點(簡稱 PIP)的所有權:每項微服務都應當擁有自己的 PIP,其了解要從哪裡引入實現訪問控制所必需的數據。在以上方案中,我們建立起的一套“整體式” PDP,其中包含全部 PIP——對應全部微服務。
如上圖所示,嵌入式 PDP 將遵循一類事件模式,其中每項微服務都會訂閱其感興趣的主題以從 PAP 處獲取合適的訪問控制策略,而後更新其內嵌 PDP。大家可以通過微服務組或者全局多租戶模型獲取 PAP。當出現新策略或者策略存在更新時,該 PAP 會向對應的主題發布事件。
這套方案不會違反微服務中的“服務器不變”原則。“服務器不變”意味著當大家在持續交付流程結尾處,直接利用加載自庫的配置構建服務器或者容器時,整個創建流程應該能夠基於同樣的配置進行不斷重復。因此,我們不希望任何用戶登入服務器並對配置做出變更。在內嵌 PDP 模式下,盡管服務器會加載對應的策略,但其仍同時處於運行當中。這意味著當我們啟動新容器時,其仍然立足於同樣的策略集。
在結束本篇文章之前,我們還有另一個重要的問題需要回答,即 API 網關在授權上下文中到底扮演著怎樣的角色。我們可以設置全局可訪問的訪問控制策略——其可用於最終用戶,並由網關進行強制執行——但無法設置服務層級的策略。因為顧名思義,服務層策略必須在服務層上執行。