Istio安全概述
作為服務網格的事實标準,極大地降低微服務架構下流量管理的複雜度往往是Istio最為引入注目的特性,但事實上,随着越來越多的服務從單體架構向微服務架構演進,子產品間由最初的函數調用轉變為程序間通信,微服務間通信的安全性,服務的通路政策控制以及如何降低大規模場景下安全配置的複雜度等問題同樣亟待解決。
當然,Istio給出了一套完整的架構用于解決這些問題,與對流量管理的處理類似,這套架構不要求對應用做任何侵入式修改,并且提供了靈活的服務通路政策以及簡易的配置方式,以近乎零成本的方式完美地解決了上述問題。

Istio安全架構
需要注意的是,本文以Istio 1.3版本作為主要分析對象,雖然随着Istio版本的演進,服務架構以及上層API都發生了一些變化,不過底層原理都是類似的,是以相信這些變化并不會造成太大的困擾。Istio的架構如下所示:
- Citadel:密鑰以及證書管理
- Mixer:鑒權以及審計
- Proxy:一般就是Envoy,接收Pilot下發的安全配置并且應用于其攔截代理的流量
- Pilot:API轉換層,将Istio上層安全相關的Kubernetes CRD轉換為底層資料面Proxy對應的安全配置
Istio安全概念、執行個體以及實作
1
Istio identity
在展開Istio安全架構中的各種概念、執行個體以及實作方法之前,有必要首先了解Istio identity這個概念。簡單地說Istio identity就是一種身份辨別,在服務間通信的開始階段,通信雙方需要交換包含各自身份辨別的證書,這樣一來,用戶端就能通過校驗證書中的身份辨別來确認這的确是它要通路的目标服務,而服務端則在認證用戶端身份的同時,還能對該使用者進行授權、審計等等更為複雜的操作。
本文以Kubernetes平台作為主要的讨論背景,而在Kubernetes下,Istio identity一般與Service Account對應,通信雙方會以各自綁定的Service Account作為身份辨別。
2
Authentication概念、執行個體及實作
對于認證,Istio提供兩種方式:Transport authentication以及Origin authentication,後者基于JWT進行認證,本文不詳細展開。
而Transport authentication允許在不對應用源碼做侵入式修改的情況下,提供服務間的雙向安全認證,同時密鑰以及證書的建立、分發、輪轉也都由系統自動完成,對使用者透明,進而大大降低了安全配置管理的複雜度。
那麼如何配置認證政策呢?對此,Istio提供了名為Policy的Kubernetes CRD。如下所示的yaml檔案展示了如何開啟對default這個namespace下名為details和reviews的service的認證,且要求對于reviews的認證僅限于9080端口:
apiVersion: "authentication.istio.io/v1alpha1"kind: "Policy"metadata:name: "details-and-reviews"spec:targets:- name: details- name: reviewsports:- number: 9000peers:- mtls: {}
當上述配置生效之後,在沒有開啟雙向認證(mTLS)的情況下,其他服務将無法直接通路details以及reviews服務。那麼該如何開啟mTLS呢?Istio在DestinationRule這個CRD中實作了該功能:
apiVersion: networking.istio.io/v1alpha3kind: DestinationRulemetadata:name: detailsspec:host: detailstrafficPolicy:tls:mode: ISTIO_MUTUAL
通過指定DestinationRule中的spec.trafficPolicy.tls.mode為ISTIO_MUTUAL,其他服務在與details服務通信時将自動開啟mTLS。
最終上述Istio提供的安全相關的抽象接口将由控制面元件Pilot轉換為控制面代理Envoy的具體配置,Envoy再基于這些配置對代理的流量進行認證操作。
為了更好地了解在Envoy層面是如何基于安全配置進行認證的,首先有必要了解Envoy的整體架構以及流量在其中的處理過程。對于這個問題,Envoy官方釋出的文章《Life of a Request》已經做了非常好的解釋,這裡将隻選取與本文相關的部分進行重點說明。
例如,當details服務相關的Pods作為服務端接收通路時,代理這些Pods的Envoy首先需要對輸入流量進行處理。對于輸入流量(Inbound Traffic)的操作較為簡單,Envoy中僅有一個監聽端口為15006的virtualInbound listener用于接收處理。
一個listener中往往包含多條filter chain,請求需要通過目的位址和端口找到比對的filter chain并周遊其中的filter進行處理,預設情況下filter chain中隻包含處理HTTP請求的HCM(HTTP connection manager,一般作為最後一個filter),HCM内部還會包含一條HTTP filter chain用于對請求的進一步處理。
對于輸入流量, HCM會将HTTP filter chain處理後的流量直接轉發到Inbound Cluster完成整個處理過程。
當使用者配置了上述Policy之後,details服務對應pods的Envoy代理中會增加如下内容用于安全處理:
{"filter_chain_match": {"prefix_ranges": [{"address_prefix": "172.16.234.118","prefix_len": 32}],"destination_port": 9080,"application_protocols": ["istio"]},"tls_context": {"common_tls_context": {"tls_certificates": [{"certificate_chain": {"filename": "/etc/certs/cert-chain.pem"},"private_key": {"filename": "/etc/certs/key.pem"}}],"validation_context": {"trusted_ca": {"filename": "/etc/certs/root-cert.pem"}},"alpn_protocols": ["h2","http/1.1"]},"require_client_certificate": true},"filters": [....
filter chain中增加了tls_context字段的内容,其中包含了作為服務端認證所需的證書,私鑰以及CA的路徑。HTTP connection manager也會額外增加一個名為istio_authn的額外的http filter,表明輸入的通路流量首先需要經過雙向認證:
"http_filters": [{"name": "istio_authn","typed_config": {"@type": "type.googleapis.com/istio.envoy.config.filter.http.authn.v2alpha1.FilterConfig","policy": {"peers": [{"mtls": {}}]}}},{"name": "mixer",....
對于輸出流量的處理過程則稍顯複雜,如下所示:
當有Pod需要通路details服務時,該Pod的代理Envoy則需要對輸出流量進行處理。輸出流量首先通過監聽端口15001進入virtualOubound listener,這個listener僅僅作為統一的接入點接收所有的輸出流量,後續根據流量的目标端口進入相應的listener。
例如目标端口為9080的所有流量都會進入名為0.0.0.0_9080的listener。在該listener中除了目标位址為本Pod IP的所有流量都會進入預設的filter chain。這個filter chain的HTTP connection manager則會将直接将流量轉發至名為"9080"的路由表。該路由表基于域名等條件進行比對,得到目标Outbound Cluster,最終将輸出流量發送至該Cluster,完成整個處理流程。
如果目标Cluster配置了上文所示的DestinationRule,即要求與目标Cluster進行雙向認證,則與Inbound類似,在Outbound Traffic的處理流程中同樣需要加載證書、私鑰以及CA等一系列配置。
與Inbound不同的是,Outbound對于這部分内容的配置下沉到了具體的Cluster配置中,而不是位于filter chain中為後續的Cluster共享。因為隻有DestinationRule配置了TLS的Cluster才需要進行雙向認證,其他Cluster預設是不需要的。配置示例如下:
{"version_info": "2020-11-23T11:34:19Z/47","cluster": {"name": "outbound|9080||details.default.svc.cluster.local","type": "EDS","eds_cluster_config": {"eds_config": {"ads": {},"initial_fetch_timeout": "0s"},"service_name": "outbound|9080||details.default.svc.cluster.local"},"connect_timeout": "10s","lb_policy": "RANDOM","circuit_breakers": {"thresholds": [{"max_retries": 1024}]},"tls_context": {"common_tls_context": {"tls_certificates": [{"certificate_chain": {"filename": "/etc/certs/cert-chain.pem"},"private_key": {"filename": "/etc/certs/key.pem"}}],"validation_context": {"trusted_ca": {"filename": "/etc/certs/root-cert.pem"},"verify_subject_alt_name": ["spiffe://cluster.local/ns/default/sa/bookinfo-details"]},"alpn_protocols": ["istio"]},"sni": "outbound_.9080_._.details.default.svc.cluster.local"},...
如上,Cluster中擴充了tls_context字段,配置了與目标Cluster進行雙向認證時所需的證書、私鑰以及CA等内容。
當需要通路目标Cluster時,請求發起端的Pod會加載Outbound Cluster中的證書、私鑰以及CA并作為用戶端,而請求接收端的Pod則會加載Inbound Filter Chain中的證書、私鑰以及CA作為服務端,兩者完成雙向認證。
在Istio 1.3中,Citadel預設會為每個Service Account簽發證書并建立相應的Secret用于儲存,Istio在注入Sidecar的同時會将包含相關SA(被注入Sidecar的Pod綁定的Service Account)的證書、私鑰以及CA的Secret挂載到Sidecar的/etc/certs目錄下,Pod在需要進行雙向認證時則引用/etc/certs下的内容。事實上,對于同一個Pod,無論是作為用戶端(Outbound Cluster中的配置)還是作為服務端(Inbound Filter Chain中的配置)引用的都是同一套證書。
不過将證書存放在Secret中并以目錄的形式挂載供Pod使用并不是一種好的方式,後續Istio對證書的簽發以及擷取機制進行了反複的疊代,關于這部分内容,後文将有獨立的章節進行詳細叙述。
3
Authorization概念、執行個體及實作
關于鑒權,與Kubernetes類似,Istio也支援基于RBAC的鑒權方式,實作了mesh、namespace、service以及方法級别的通路控制。首先,抽象了一個網格級别的CRD資源對象CluterRbacConfig用于啟動鑒權。需要注意的是ClusterRbacConfig隻能存在一個執行個體且名字需要為’default’,例如:
apiVersion: "rbac.istio.io/v1alpha1"kind: ClusterRbacConfigmetadata:name: defaultspec:mode: 'ON_WITH_INCLUSION'inclusion:namespaces: ["default"]
上述配置表示隻對default這個namespace開啟鑒權,即default内的所有服務預設不可通路。
類比于Kubernetes中的RBAC,若要放開對于某些服務的通路權限,需要配置相應的ServiceRole和ServiceRoleBinding。不難了解,ServiceRole用于定義一系列的通路權限而ServiceRoleBinding則将ServiceRole表示的權限授予特定的對象,兩者的示例如下:
apiVersion: "rbac.istio.io/v1alpha1"kind: ServiceRolemetadata:name: products-viewernamespace: defaultspec:rules:- services: ["products.default.svc.cluster.local"]methods: ["GET"]
上述ServiceRole表示允許用’GET’方法通路default namespace下的products service
apiVersion: "rbac.istio.io/v1alpha1"kind: ServiceRoleBindingmetadata:name: binding-products-allusersnamespace: defaultspec:subjects:- user: "*"roleRef:kind: ServiceRolename: "products-viewer"
上述ServiceRoleBinding則表示将’product-views’代表的權限授予任何使用者,包括已認證的和非認證的。
同樣,控制面元件Pilot會将上述ServiceRole等鑒權相關的上層抽象資源轉換為控制面代理Envoy的配置。每個Envoy中都内置了一個Authorization Engine用于在運作時對請求進行鑒權處理,決定允許或者拒絕對相關服務的通路。最終,Envoy用Inbound listener filter chain中一個額外的名為"envoy.filters.http.rbac"的HTTP filter來承載上文鑒權相關的配置:
{"name": "envoy.filters.http.rbac","typed_config": {"@type": "type.googleapis.com/envoy.config.filter.http.rbac.v2.RBAC","rules": {"policies": {"productpage-viewer": {"permissions": [{"and_rules": {"rules": [{"or_rules": {"rules": [{"header": {"name": ":method","exact_match": "GET"}}]}}]}}],"principals": [{"and_ids": {"ids": [{"any": true}...
需要注意的是,如果目标服務所在的namespace沒有開啟鑒權的,上面的RBAC HTTP filter是不會存在的(因為所有服務預設都可以通路);而在開啟鑒權的情況下,如果沒有建立對應的ServiceRole以及ServiceRoleBinding,則RBAC HTTP filter的規則清單為空,即該Envoy代理的輸入流量的目标服務完全不允許通路:
{"name": "envoy.filters.http.rbac","typed_config": {"@type": "type.googleapis.com/envoy.config.filter.http.rbac.v2.RBAC","rules": {}}}
總的來說,Istio提供了多層級、細粒度的權限通路控制并且配置方法也較為簡單,對于更為複雜的授權政策可以參見官網。
證書擷取機制及其演進過程
已知在Kubernetes環境下,Istio将Pod綁定的Service Account作為其辨別。預設情況下,Citadel會監聽叢集中Service Account,為其生成對應的私鑰以及證書并建立類型為istio.io/key-and-cert的Secret用于儲存:
[root@physical-56 yzz]# kubectl get sa NAME SECRETS AGEbookinfo-details 1 26hbookinfo-productpage 1 26hdefault 1 5d4h[root@physical-56 yzz]# kubectl get secret NAME TYPE DATA AGEbookinfo-details-token-njnts kubernetes.io/service-account-token 3 26hbookinfo-productpage-token-6pwnk kubernetes.io/service-account-token 3 26hdefault-token-mfwwk kubernetes.io/service-account-token 3 5d4histio.bookinfo-details istio.io/key-and-cert 3 26histio.bookinfo-productpage istio.io/key-and-cert 3 26histio.default istio.io/key-and-cert 3 28h
當建立Pod并将其加入網格,在注入Sidecar的同時,Istio會将該Pod綁定的Service Account對應的Secret挂載至Sidecar中:
[root@physical-56 yzz]# kubectl describe pods productpage-v1-59984c8fb5-27lp7Name: productpage-v1-59984c8fb5-27lp7Namespace: defaultPriority: 0Node: 192.168.132.14/192.168.132.14Start Time: Mon, 23 Nov 2020 17:23:41 +0800Labels: app=productpagepod-template-hash=59984c8fb5version=v1...istio-proxy:Port: 15090/TCPHost Port: 0/TCPArgs:proxysidecar--domain...Mounts:/etc/certs/ from istio-certs (ro)/etc/istio/proxy from istio-envoy (rw)/var/run/secrets/kubernetes.io/serviceaccount from bookinfo-productpage-token-6pwnk (ro)Volumes:istio-certs:Type: Secret (a volume populated by a Secret)SecretName: istio.bookinfo-productpageOptional: true...[root@physical-56 yzz]# kubectl exec productpage-v1-59984c8fb5-27lp7 -c istio-proxy -- ls /etc/certscert-chain.pem key.pem root-cert.pem
Citadel會對證書的生命周期進行管理并且能夠通過重寫Secret的方式對證書進行輪轉,但是顯而易見,這種證書簽發管理方式存在如下問題:
- 私鑰存放在Secret中,幾乎等同于明文存儲,存在安全隐患
- 證書輪轉時需要熱重新開機Envoy,影響性能
針對上述問題,Istio在1.1版本之後引入了一種新的基于SDS的證書簽發方式:
已知Envoy基于XDS API進行資源的動态發現,而SDS則是其中一種Envoy用于從遠端SDS Server動态獲驗證書密鑰的API。在新的方案中,每個節點都會部署一個Node Agent作為SDS Server,用于處理該節點所有Envoy的SDS請求。Pilot依然會對上層認證相關的API進行轉換并下發至Envoy,但是此時tls_context中将不再包含對/etc/certs目錄下的證書的直接引用,而是包含了與SDS Server互動的配置:
"tls_context": {"common_tls_context": {..."tls_certificate_sds_secret_configs": [{"name": "default","sds_config": {"api_config_source": {"api_type": "GRPC","grpc_services": [{"google_grpc": {"target_uri": "unix:/var/run/sds/uds_path",..."call_credentials": [{"from_plugin": {"name": "envoy.grpc_credentials.file_based_metadata","typed_config": {"@type": "type.googleapis.com/envoy.config.grpc_credential.v2alpha.FileBasedMetadataConfig","secret_data": {"filename": "/var/run/secrets/kubernetes.io/serviceaccount/token"},...],"combined_validation_context": {"default_validation_context": {},"validation_context_sds_secret_config": {"name": "ROOTCA","sds_config": {"api_config_source": {"api_type": "GRPC","grpc_services": [{"google_grpc": {"target_uri": "unix:/var/run/sds/uds_path",..."call_credentials": [{"from_plugin": {"name": "envoy.grpc_credentials.file_based_metadata","typed_config": {"@type": "type.googleapis.com/envoy.config.grpc_credential.v2alpha.FileBasedMetadataConfig","secret_data": {"filename": "/var/run/secrets/kubernetes.io/serviceaccount/token"},...
在上面的tls_context中包含了兩段SDS配置,命名為"default"的SDS配置用于請求證書以及私鑰,而命名為"ROOTCA"的SDS配置則用于請求root ca。在每段SDS配置中,重點關注target_uri和filename字段,前者表明與SDS Server的互動位址,一般是一個Unix Domain Socket,Node Agent和應用Pod的Sidecar都會以HostPath的形式對該socket所在的目錄進行挂載。後者為Pod關聯的Service Account的JWT的所在路徑。
在這種模式下,證書的分發過程如下:
- Envoy基于SDS配置發送SDS請求,請求中包含資源名"default"或者"ROOTCA"以及其所在Pod綁定的Servcie Account的JWT,Citadel利用JWT可以擷取對應Service Account的資訊并且将其包含在證書中,用于辨別。
- Node Agent建構私鑰并且基于SDS請求中的JWT向Citadel發起CSR(Certificate Signing Requests)
- Citadel接收到CSR之後,首先向Kubernetes APIServer驗證Service Account的JWT,确認後,簽發證書并傳回至Node Agent
- Node Agent将簽發後的證書、私鑰以及root ca以SDS Response的形式傳回至Envoy,完成整個證書的簽發流程,Envoy則會将擷取的證書緩存如下:
{"@type": "type.googleapis.com/envoy.admin.v2alpha.SecretsConfigDump","dynamic_active_secrets": [{"name": "spiffe://cluster.local/ns/istio-system/sa/istio-pilot-service-account","version_info": "2020-11-25 00:54:08.854459604 +0000 UTC m=+495600.689938099","last_updated": "2020-11-25T00:54:10.058Z","secret": {"name": "default","tls_certificate": {"certificate_chain": {"inline_bytes": "LS0tLS1CRUdJ....."},"private_key": {"inline_string": "[redacted]"}}}},{"name": "ROOTCA","version_info": "2020-11-24 01:47:08.6870616 +0000 UTC m=+412380.522540112","last_updated": "2020-11-24T01:47:08.858Z","secret": {"name": "ROOTCA","validation_context": {"trusted_ca": {"inline_bytes": "LS0tLS1CRUdJTiBDRVJUSUZJ......"}}}}...
可以看到與基于Secret的證書簽發方式相比,新的方案具有的優點如下:
- Envoy能夠利用SDS API動态重新整理證書和密鑰而無須重新開機,不再影響性能
- 無須依賴Kubernetes Secret,進而避免Secret機制中已知的安全問題
-
私鑰不再離開其生成節點:
私鑰由Node Agent建立并且隻會在SDS Response中傳遞給Envoy,而且隻在兩者的記憶體中存在
但是在這種模式下,因為運作在同一個節點的所有Pods都會共享一個Node Agent以及Unix Domain Socket,一方面Node Agent會存在可用性問題,另一方面為了防止對于共享的Unix Domain Socket的惡意篡改進而竊取該節點其他Pods的認證資訊,需要配置複雜的PSP規則,限制隻有Node Agent所在的Pod能夠修改共享的Unix Domain Socket,而其他都Pod都隻享有隻讀權限。
針對這些問題,Istio社群對這個方案進行了進一步的演化,去除了每個節點的Node Agent,并且将SDS Server内嵌到每個Sidecar的Pilot-Agent程序中:

Istio為應用注入的Sidecar容器事實上不止一個Envoy程序,還有另一個名為Pilot-Agent的程序用于Envoy的生命周期管理,而現在它又承擔起了作為SDS Server的責任。雖然從架構來看,兩個方案之間似乎存在着巨大的差異,但是真正的差異隻有以下兩點:
-
Envoy與SDS Server(此處即為Pilot-Agent)互動的Unix Domain Socket為每個Sidecar私有,無需再配置複雜的PSP機制防止惡意篡改。
SDS Server建立的私鑰的傳輸範圍也進一步縮小,甚至都不會離開Sidecar容器。
- 之前共享SDS Server時,Envoy發送的SDS Request需要攜帶相應的Service Account JWT用于辨別身份,在新的方案下就不再需要了,因為每個SDS Server都是每個Pod獨享的,直接從相應的挂載目錄/var/run/secrets/kubernetes.io/serviceaccount/token讀取JWT即可。
經過這三個方案的演進,Istio的證書簽發管理機制已經變得非常安全優雅。
總結