ljzsdut
GitHubToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

13 Envoy服务网格安全

Envoy的身份认证机制

Envoy认证机制

Envoy支持两种类型的认证机制:

  • 传输认证:即服务间的认证,它基于双向TLS实现传输认证(即mTLS),包括双向认证 、信道安全和证书自动管理;每个服务都需要有其用于服务间双向认证的标识,以实现 此种认证机制;
  • 用户认证:也称为终端用户认证,用于认证请求的最终用户或者设备;Envoy通过JWT( JSON Web Token)实现此类认证需求,以保护服务端的资源;
    • 客户端基于HTTP标头向服务端发送JWT
    • 服务端验证签名
    • envoy.filters.http.jwt_authn过滤器

TLS&mTLS静态配置

Envoy支持在侦听器中实现TLS终止以及与上游集群建立连接时的TLS始发。

  • Listener:与客户端通信时的TLS终止
  • Cluster:同上游建立TLS通信时的始发

TLS终止定义于Listener中,而与上游集群的连接始发定义于Cluster中;在底层使用BoringSSL作为SSL库;DownstreamTlsContexts支持多个TLS证书(多个证书需要属于同一类型,RSA或ECDSA),但UpstreamTlsContexts目前仅支持单个证书;支持执行标准边缘代理任务,以及启动与具有高级TLS要求的外部服务(TLS1.2,SNI等)的连接;

仅在验证上下文指定一个或多个受信任的证书颁发机构后才会启用上下游的证书 验证功能;Linux和BSD系统上CA包的常用路径如下:

  • /etc/ssl/certs/ca-certificates.crt (Debian/Ubuntu/Gentoo等)
  • /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem (CentOS/RHEL 7)
  • /etc/pki/tls/certs/ca-bundle.crt (Fedora/RHEL 6)
  • /etc/ssl/ca-bundle.pem (OpenSUSE)
  • /usr/local/etc/ssl/cert.pem (FreeBSD)
  • /etc/ssl/cert.pem (OpenBSD)

设定数字证书

配置时,可以通过静态资源格式指定使用的TLS证书,也可以通过SDS动态获取 TLS证书;

  • SDS可以简化证书管理,各实例的证书可由SDS统一推送,证书过期后,SDS推送新证书至Envoy实例可立即生效而无需重启或重新部署;
  • 获取到所需要的证书之后侦听器方能就绪;不过,若因同SDS服务器的连接失败或收到其错误 响应而无法获取证书,则侦听器会打开端口,但会重置连接请求;
  • Envoy同SDS服务器之间的通信必须使用安全连接;
  • SDS服务器需要实现gRPC服务SecretDiscoveryService,它遵循与其他xDS相同的协议;

设定数字证书的方式:

  • 静态格式的Secret定义在static_resources上下文,并由listener或cluster在tls_context通过指定文件路径引用
  • 不予事先定义Secret,而由listener或cluster直接在tls_context中定义
  • 通过SDS提供证书时,需要配置好SDS集群,并由listener或cluster在tls_context中通过sds_config 引用;

设定Secret

定义Secret时,通常有定义数字证书(服务端或客户端)、票证密钥和证书校验机 制三种类型,但每个定义仅能指定为其中一种类型:

