10istio授权和认证2
现在,我们已经确认了这些身份是可信的,这就为下一步——访问控制奠定了基础。简单来说,知道身份是第一步,而定义这个身份能做什么,就是我们接下来要讨论的授权。这就像给每个员工发了工牌,但还得明确谁能进哪个办公室,对吧?这就是授权的核心。所以,授权到底是什么?它本质上就是一套规则,用来决定一个已经通过身份验证的实体,比如一个服务或者一个用户,是否有权限执行特定的操作。比如,读取某个配置文件、写入某个数据库,或者调用某个API接口。我们的目标很明确:谁可以做什么样的事情。Istio提供了AuthorizationPolicy这个强大的工具,它允许我们以非常灵活的方式定义这些规则,可以是针对整个服务网格,也可以是针对特定的命名空间,甚至可以精确到某个工作负载。这种能力对于构建微服务架构至关重要,因为它能有效限制攻击面。想象一下,如果一个服务的凭证被泄露,有了严格的授权策略,攻击者最多只能访问到该服务被明确允许访问的资源,而不是整个系统,这大大降低了潜在的损失。图中展示的场景就很形象:即使API网关的身份被窃取,由于它只被授权访问目录,无法访问论坛,攻击者的影响就被限制在了最小范围。那么,Istio是如何实现这么高效的授权呢?答案就在于服务代理。每个被Istio管理的工作负载都运行着一个Envoy代理。这个代理不仅仅负责网络通信,它还是一个强大的授权引擎。所有的授权策略,也就是我们定义的那些谁能做什么的规则,都直接配置在了这些代理上。这意味着,当请求到达时,代理可以立刻根据本地配置做出决策,是允许还是拒绝。这使得Istio的授权非常快,几乎是零延迟,因为决策是在网络边缘做出的,而不是在后端服务上。我们通过创建一个AuthorizationPolicy的Kubernetes资源,将策略应用到集群。然后,Istiod这个控制平面组件会自动把策略推送到相关的代理实例中。这种设计非常优雅,将安全决策和网络服务紧密结合,实现了高性能和高可用性。一个AuthorizationPolicy资源主要由三个关键部分组成。首先是selector,它决定了这条规则适用于哪些工作负载。你可以把它想象成一个标签过滤器,可以精确地指定只对某个特定的Pod、Deployment或者整个命名空间生效。其次是rules,这是一个列表,里面包含了多个具体的规则。只有当一个请求匹配了rules列表中的任何一个规则时,这条策略才会被激活。如果没有任何规则匹配,那么这条策略就不会起作用。最后是action,它定义了策略被激活后,请求应该如何处理,通常是允许或者拒绝。记住,action只在规则匹配成功之后才起作用。这三个部分共同构成了一个完整的授权策略,让我们可以非常灵活地定义访问控制逻辑。我们来深入看看规则的细节。rules字段是一个列表,里面可以包含多个独立的规则。每一条规则都描述了什么样的请求会触发这个规则。规则的核心在于from和to这两个字段。from字段定义了请求的来源。它可以是多种类型:principals,也就是源身份,通常是指另一个经过身份验证的Istio工作负载,这需要双方都启用mTLS;notPrincipals则是它的反义,表示来自非指定身份的请求;namespaces可以用来匹配源请求所在的命名空间,同样需要mTLS;ipBlocks则用于匹配源IP地址或网段。to字段则定义了请求的目标操作,比如请求的主机名、HTTP方法、请求路径等。还有一个when字段,它允许我们在规则匹配之后,再添加一些额外的条件判断。理解了这些字段,就掌握了构建复杂授权规则的基础。这里有一个非常重要的设计原则,也是很多新手容易踩坑的地方:默认拒绝。这意味着,如果你对某个工作负载应用了任何一条允许访问的策略,那么默认情况下,所有其他没有被明确允许的请求都会被拒绝。这听起来有点反直觉,但逻辑上是合理的。它迫使你主动思考并明确哪些访问是被允许的,而不是反过来想哪些是不被允许的。举个例子,我们创建了一个策略,只允许API Gateway访问斜杠api斜杠catalog路径。那么,如果你用curl去访问斜杠api斜杠catalog,会被允许;但如果你访问斜杠hello斜杠world,即使没有其他策略,也会被拒绝。这可能会让人困惑,为什么明明没有策略,访问却被拒绝了?这就是默认拒绝的体现。为了简化这种思考模式,避免每次都要问自己是不是忘了加允许策略,强烈推荐添加一个全局的拒绝策略作为兜底。为了更好地管理安全策略,我们通常会在 istio-system 命名空间下创建一个全局性的策略,叫做deny all。这个策略非常简单,它没有定义任何规则,spec 字段是空的。它的作用就是:如果没有任何其他的策略明确允许某个请求,那么这个请求就会被拒绝。这就像给整个系统设置了一个默认的防火墙,只允许白名单上的流量通过。这不仅提高了安全性,也让我们在编写具体的允许策略时,思路更加清晰:我们只需要关注哪些请求是允许的,而不需要担心遗漏了默认的拒绝。我们把这个 deny all 策略应用到集群中,然后尝试访问 API Gateway 的一个端点,就会得到 RBAC: access denied 的响应。这证明了我们的全局拒绝策略已经生效。记住,没有规则的策略就是拒绝所有,而一个空的策略规则则是允许所有。现在,我们想让来自 default 命名空间的 GET 请求能够访问到 apigateway 服务。我们尝试创建一个策略,使用 selector 指定目标是 apigateway,然后在 rules 中定义:from 是 default 命名空间,to 是 GET 方法。这看起来很合理,对吧?但这里有个问题:我们的 Sleep 服务,也就是发起请求的源头,它是一个遗留的、没有经过 Istio 代理注入的工作负载。这意味着它无法进行身份验证,也无法提供有效的 SPIFFE ID 或者命名空间信息。所以,当 API Gateway 代理收到请求时,它根本无法验证这个请求是否真的来自 default 命名空间。这就像你想检查一个人的身份证,但他根本就没带。所以,这个策略在当前情况下是无法正常工作的。面对 Sleep 服务无法认证的问题,我们有两种主要的应对方案。第一种,也是推荐的做法,是为 Sleep 服务注入服务代理,让它成为 Istio 管理的一部分。这样,我们就可以启用 mTLS,让 Sleep 服务能够与 API Gateway 进行双向认证,从而准确地获取并验证对方的身份和命名空间。这需要我们修改 Sleep 的部署配置,或者使用 istioctl kube inject 命令。不过,为了演示目的,我们暂时假设团队都在休假,无法立刻实施这个方案。那么,我们只能采取第二种方法:允许 apigateway 接受来自非认证用户的请求。怎么做呢?很简单,我们在定义的策略中,直接把 from 字段给删掉。这样,策略就变成了只关心目标操作,而不再关心来源。我们把这个策略只应用到 apigateway 服务上,这样 Catalog 服务仍然可以保持严格的认证要求。这就像给 API Gateway 开了一个后门,允许匿名访客进入。我们再次尝试从 Sleep 服务发起请求,这次应该能成功了吧?结果是,我们得到了一个错误:error calling Catalog service。这说明请求确实到达了 API Gateway,但 API Gateway 在尝试调用下游的 Catalog 服务时失败了。为什么会这样?还记得我们之前设置的全局 deny all 策略吗?正是这个策略在起作用!虽然 API Gateway 本身允许了来自 Sleep 的请求,但 API Gateway 本身并没有被允许去调用 Catalog 服务。因为全局策略是拒绝所有,除非有明确的允许策略,否则 API Gateway 不能访问任何其他服务。这再次提醒我们,deny all 策略虽然简化了管理,但也意味着我们需要为每一个需要被访问的服务都单独配置允许策略。这正是我们下一步要做的。现在,我们需要给 Catalog 服务添加一个允许策略,让它能够接收来自 API Gateway 的请求。由于 API Gateway 和 Catalog 都是 Istio 管理的工作负载,并且我们假设它们之间已经建立了 mTLS 通信,我们可以精确地指定 Catalog 只允许接收来自特定 Principal 的请求。这个 Principal 就是 API Gateway 的身份标识,通常格式是 cluster.local/ns/命名空间/sa/服务账户名。我们创建了一个策略,指定 Catalog 允许来自 apigateway 的 GET 请求。应用这个策略后,我们再次尝试从 Sleep 发起请求,这次请求能够成功到达 Catalog,并返回了正确的数据。这说明我们的授权策略已经生效。更重要的是,即使 Sleep 服务无法认证,通过 API Gateway 这个中间环节,我们仍然可以安全地控制对 Catalog 的访问,同时保证了服务间通信的安全。除了基于来源和目标,我们还可以使用 when 条件来让授权策略更加智能。比如,我们可能希望只允许来自某个认证源的用户,并且这个用户还拥有特定的权限声明。Istio 允许我们检查 JWT Token 中的 claims。上面这个例子,我们定义了一个策略,允许来自 auth at istioinaction dot io 的任何用户,但前提是他们的 JWT Token 中必须包含一个 key 为 group 的声明,并且这个声明的值是 admin。这样,我们就实现了基于用户角色的访问控制。反过来,我们也可以使用 notValues 来否定某些条件,比如只允许 group 不是 users 的用户。Istio 提供了非常丰富的属性来供我们使用,比如 request dot auth dot claims 代表用户声明,source dot principal 代表服务来源身份等等。这些属性的详细列表可以在官方文档中找到。这里要特别注意区分 Principal 和 Request Principal,前者是服务间认证的身份,后者是终端用户认证的身份。在定义 values 列表时,我们并不需要完全精确匹配。Istio 支持几种灵活的匹配表达式。比如,如果写 GET,那就只匹配 HTTP 方法为 GET 的请求。如果写斜杠api斜杠catalog星号,那么斜杠api斜杠catalog、斜杠api斜杠catalog斜杠1、斜杠api斜杠catalog斜杠products等等,只要是斜杠api斜杠catalog开头的路径都会匹配。类似地,如果写星号点istiioinaction点io,那么所有以 istiioinaction点io结尾的子域名,比如 login点istiioinaction点io,都会被匹配。还有一个非常有用的,就是用星号本身。如果 values 写成星号,它表示这个字段必须存在,但它的值可以是任何值。这在需要检查某个字段是否存在,而不需要关心具体值的情况下非常有用。这些灵活的匹配方式,让我们的授权策略能够适应各种复杂的业务场景。当多个策略应用于同一个工作负载时,它们是如何被评估的呢?Istio 采用了一套清晰的逻辑:首先,它会检查所有 DENY 策略。如果任何一个 DENY 策略的规则匹配了当前请求,那么这个请求就被直接拒绝,后续的评估就停止了。如果没有任何 DENY 策略匹配,那么 Istio 会检查所有 ALLOW 策略。如果任何一个 ALLOW 策略的规则匹配了当前请求,那么这个请求就被允许通过,同样,后续的评估也停止了。如果既没有 DENY 策略匹配,也没有 ALLOW 策略匹配,那么就会看是否存在一个兜底策略,比如我们前面提到的 deny all。如果存在兜底策略,就执行它。如果不存在兜底策略,或者兜底策略本身就是 DENY,那么最终的决定就是拒绝。此外,对于一个复杂的策略规则,from、to 和 when 这三个部分的条件是 AND 关系,必须同时满足。而一个 rules 列表中,多个 operation 是 OR 关系,满足任何一个 operation 就可以。when 条件内部的多个条件也是 AND 关系。前面我们讨论的主要是服务间通信的授权,但很多时候,我们的服务需要处理来自外部用户的请求,比如 Web API 或者前端应用。这时候就需要用户认证。业界最常用的标准就是 JWT 令牌。Istio 提供了强大的支持来处理用户认证,主要通过 RequestAuthentication 资源来实现。它可以帮助我们验证用户提供的 JWT 令牌是否有效,以及从中提取出用户的身份信息。将用户认证放在 Ingress Gateway 上进行,有几个好处。首先,可以提高性能,因为无效的请求在进入后端服务之前就被拦截了。其次,可以增强安全性,比如在 Ingress Gateway 处移除 JWT 令牌,防止它在后续的请求链中被意外泄露,从而降低被用于重放攻击的风险。当然,Istio 也在规划未来可能引入一个 Token Service,进一步优化这个过程。在深入 Istio 的配置之前,我们快速回顾一下 JWT 的基本概念。JWT 是一种开放标准,用于在网络应用环境间安全地传递声明信息。一个典型的 JWT 由三个部分组成,通过点号连接,然后进行 Base64 URL 编码。第一部分是 Header,声明了 Token 的类型和使用的签名算法。第二部分是 Payload,也就是我们常说的声明,里面包含了关于用户的各种信息,比如签发者 iss、主题 sub、过期时间 exp、签发时间 iat 等等。第三部分是 Signature,用于验证这个 Token 的完整性和真实性,防止被篡改。我们可以通过解码 Payload 来查看里面的具体信息,比如这个例子中的 token,它包含了 iss、sub、exp、iat,还有一个自定义的 group 声明,值为 user。这些声明是服务进行身份验证和授权决策的关键依据。那么,这些 JWT Token 是怎么来的,又是怎么被验证的呢?通常,一个认证服务器,比如 Keycloak 或者 Auth0,负责生成 JWT。这个服务器会使用一个私钥来对 Token 的 Header 和 Payload 进行签名,生成 Signature。同时,它会将对应的公钥放在一个叫做 JWKS 的端点上公开。当我们的服务收到一个带有 JWT 的请求时,它会先从这个 JWKS 端点获取公钥,然后用这个公钥去解密 JWT 的 Signature。解密后,服务会将解密得到的哈希值与 Header 和 Payload 的组合哈希值进行比较。如果两者一致,就说明这个 Token 是由可信的认证服务器签发的,里面的声明是可信的。这个过程保证了 Token 的真实性和完整性。既然要处理用户请求的认证,那么放在哪里最合适呢?Istio 推荐的做法是将用户认证和授权的逻辑放在 Ingress Gateway 上。为什么?主要有两个原因。第一,性能。如果在后端服务上做认证,每次请求都要经过认证,即使请求无效,也会浪费资源。如果在 Ingress Gateway 就把无效的请求挡掉,可以大大减轻后端服务的压力。第二,安全。我们可以在 Ingress Gateway 这个入口处,把用户请求头中的 JWT Token 移除掉,这样即使请求转发到了内部服务,也不会暴露用户的敏感信息。这就像在门口检查了证件,然后把证件收走了,内部人员就看不到你的证件了。此外,Istio 还在计划一个叫做 Token Service 的功能,它可以在 Ingress Gateway 生成一个短生命周期的、内部使用的 Token,替代原始的 JWT。这样做的好处是,即使这个内部 Token 泄露了,由于它的有效期很短,风险也大大降低。不过,这个功能目前还在计划中。现在我们来创建一个 RequestAuthentication 资源。这个资源的目标是告诉 Ingress Gateway 如何验证 JWT。我们指定它只作用于 istioingressgateway 这个服务,并且配置了一个 jwtRules,告诉它要验证由 issuer auth at istioinaction dot io 签发的 Token,并且提供了对应的 JWKS 公钥。这个 JWKS 是一个 JSON 对象,包含了用于验证签名的公钥。应用这个配置后,我们就可以测试不同类型的请求了。如果请求携带了有效的、由我们指定的 issuer 发行的 JWT,那么 Ingress Gateway 会验证通过,并且把 Token 中的 Payload 里的声明,比如 iss, sub, group 等,提取出来,存放到一个叫做 filter metadata 的地方。这个 filter metadata 是一个 Envoy 内部的数据结构,可以被后续的过滤器访问。如果请求没有携带 Token,或者 Token 无效,那么请求会被直接拒绝。这就是 RequestAuthentication 的核心作用:验证 JWT,并为后续的授权决策提供必要的用户信息。我们已经创建了 RequestAuthentication 资源,现在来验证一下。首先,我们用一个有效的 Token,也就是由 auth at istioinaction dot io 发行的,去访问 API 服务。可以看到,请求成功返回了 200 OK。这说明 RequestAuthentication 已经成功验证了 Token,并且允许请求继续。但是,如果我们使用一个由其他 issuer 发行的 Token,比如 old auth at istioinaction dot io,再去请求,就会得到 Jwt issuer is not configured 的错误。这说明 Ingress Gateway 只有在 RequestAuthentication 中明确配置了的 issuer,才会去验证对应的 Token。这符合我们的预期。刚才我们发现,即使没有提供 JWT,请求也能成功进入集群。这在某些场景下是合理的,比如访问静态资源。但在需要严格用户认证的 API 服务中,我们肯定希望拒绝没有 Token 的请求。怎么办呢?我们可以创建一个 AuthorizationPolicy,专门用来拒绝这些无 Token 的请求。这个策略的关键在于使用了 notRequestPrincipals。requestPrincipal 是由 RequestAuthentication 过滤器提取并存储在 filter metadata 中的一个字段,它通常由 JWT 的 iss 和 sub 声明组合而成。如果请求没有通过 RequestAuthentication 的验证,那么这个 requestPrincipal 字段就不会存在。所以,notRequestPrincipals: [""] 这个条件,就是匹配所有没有 requestPrincipal 的请求。我们将这个策略应用到 Ingress Gateway 服务上,然后再次尝试用 curl 发送一个没有 Authorization 头的请求。这次,我们得到了 RBAC: access denied 的响应。这说明我们的策略生效了,只有携带有效 JWT 的请求才能被允许通过。现在我们来实现更精细的权限控制:基于 JWT 中的声明。假设我们的用户分为普通用户和管理员,普通用户只能读取数据,管理员可以读写。我们创建了两个策略。第一个策略,allow all with jwt to apiserver,它允许所有来自 auth at istioinaction.io 的请求,并且只允许 GET 方法访问 apiserver 服务。第二个策略,allow mesh all ops admin,它允许所有来自 auth at istioinaction.io 的请求,但使用了 when 条件,要求 JWT 中必须包含一个 key 为 group 的声明,并且这个声明的值是 admin。这样,只有管理员用户才能执行所有操作。我们将这两个策略都应用到 Ingress Gateway 服务上。现在我们来验证一下这两个策略是否生效。我们先用一个普通用户,也就是 JWT 中 group 声明为 user 的 Token,去发送 GET 请求。请求成功返回了 200 OK。这说明普通用户可以读取数据。但是,如果用同一个普通用户 Token 去发送 POST 请求,就会得到 RBAC: access denied 的拒绝。这说明普通用户不能写入数据。接着,我们用一个管理员,也就是 JWT 中 group 声明为 admin 的 Token,去发送 POST 请求。这次请求也成功返回了 200 OK。这说明管理员可以执行所有操作。至此,我们已经实现了基于 JWT Claims 的用户分级授权。在我们讨论的整个过程中,无论是服务间认证还是用户认证,最终都会提取出一些关于请求的身份信息。这些信息,就是所谓的请求身份。它们是由 PeerAuthentication 和 RequestAuthentication 两个资源收集起来的,并且以一种叫做 filter metadata 的形式,传递给后续的 AuthorizationPolicy 进行决策。这个 filter metadata 里包含了哪些信息呢?主要包括:Principal 和 Namespace,它们来自 PeerAuthentication,代表了服务来源的身份;Request Principal 和 Request Authentication Claims,它们来自 RequestAuthentication,代表了终端用户的身份和声明。这些信息就像是给每个请求贴上了一个标签,告诉授权策略:这个请求是谁发起的,它代表了谁,以及它有哪些属性。有了这些信息,授权策略才能做出正确的决策。我们可以通过调整 Envoy 代理的日志级别来查看这些 filter metadata。我们使用 istioctl proxy-config log 命令,将日志级别设置为 filter:debug。然后,我们用一个管理员 Token 发起一个 POST 请求,触发请求。接着,我们查看 Ingress Gateway 的日志。在日志中,我们可以搜索 Saved Dynamic Metadata,这里会显示 Envoy 代理收集到的关于请求的所有信息。可以看到,request.auth.principal 字段被设置成了 iss/sub 的形式,request.auth.claims 字段则包含了我们 JWT Token 中的 iss, sub, iat, exp, group 等声明。这些信息正是 RequestAuthentication 过滤器提取并存储的。同样地,我们也可以查看服务间认证 Peer Authentication 收集的元数据。这次我们把日志级别调到 API Gateway 服务上。同样地,我们查看日志,搜索 Saved Dynamic Metadata。可以看到,这里记录了 source.principal 和 source.namespace 等信息。这些信息是由 Peer Authentication 过滤器在服务间通信时提取的,代表了请求来源服务的身份和命名空间。这再次证明了,无论是服务间通信还是用户请求,Istio 都能通过相应的机制,将身份验证的结果传递给后续的授权过滤器。最后,我们来总结一下一个请求在 Istio 服务网格中是如何一步步处理的。当一个用户请求到达 Ingress Gateway 时,它首先会经过 JWT 认证过滤器。这个过滤器会检查请求头中的 Authorization 字段,看是否包含有效的 JWT。如果验证通过,它会从 JWT 中提取出 Payload 里的声明,比如 iss, sub, group 等,然后把这些信息放到 filter metadata 里。接着,如果请求需要进一步转发到后端服务,比如 API Gateway,那么请求还会经过 Peer 认证过滤器。这个过滤器会检查请求来源是否符合 PeerAuthentication 的要求,比如是否需要 mTLS,如果需要,它会验证来源服务的身份,同样会将来源服务的身份信息,比如 source.principal, source.namespace,也放到 filter metadata 里。最后,当请求到达目标服务时,它会经过授权过滤器。这个过滤器会读取前面两个过滤器传递下来的 filter metadata,然后根据预先定义的 AuthorizationPolicy 策略,来决定是否允许这个请求。整个流程清晰地展示了身份验证和授权是如何协同工作的。今天我们深入探讨了 Istio 的授权机制,从服务间通信的严格控制,到基于 JWT 的用户认证和授权,再到如何利用这些机制实现精细化的访问控制。我们学习了如何使用 PeerAuthentication、AuthorizationPolicy 和 RequestAuthentication 这三个核心资源来构建安全策略。理解了 Principal、Request Principal、Filter Metadata 这些关键概念,以及 DENY 和 ALLOW 策略的评估顺序,还有如何基于来源、目标、条件和 JWT Claims 来定义复杂的规则。希望今天的讲解能帮助大家更好地理解和运用 Istio 的安全功能,为构建更安全、更可靠的服务网格打下坚实的基础。