系列目錄
上一節「分散式架構-可靠通訊-零信任網路」裡,我們探討了與微服務運作特點相適應的零信任安全模型。本節,我們將從實踐和編碼的角度出發,介紹在前微服務時代(以 Spring Cloud 為例)和雲原生時代(以 Istio over Kubernetes 為例)分別是如何實現安全傳輸、認證和授權的,通過這兩者的對比,探討在微服務架構下,應如何將業界的安全技術標準引入並實際落地,實現零信任網路下安全的服務存取。
注:本節內容快速看完即可。關注核心思路即可。主要看雲原生Istio為例,是如何實現服務安全校驗的。
零信任網路里不存在預設的信任關係,一切服務呼叫、資源存取成功與否,均需以呼叫者與提供者間已建立的信任關係為前提。目前通用方案就是使用PKI:公開金鑰基礎設施(Public Key Infrastructure),基本結構如下圖:
PKI基本結構由證書認證機構(certificate authority, CA)、證書持有者(certificate holder)、依賴方(relying party)三方構成:
PKI 是構建傳輸安全層(Transport Layer Security,TLS)的必要基礎。在任何網路設施都不可信任的假設前提下,無論是 DNS 伺服器、代理伺服器、負載均衡器還是路由器,傳輸路徑上的每一個節點都有可能監聽或者篡改通訊雙方傳輸的資訊。要保證通訊過程不受到中間人攻擊的威脅,啟用 TLS 對傳輸通道本身進行加密,讓傳送者發出的內容只有接受者可以解密是唯一具備可行性的方案。除了隨服務節點動態擴縮而來的運維壓力外,比起公眾網際網路中主流單向的 TLS 認證,在零信任網路中,往往要啟用雙向 TLS 認證(Mutual TLS Authentication,常簡寫為 mTLS),即不僅要確認伺服器端的身份,還需要確認呼叫者的身份。
單向 TLS 認證:只需要伺服器端提供證書,使用者端通過伺服器端證書驗證伺服器的身份,但伺服器並不驗證使用者端的身份。單向 TLS 用於公開的服務,即任何使用者端都被允許連線到服務進行存取,它保護使用者端免遭冒牌伺服器的欺騙。
雙向 TLS 認證:使用者端、伺服器端雙方都要提供證書,雙方各自通過對方提供的證書來驗證對方的身份。雙向 TLS 用於私密的服務,即服務只允許特定身份的使用者端存取,它除了保護使用者端不連線到冒牌伺服器外,也保護伺服器端不遭到非法使用者的越權存取。
對於以上提到的圍繞 TLS 而展開的金鑰生成、證書分發、簽名請求(Certificate Signing Request,CSR)、更新輪換等是一套操作起來非常繁瑣的流程,稍有疏忽就會產生安全漏洞,所以儘管理論上可行,但實踐中如果沒有自動化的基礎設施的支援,僅靠應用程式和運維人員的努力,是很難成功實施零信任安全模型的。下面我們聚焦於「認證」和「授權」兩個最基本的安全需求,看它們在微服務架構下,有或者沒有基礎設施支援時,各是如何實現的。
根據認證的目標物件可以把認證分為兩種型別:
服務認證:以機器作為認證物件,即存取服務的流量來源是另外一個服務,稱為服務認證(Peer Authentication,直譯過來是「節點認證」)。
請求認證:以人類作為認證物件,即存取服務的流量來自於終端使用者,稱為請求認證(Request Authentication)。
如果每一個服務提供者、呼叫者均受 Istio 管理,那 mTLS 就是最理想的認證方案。你只需要參考以下簡單的 PeerAuthentication CRD設定,即可對某個Kubernetes 名稱空間範圍內所有的流量均啟用 mTLS:
apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: authentication-mtls namespace: bookstore-servicemesh spec: mtls: mode: STRICT
如果你的分散式系統還沒有達到完全雲原生的程度,其中仍存在部分不受 Istio 管理(即未注入 Sidecar)的伺服器端或者使用者端(這是頗為常見的),你也可以將 mTLS 傳輸宣告為「寬容模式」(Permissive Mode)。寬容模式為普通微服務向服務網格遷移提供了良好的靈活性。
舉例使用OAtuh2 協定的使用者端模式。
/** * 使用者端列表 */ private static final List<Client> clients = Arrays.asList( new Client("bookstore_frontend", "bookstore_secret", new String[]{GrantType.PASSWORD, GrantType.REFRESH_TOKEN}, new String[]{Scope.BROWSER}), // 微服務一共有Security微服務、Account微服務、Warehouse微服務、Payment微服務四個使用者端 // 如果正式使用,這部分資訊應該做成可以設定的,以便快速增加微服務的型別。clientSecret也不應該出現在原始碼中,應由外部設定傳入 new Client("account", "account_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}), new Client("warehouse", "warehouse_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}), new Client("payment", "payment_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}), new Client("security", "security_secret", new String[]{GrantType.CLIENT_CREDENTIALS}, new String[]{Scope.SERVICE}) );
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() { return new ClientCredentialsResourceDetails(); }
由於每一個微服務都同時具有伺服器端和使用者端兩種身份,既消費其他服務,也提供服務供別人消費,所以這些程式碼在每個微服務中都應有包含(放在公共 infrastructure 工程裡)。Spring Security 提供的過濾器會自動攔截請求、驅動認證、授權檢查的執行,申請和驗證 JWT 令牌等操作無論在開發期對程式設計師,還是執行期對使用者都能做到相對透明。儘管如此,以上做法仍然是一種應用層面的、不加密傳輸的解決方案。這種方案不適用於零信任安全模型,只能在預設內網節點間具備信任關係的邊界安全模型上才能良好工作。
當來自終端使用者的請求進入服務網格時,Istio 會自動根據設定中的JWKS(JSON Web Key Set)來驗證令牌的合法性,如果令牌沒有被篡改過且在有效期內,就信任 Payload 中的使用者身份,並從令牌的 Iss 欄位中獲得 Principal。以下是 Istio 使用者認證設定,其中jwks
欄位配的就是 JWKS 全文(實際生產中並不推薦這樣做,應該使用jwksUri
來設定一個 JWKS 地址,以方便金鑰輪換),根據這裡設定的金鑰資訊,Istio 就能夠驗證請求中附帶的 JWT 是否合法。
apiVersion: security.istio.io/v1beta1 kind: RequestAuthentication metadata: name: authentication-jwt-token namespace: bookstore-servicemesh spec: jwtRules: - issuer: "[email protected]" # Envoy預設只認「Bearer」作為JWT字首,之前其他地方用的都是小寫,這裡專門相容一下 fromHeaders: - name: Authorization prefix: "bearer " # 在rsa-key目錄下放了用來生成這個JWKS的證書,最初是用java keytool生成的jks格式,一般轉jwks都是用pkcs12或者pem格式,為方便使用也一起附帶了 jwks: | { "keys": [ { "e": "AQAB", "kid": "bookstore-jwt-kid", "kty": "RSA", "n": "i-htQPOTvNMccJjOkCAzd3YlqBElURzkaeRLDoJYskyU59JdGO-p_q4JEH0DZOM2BbonGI4lIHFkiZLO4IBBZ5j2P7U6QYURt6-AyjS6RGw9v_wFdIRlyBI9D3EO7u8rCA4RktBLPavfEc5BwYX2Vb9wX6N63tV48cP1CoGU0GtIq9HTqbEQs5KVmme5n4XOuzxQ6B2AGaPBJgdq_K0ZWDkXiqPz6921X3oiNYPCQ22bvFxb4yFX8ZfbxeYc-1rN7PaUsK009qOx-qRenHpWgPVfagMbNYkm0TOHNOWXqukxE-soCDI_Nc--1khWCmQ9E2B82ap7IXsVBAnBIaV9WQ" } ] } forwardOriginalToken: true
Spring Cloud,採用 JWT 令牌+在Spring Security 的過濾器實現。Spring Security 已經做好了認證所需的絕大部分的工作,真正要開發者去編寫的程式碼是令牌的具體實現,即程式碼中名為RSA256PublicJWTAccessToken
的實現類。它的作用是載入 Resource 目錄下的公鑰證書public.cert
(實在是怕「抄作業不改名字」的行為,筆者再一次強調不要將密碼、金鑰、證書這類敏感資訊打包到程式中,範例程式碼只是為了演示,實際生產應該由運維人員管理金鑰),驗證請求中的 JWT 令牌是否合法。
@Named public class RSA256PublicJWTAccessToken extends JWTAccessToken { RSA256PublicJWTAccessToken(UserDetailsService userDetailsService) throws IOException { super(userDetailsService); Resource resource = new ClassPathResource("public.cert"); String publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); setVerifierKey(publicKey); } }
如果 JWT 令牌合法,Spring Security 的過濾器就會放行呼叫請求,並從令牌中提取出 Principals,放到自己的安全上下文中。(即SecurityContextHolder.getContext()
)。
經過認證之後,合法的呼叫者就有了可信任的身份,此時就已經不再需要區分呼叫者到底是機器(服務)還是人類(終端使用者)了,只根據其身份角色來進行許可權存取控制,即我們常說的 RBAC(Role-Based Access Control )。舉個具體例子,如果我們準備把一部分微服務視為私有服務,限制它只接受來自叢集內部其他服務的請求,另外一部分微服務視為公共服務,允許它可接受來自叢集外部的終端使用者發出的請求;
通過以下設定,限制了來自bookstore-servicemesh
名空間的內部流量只允許存取accounts
、products
、pay
和settlements
四個端點的 GET、POST、PUT、PATCH 方法,而對於來自istio-system
名空間(Istio Ingress Gateway 所在的名空間)的外部流量就不作限制,直接放行。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: authorization-peer namespace: bookstore-servicemesh spec: action: ALLOW rules: - from: - source: namespaces: ["bookstore-servicemesh"] to: - operation: paths: - /restful/accounts/* - /restful/products* - /restful/pay/* - /restful/settlements* methods: ["GET","POST","PUT","PATCH"] - from: - source: namespaces: ["istio-system"]
Istio 已經提供了比較完善的目標匹配工具,如上面設定中用到的源from
、目標to
,還有未用到的條件匹配when
,以及其他如萬用字元、IP、埠、名空間、JWT 欄位,等等。要說靈活和功能強大,肯定還是不可能跟在應用中由程式碼實現的授權相媲美,但對絕大多數場景已經夠用了。在便捷性、安全性、無侵入、統一管理等方面,Istio 這種在基礎設施上實現授權方案顯然就要更具優勢。
常見的 Spring Security 授權方法有兩種,
ExpressionUrlAuthorizationConfigurer
,即類似如下編碼所示的寫法來進行集中設定,也是 Spring Security 資料中都有介紹的最主流方式,適合對批次端點進行控制。http.authorizeRequests() .antMatchers("/restful/accounts/**").hasScope(Scope.BROWSER) .antMatchers("/restful/pay/**").hasScope(Scope.SERVICE)
@RolesAllowed
註解來做授權控制。這種寫法對程式碼的侵入性更強,要以註解的形式分散寫到每個服務甚至是每個方法中,但好處是能以更方便的形式做出更加精細的控制效果。譬如要控制服務中某個方法只允許來自服務或者來自瀏覽器的呼叫,那直接在該方法上標註@PreAuthorize
註解即可,還支援SpEL 表示式來做條件。表示式中用到的SERVICE
、BROWSER
代表授權範圍,就是在宣告使用者端列表時傳入的,具體可參見開頭宣告使用者端列表的程式碼清單。/** * 根據使用者名稱稱獲取使用者詳情 */ @GET @Path("/{username}") @Cacheable(key = "#username") @PreAuthorize("#oauth2.hasAnyScope('SERVICE','BROWSER')") public Account getUser(@PathParam("username") String username) { return service.findAccountByUsername(username); }
如果你覺得本文對你有點幫助的話,記得在右下角點個「推薦」哦,博主在此感謝!