static_resources:
	listeners: []
	clusters: []
	secrets: [] # 静态指定的Secret列表,定义时,以下三种方式可选其一; 
	- name: ... # 可用于引用此秘密(Secret)的惟一标识;
		tls_cretificate: {...} # 数字证书
			certificate_chain: {...} # TLS证书链
				filename: ... # 保存有证书信息的文件;也可使用inline_bytes或inline_string进行指定,即DataSource格式;
			private_key: {...} # TLS私钥,遵循DataSource格式;
			password: {...} # 私钥信息的加解密密钥,未指定时需要私钥文件处于未加密状态,遵循DataSource格式;
	- name: ... # 可用于引用此秘密的惟一标识;
		session_ticket_keys: {...} # 定义用于加密和解密TLS会话票证的密钥
			keys: [] # 密钥列表,未指定时将使用内部生成和管理的密钥,定义格式遵循DataSource格式; 
								#安全起见,应该每小时轮换一次,但使用安全的随机数据源;
	- name: ... # 可用于引用此秘密(Secret)的惟一标识;
		validation_context: {...} # 对等证书验证机制的相关配置
			trusted_ca: {...} # 信任的CA的证书,未指定时不会验证对端证书;
			crl: {...} # 可选的PEM格式的证书吊销列表,定义格式遵循DataSource格式;
			verify_certificate_spki: [] # base64编码的SHA-256哈希码,用于验证DER编码格式证书的公钥信息的SHA-256编码是否与列表项之一匹配 
			verify_certificate_hash: [] # base64编码的SHA-256哈希码,用于验证DER编码格式证书的SHA-256编码是否与列表项之一匹配 
			verify_subject_alt_name: [] # Subject备用名称列表,可选,用于验证证书的主题备用名称是否与列表项之一匹配 
			allow_expired_certificate: ... # 布尔型数据,用于定义是滞不会拒绝过期的证书
 

例如:

static_resources:
  secrets:
  - name: server_cert
    tls_certificate:
      certificate_chain:
        filename: "/etc/envoy/certs/server.crt"
      private_key:
        filename: "/etc/envoy/certs/server.key"
  - name: client_cert
    tls_certificate:
      certificate_chain:
        filename: "/etc/envoy/certs/client.crt"
      private_key:
        filename: "/etc/envoy/certs/client.key"
  - name: validation_context
    validation_context:
      trusted_ca:
        filename: "/etc/envoy/ca/ca.crt"

TLS Context配置

tls_context是过滤链的一个属性,服务于过滤链中HCM过滤器中的router过滤器,为router过滤器中的domain提供tls配置。

DownstreamTlsContext(listener)

auth.DownstreamTlsContext,定义在listener中,它支持三种定义格式:

  • 直接在listener的tls_context中通过tls_certificates参数定义;
  • 在static_resource上下文定义secret,而后在listener的tls_context中直接通过 tls_certificate_sds_secret_configs参数引用;
  • 直接在listener的tls_context中通过tls_certificate_sds_secret_configs参数的sds_config指定通过SDS API获取;
listeners:
- name: ...
	... 
	filter_chains: 
	- filters: []
		tls_context: {...} # 当前过滤器链的TLS上下文;
			tls_params: {...} # TLS协议版本和密码套件等; 
			tls_certificates: [] # TLS证书列表;
				certificate_chain: {...} # TLS证书链;
					filename: ... # 保存有证书信息的文件;也可使用inline_bytes或inline_string进行指定,即DataSource格式;
				private_key: {...} # TLS私钥;
				password: {...} # 解密私钥文件的口令,未指定时需要私钥文件处于未加密状态;
			tls_certificate_sds_secret_configs: {...} # 通过静态资源中定义的Secret或SDS获取Secret;
      	name: ... # secret的惟一标识,可以FQDN、UUID、SPKI或SHA256格式;
      						# 仅指定name时,表示加载static_resources中定义的secret;
				sds_config: {...} # xDS API ConfigSource,支持path、api_config_source或ads格式;
			validation_context: {...} # 如何验证对等证书
			validation_context_sds_secret_config: {...} # 通过SDS获取验证上下文;
		require_client_certificate: ... # 布尔型值,是否要求并验证客户端证书;、 
		session_ticket_keys: {...} # TLS会话票证相关的设置

例如:

