系列目錄
引子
上一節“分散式架構-可靠通訊-零信任網路”裡,我們探討了與微服務運作特點相適應的零信任安全模型。本節,我們將從實踐和編碼的角度出發,介紹在前微服務時代(以 Spring Cloud 為例)和雲原生時代(以 Istio over Kubernetes 為例)分別是如何實現安全傳輸、認證和授權的,透過這兩者的對比,探討在微服務架構下,應如何將業界的安全技術標準引入並實際落地,實現零信任網路下安全的服務訪問。
注:本節內容快速看完即可。關注核心思路即可。主要看雲原生Istio為例,是如何實現服務安全校驗的。
一、建立信任
零信任網路裡不存在預設的信任關係,一切服務呼叫、資源訪問成功與否,均需以呼叫者與提供者間已建立的信任關係為前提。目前通用方案就是使用PKI:公開金鑰基礎設施(Public Key Infrastructure),基本結構如下圖:
PKI基本結構由證照認證機構(certificate authority, CA)、證照持有者(certificate holder)、依賴方(relying party)三方構成:
- 1.CA是一個獨立的可信第三方,為證照持有者簽發數字證照,數字證照中宣告瞭證照持有者的身份和公鑰。CA在簽發證照前應對證照持有者的身份資訊進行核實驗證,並根據其核驗結果為其簽發證照。
- 2.證照持有者向CA申請數字證照,並向CA提供必要的資訊以證明其身份及能力,獲得由CA簽發的證照;證照持有者在與依賴方進行互動時,需向依賴方提供由CA簽發的數字證照證明其有效身份。
- 3.依賴方是證照的驗證方,依賴方與證照持有者進行互動(如建立通訊連線)時,需獲取證照持有者的數字證照,驗證數字證照的真實性和有效性。依賴方可以指定其信任的CA列表,若證照持有者提供的數字證照不是受信CA簽發的數字證照,依賴方將不認可該證照所宣告的資訊。
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)。
2.1 服務認證
2.1.1 雲原生架構Istio-服務認證
如果每一個服務提供者、呼叫者均受 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)。寬容模式為普通微服務向服務網格遷移提供了良好的靈活性。
2.1.2 Spring Cloud非雲原生-服務認證
舉例使用OAtuh2 協議的客戶端模式。
- 客戶端:提前配置好客戶端金鑰,客戶端呼叫服務時,會先使用該金鑰向認證伺服器申請到 JWT 令牌,然後透過令牌證明自己的身份,最後訪問服務。
/** * 客戶端列表 */ 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}) );
- 每一個對外提供服務的服務端,都扮演著 OAuth 2 中的資源伺服器的角色,它們均宣告為要求提供客戶端模式的憑證,如以下程式碼所示。客戶端要呼叫受保護的服務,就必須先出示能證明呼叫者身份的 JWT 令牌,否則就會遭到拒絕,這個操作本質上是授權,但是在授權過程中已實現了服務的身份認證。
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() { return new ClientCredentialsResourceDetails(); }
由於每一個微服務都同時具有服務端和客戶端兩種身份,既消費其他服務,也提供服務供別人消費,所以這些程式碼在每個微服務中都應有包含(放在公共 infrastructure 工程裡)。Spring Security 提供的過濾器會自動攔截請求、驅動認證、授權檢查的執行,申請和驗證 JWT 令牌等操作無論在開發期對程式設計師,還是執行期對使用者都能做到相對透明。儘管如此,以上做法仍然是一種應用層面的、不加密傳輸的解決方案。這種方案不適用於零信任安全模型,只能在預設內網節點間具備信任關係的邊界安全模型上才能良好工作。
2.2 使用者認證
2.2.1 雲原生架構Istio-使用者認證
當來自終端使用者的請求進入服務網格時,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: "icyfenix@gmail.com" # 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
2.2.2 Spring Cloud非雲原生-使用者認證
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 )。舉個具體例子,如果我們準備把一部分微服務視為私有服務,限制它只接受來自叢集內部其他服務的請求,另外一部分微服務視為公共服務,允許它可接受來自叢集外部的終端使用者發出的請求;
2.3.1雲原生架構Istio-授權
透過以下配置,限制了來自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 這種在基礎設施上實現授權方案顯然就要更具優勢。
2.3.1非雲原生架構SpringCloud-授權
常見的 Spring Security 授權方法有兩種,
- 1.程式碼配置:一種是使用它的
ExpressionUrlAuthorizationConfigurer
,即類似如下編碼所示的寫法來進行集中配置,也是 Spring Security 資料中都有介紹的最主流方式,適合對批次端點進行控制。
http.authorizeRequests() .antMatchers("/restful/accounts/**").hasScope(Scope.BROWSER) .antMatchers("/restful/pay/**").hasScope(Scope.SERVICE)
- 2.註解標識:Spring 的全域性方法級安全(Global Method Security)以及JSR 250的
@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); }