static_resources:
  secrets:
  - name: server_cert
    tls_certificate:
      certificate_chain:
        filename: "/etc/envoy/certs/server.crt"
      private_key:
        filename: "/etc/envoy/certs/server.key"
  - name: client_cert
    tls_certificate:
      certificate_chain:
        filename: "/etc/envoy/certs/client.crt"
      private_key:
        filename: "/etc/envoy/certs/client.key"
  - name: validation_context
    validation_context:
      trusted_ca:
        filename: "/etc/envoy/ca/ca.crt"
  listeners:
  - name: listener_https
    address:
      socket_address: { address: 0.0.0.0, port_value: 443 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          stat_prefix: ingress_https
          codec_type: AUTO
          route_config:
            name: https_route
            virtual_hosts:
            - name: https_route
              domains: ["*"]
              routes:
              - match:
                  prefix: "/service/gray"
                route:
                  cluster: service-gray
              - match:
                  prefix: "/service/purple"
                route:
                  cluster: service-purple
              - match:
                  prefix: "/"
                route:
                  cluster: mycluster
          http_filters:
          - name: envoy.router
            typed_config: {}
      tls_context:
        common_tls_context:
          tls_certificate_sds_secret_configs:
          - name: server_cert

tls_context或使用如下方式:

      tls_context:
        common_tls_context:
          tls_certificates:
          - certificate_chain:
              filename: "/etc/envoy/certs/server.crt"
            private_key:
              filename: "/etc/envoy/certs/server.key"

UpstreamTlsContext(cluster)

auth.UpstreamTlsContext ,定义在cluster中,与集群中的主机通信时使用, 它同样支持类似listener的tls_context一样的三种定义格式;

clusters:
- name: ...
	...
		tls_context: {...} # 当前过滤器的TLS上下文;
			tls_params: {...} # TLS协议版本和密码套件等; 
			tls_certificates: [] # TLS证书列表;
				certificate_chain: {...} # TLS证书链;
					filename: ... # 保存有证书信息的文件;也可使用inline_bytes或inline_string进行指定,即DataSource格式;
				private_key: {...} # TLS私钥;
				password: {...} # 解密私钥文件的口令,未指定时需要私钥文件处于未加密状态;
			tls_certificate_sds_secret_configs: {...} # 通过静态资源中定义的Secret或SDS获取Secret;
				name: ... # secret的惟一标识,可以FQDN、UUID、SPKI或SHA256格式;仅指定name时,表示加载static_resources中定义的secret;
				sds_config: {...} # xDS API ConfigSource,支持path、api_config_source或ads格式;
			validation_context: {...} # 如何验证对等证书
			validation_context_sds_secret_config: {...} # 通过SDS获取验证上下文;
		allow_renegotiation: ... # 布尔型值,是否允许服务器启动的TLS重新协商;
		max_session_key: ... # 为会话恢复而存储的最大会话密钥数,默认为1,0表示禁用会话恢复; 
		sni: ... # 创建TLS后端连接时使用的SNI字符串
 

例如:

  clusters:
  - name: service-purple
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: service-purple
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: service-purple
                port_value: 443
    tls_context:
      common_tls_context:
        tls_certificate_sds_secret_configs:
        - name: client_cert
        validation_context_sds_secret_config:
          name: validation_context

演示示例

root@istio:~/ServiceMesh_in_Practise/security/tls-static# ls
README.md  certs  docker-compose.yaml  front-envoy.yaml  gencerts.sh  openssl.conf  service-gray.yaml  service-purple.yaml

以上示例中:

访问:8443/service/colors,通过http代理至后端。

访问:8443/service/gray,通过https代理至后端。

访问:8443/service/purple,通过双向https(mtls)代理至后端。

TLS&mTLS的SDS配置Spiffe/Spire

SPIFFE标准定义了一种能够跨异构环境和组织边界完成bootstrap以及向服务发布 身份ID的规模框架;SPIFFE规范标准化了向工作负载(workload)分配身份、验证和确认工作负载身份以及 工作负载API以检索身份的过程;SPIFFE身份包含在SVID(SPIFFE Verifiable Identity Document)中, SVID规范提供了实 现SVID时必须支持的属性的要求;

SPIRE是SPIFFE规范的工具链实现,也是一种参考实现,它可以跨不同的部署环 境在工作负载(使用mTLS或JWT)之间建立信任关系,签发SPIFFE ID和工作负载 API以检索工作负载SVID;

简而言之,SPIFFE是一个可信bootstrapping和identification框架,它已作为标准提 交并被CNCF(云原生计算基金会)接受;到目前为止,该标准具有两个主要实现,分别是SPIRE和Istio Citadel;

SPIFFE标准的组件

  • SPIFFE ID:标准化身份名称空间,并定义服务如何确认彼此身份;用于惟一标识一个workload的字符串,也可用于标识workload运行的中间层,例如虚拟机、kubernetes 等;使用URI格式:spiffe://trust domain/workload_identifier

    trust domain:可信域。一个可信域对应于一个系统的trust root,同一可信域中的所有workload都可由所属域的根密钥进行验证;可信域名义上是自注册的,与公共DNS不同,可信域没有委派机制;可信域中必须存在签名机构,并且该签名机构必须拥有自己的SVID;

  • SVID:(SPIFFE Verifiable Identity Document)用于定义如何表示及验证颁发的身份标识的规范;SVID是workload用于证明自身身份的文档,由可信域的CA签署后才有效;SVID将SPIFFE ID编码为可加密验证的文档,目前支持X.509-SVID和JWT-SVID两种编码格式;后一种格式易受到重放攻击,建议尽可能地使用前一种格式;

  • Workload API:工作负载泛指单个应用程序,它可能包含多个正在运行的实例,所有这些实例都执行相同的任务;Workload API用于颁发X.509-SVID或JWT-SVID

SPIFFE Workload API用于提供使工作负载能够使用的SPIFFE身份,以及基于的身份验证系统的信息和服务(例如Spire agent);SPIFFE Workload API支持任意数量的本地客户端,从而使其能够引导可以访问它的任何进 程的身份;通常,希望在每个进程被授予某些身份的情况 下即可以每个进程为基础分配身份;为此,SPIFFE Workload API实现必须能够确定调用者的身份;

JWT认证

Envoy基于JWT Authentication过滤器完成终端用户认证。

  • 基于过滤器核验JWT的签名、受众和颁发者以确定其携带有有效的令牌;若验证失败则请求被拒绝;
  • 支持在请求的各种条件下检查JWT,例如仅针对特定路径;
  • 支持从请求的各个位置提取JWT,并可合并同一请求中的多个JWT需求;
  • JWT签名所需的JWKS(Json Web Key Set)可在过滤器配置中内联指定,也可通过HTTP/HTTPS从远程 服务器获取

配置时,需要使用名称envoy.filters.http.jwt_authn配置此过滤器,主要由两个字段:

  • providers:定义如何验证JWT,例如提取令牌的位置、获取公钥的位置以及输出有效负载的位置等;
  • rules:定义匹配规则及相关要求;

JWT配置

http_filters:
- name: envoy.filters.http.jwt_authn
	config:
		providers:  	# 定义provider名称及其相关的属性,以指定如何验证JWT;可定义多个porovider
	    xxx: ... 											# provider名称;
				issuer: ... 								# JWT的签发者,通常是一个URL或一个email;
				audiences: [] 							# 允许访问的JWT受众列表,含有此处指定的audiences其中之一的JWT即可被接受; 
				remote_jwks: {...} 					# 可通过HTTP/HTTPS远程获取到的JWK,因此内嵌的最重要字段即为uri; 
				local_jwks: {...} 					# 本地数据源可以访问到的JWK,支持通过本地文件或inline_string方式加载; 
				from_headers: {...} 				# 定义从通过哪个HTTP协议标头获取JWT,例如“Authorization: Bear <token>”; 
				from_params: {...} 					# 定义从哪个URL param中获取JWT;
				forward: ... 								# 布尔型值,定义是否无需在认证成功后从请求中移除JWT,默认为false,即需要移除; 
				forward_payload_header: ... # 指定用于将经过验证的JWT的payload转发至后端的标头;
		rules: [] # 定义特定路由条件下的JWT验证要求(对哪些request进行jwt验证); 
		- match: {...} # 路由匹配条件
			requires: {...} # JWT验证要求,以下验证方式仅能使用其中一种;
				provider_name: ... # 需要的provider名称;
				provider_and_audiences: {...} # 需要的provider和audiences;
				requires_any: {...} # 由requirements参数指定需要的providers列表,其中任何一个provider验证通过,结果即为通过; 
				requires_all: {...} # 由requirements参数指定需要的providers列表,其中所有的provider验证通过,结果才为通过; 
				allow_missing_or_failed: {...} # 即便JWT缺失或验证失败,结果依然通过;

授权

Role Based Access Control (RBAC)

  • 基于角色的访问控制,围绕“角色”和“许可”定义,与策略无关;
  • Grant access by roles
  • Coarse-grained
  • May cause role explosions
  • separation of duty (SOD)

Attribute Based Access Control (ABAC)

  • 下一代授权模型
  • Grant access by policies,also known as policy-based access control,定义了一种访问控制范式,通过将属性组合为 策略授予用户访问权限;与RBAC相比,它还额外兼顾使用角色和组之外的属性,且基于策略而非静态定义的 权限;
  • boolean logic:可基于复杂的布尔规则集定义;
  • context,例如time、location和IP等;
  • ABAC可以看作是外部的和动态的授权管理机制,能够完成细粒度的授权;

image-20220503111221987

RBAC

RBAC是一种操作授权机制,用于界定“谁(Subject)”能够“操作( Verb)”哪个或哪类“对象(Object)”;

Envoy的RBAC过滤器为服务提供服务级别和方法级别(GET/POST/…)的访问控制功能,相关过滤器配置名称为envoy.filters.http.rbac。该过滤器支持基于连接属性(IP、Port或SSL Subject)以及传入的请求的HTTP标头安全列表(Allow)或阻止列表(Deny)策略集进行配置;支持强制模式和影子模式,影子模式(仅供参考)仅用于验证策略而不会产生真正的影响。

Envoy的RBAC配置主要由两个参数组成:

  • action:策略匹配时要采取的操作,当且仅当以下情形方才允许请求的操作。
    • action为允许,且至少有一个策略匹配;
    • action为拒绝,但没有任何策略匹配。
  • policies:从策略名称到策略的映射,成功的条件是至少一个策略与请求匹配

配置语法

action: ... # 策略匹配时的操作行为,支持ALLOW和DENY两个; 
policies: {...} # 授权策略
	ROLE_NAM: # 角色名称
		permissions: [] # 应用于一个角色之上的权限许可列表,各列表项之间为“或”关系;
			any: ... # 布尔型值,是否匹配所有操作权限;
			header: {...} # 核验传入的HTTP请求报文的指定标头;仅适用于HTTP请求; 
			destination_ip: {...} # 针对于目标IP的CIDR地址块的操作权限; 
			destination_port: {...} # 针对于目标端口的操作权限;
			metadata: {...} # 针对于指定的元数据的操作权限; 
			requested_server_name: ... # 针对于客户端请求的目标服务器的操作权限; 
			and_rules: {...} # 以“与”关系定义的一组操作权限;
			or_rules: {...} # 以“或”关系定义的一组操作权限;
			not_rules: {...} # 以“非”关系定义的一组操作权限;
		principals: []  # 如何才能具有相关的权限,多个列表“或”关系
			authenticated: {...} # 经过认证的;
			header: {...} # 传入HTTP请求报文的指定标头;
			metadata: {...} # 描述有关Subject的其它信息的元数据; 
			and_ids: {...} # “与”关系的一组主体;
			or_ids: {...} # “或”关系的一组主体;
			not_id: {...} # “非”关系主体,即指定主体之外的其他主体;
 

示例

下面是一个许可权限示例,它定义了service-admin和product-viewer两个role

  • service-admin: 经过认证的Service账号admin和superuser拥有全部操作权限;
  • 其它任何用户仅可于端口80或443之上针对/products起始的URL执行GET请求方法;
action: ALLOW 
policies:
	service-admin:
		permissions:
			- any: true
		principals:
			- authenticated:
					principal_name:
						exact: "cluster.local/ns/default/sa/admin"
			- authenticated:
					principal_name:
						exact: "cluster.local/ns/default/sa/superuser"
	product-viewer:
  	permissions:
  		- and_rules:
  				rules:
  					- header: { name: ":method", exact_match: "GET" }
						- header: { name: ":path", regex_match: "/products(/.*)?" } 
						- or_rules:
								rules:
									- destination_port: 80 
									- destination_port: 443
  	principals:
  		- any: true

外部授权(envoy.ext_authz)-ABAC

外部授权

外部授权(External Authorization)即调用第三方的授权服务来核验用户权限的机制;

Envoy通过外部授权过滤器(envoy.ext_authz)调用外部的授权服务以检查传入的请求是否已经获取授权。此过滤器可以配置为网络过滤器(config.filter.network.ext_authz.v2.ExtAuthz),也可以配 置为HTTP过滤器(config.filter.http.ext_authz.v2.ExtAuthz)甚至是二者的组合。若是网络过滤器核验授权失败,则直接关闭该连接;若是HTTP过滤器核验授权失败,则响应为403(Forbidden);

外部授权服务通常应该定义为上游集群。收到请求并核验其授权时,若外部授权服务不可用,请求是否能获得授权则取决于过滤器的failure_mode_allow参数的配置;

Open Policy Agent(OPA)

Open Policy Agent(简称OPA)是一款go语言编写的开源通用策略引擎,它通过高级声明式语言rego编写策略代码为应用程序实现细粒度的访问控制机制,可用于为微服务、Kubernetes、CI/CD pipeline和API Gateway等应用场景实施策略机制;

OPA可以同微服务一起部署为独立运行的服务,例如以sidecar形式运行。通常,出于保护应用程序的目的,发往微服务的每个请求都需要获得授权后才能进行处理。而为了检查授权,微服务则需要对OPA服务发出API调用,以确定收到的请求是否被授权,但策略的执行需要由应用程序完成,例如某HTTP请求被策略拒绝时,应用程序需 要响应以“HTTP 403 Forbidden”;

image-20220503104903415

OPA决策机制

在OPA中,决策过程依赖于三个输入:

  • Query:查询输入,它会触发决策过程;查询数据指定了OPA应该决策的事情,它必须格式化 为JSON格式;例如,对于“是否允许用户Alice调用“GET /resrources”这个问题,查询输入包含了Alice、GET和 /resources三个参数;
  • Data:OPA进行决策时需要参考的适配于特定环境的事实定义;例如,需要决策将Pod调度至哪个Kubernetes集群节点 ,数据应该是可用节点及相应容量的列表。Data同样必须以JSON格式提供,且可能会随时间而变化,OPA会将其最新状态缓存在内存中;
  • Policy:指定了决策计算逻辑,对于给定的Data和Query,该计算逻辑会产生策略决定,即查询结果。 但OPA只是策略引擎,策略需要由用户自己定义;

策略引擎负责解释Policy中包含的规则,并基于Data和Query做出策略决策,并将决策结果格式化为JSON格式进行输出;

image-20220503105020480

OPA策略示例

如下策略定义了哪些客户端主机能够访问哪些path策略;

  • 默认策略为deny;
  • allow{}用于定义授权机制;
root@istio:~/ServiceMesh_in_Practise/security/envoy-spire-ext-authz# cat docker/web/opa/policies/policy.rego
package envoy.authz

import input.attributes.request.http as http_request
import input.attributes.source.address as source_address

default allow = false

allowed_paths = {"/hello", "/the/good/path", "/the/bad/path"}
allowed_local_paths = {"/good/backend", "/good/db"}

# allow access to the Web service from the subnet 172.28.0.0/16 for the allowed paths
allow {
    allowed_paths[http_request.path]
    http_request.method == "GET"
    net.cidr_contains("172.28.0.0/16", source_address.Address.SocketAddress.address)
}

# allow Web service access from localhost for locally allowed paths
allow {
    source_address.Address.SocketAddress.address == "127.0.0.1"
    allowed_local_paths[http_request.path]
    http_request.method == "GET"
}

https://github.com/open-policy-agent/opa-envoy-spire-ext-authz/blob/master/arch.png

演示示例

基于上图

root@istio:~/ServiceMesh_in_Practise/security/envoy-spire-ext-authz# ls
LICENSE  README.md  arch.png  build.sh  configure-spire.sh  docker  docker-compose.yml  gencerts.go  src  start-spire-agent.sh