Skip to content

第38讲:如何实现服务访问控制与双向TLS

安全相关的功能是应用开发中重要的一环,在第 25 课时中,我对与安全相关的身份认证、授权管理和审计等内容进行了基本的介绍。在服务网格出现之前,这些功能都需要由应用来实现;服务网格出现之后,一部分与安全相关的功能从应用中抽离出来,由底层的平台来实现。

本课时将对 Istio 提供的安全相关的功能进行介绍。

Istio 提供了声明式的安全管理策略,服务代理负责在运行时应用安全控制相关的策略,开发人员只需要以 Kubernetes 自定义资源声明。声明式的方式降低了配置的复杂度,同时也更容易在运行时动态调整策略。

身份标识

身份标识用来区分不同的实体,在进行认证和授权时,都需要使用这个身份标识。服务网格中的身份标识分成服务标识用户标识两类。

服务标识用来确定服务网格中的参与者,代表请求的发起者和响应者。在 Kubernetes 上,Istio 使用服务账户(Service Account)来作为身份标识。每个服务在部署时,都会创建各自独立的服务账户。

用户标识与具体的应用相关,一般以用户 ID、用户名或 Email 地址来表示。服务代理可以从请求中抽取出用户标识,比较典型的做法是在 JWT 令牌的 sub 申明中保存用户标识。

身份认证

Istio 中的身份认证分为对等认证(Peer Authentication)请求认证(Request Authentication)。对等认证指的是服务之间的身份认证,而请求认证指的是验证请求中包含的用户的身份。

策略

Istio 使用自定义资源来表示身份认证策略。策略有不同的应用范围,根据策略资源所在的名称空间和 selector 字段的值来划分。具体的范围说明如下表所示:

范围说明
服务网格声明在 Istio 名称空间中,并且没有 selector 字段或 selector 字段为空
名称空间声明在非 Istio 名称空间中,并且没有 selector 字段或 selector 字段为空
工作负载声明在非 Istio 名称空间中,并且 selector 字段不为空

对等认证策略只能有一个服务网格范围的策略,每个名称空间只能有一个该名称空间对应的策略,每个工作负载也只能有一个匹配的策略。对于同一个范围,如果存在多个匹配的策略,会应用创建时间最早的策略。

对请求认证策略来说,Istio 可以把多个匹配的策略合并成一个,这一点与对等认证策略是不同的。不过,推荐的做法是只创建一个服务网格范围的策略,并为每个名称空间创建一个对应的策略,这样可以避免多个策略合并时带来的潜在的配置问题。

在确定工作负载所应用的策略时,如果存在多个匹配的策略,范围最具体的策略的优先级最高。范围的优先级顺序依次为工作负载、名称空间、服务网格。

双向 TLS

双向 TLS 可以验证服务双方的身份,同时对服务之间的传输进行加密。每个服务的身份由 X.509 证书来标识,Istio 则提供密钥和证书的管理。Istio 有自己的证书权威机构,可以为每个服务代理签发证书。在 Istio 的服务代理边车容器的运行过程中,Istio 的代理程序与证书权威机构进行交互,并把密钥和签发的证书通过 Envoy 的密钥发现服务,提供给 Envoy。

由于 Istio 提供了对证书的管理,启用双向 TLS 就变得很简单。

对等认证通过 PeerAuthentication 资源来声明。下表列出了 PeerAuthentication 资源的字段:

字段说明
selector应用该策略的工作负载的选择器
mtls双向 TLS 的配置
portLevelMtls对指定的端口应用不同的双向 TLS 配置

在 mtls 字段中,通过属性 mode 来配置双向 TLS 的工作模式。可以使用的模式如下表所示:

模式说明
PERMISSIVE可以同时使用 TLS 和明文消息
STRICT只能使用 TLS 消息
DISABLE禁用双向 TLS
UNSET从父策略中继承。如果没有的话,相当于 PERMISSIVE

在上表的 4 种模式中,推荐使用 STRICT 模式,因为只能使用 TLS,安全性最高。PERMISSIVE 模式的主要作用是在迁移过程中,最终的目标仍然是使用 STRICT 模式。从明文模式迁移到双向 TLS 时,可能有一部分服务的使用者暂时还不支持 TLS。因此,为了保证服务不中断,服务提供者可以先设置为 PERMISSIVE 模式,等迁移全部完成之后,再设置为 STRICT 模式。如果没有做任何设置,PERMISSIVE 是默认值,这是为了保证最大程度的兼容性。

下面代码中的 PeerAuthentication 资源对名称空间 happyride 启用了 STRICT 模式的双向 TLS。

yaml
apiVersion: security.istio.io/v1beta1 
kind: PeerAuthentication 
metadata: 
  name: default 
  namespace: happyride 
spec: 
  mtls: 
    mode: STRICT

下面代码中的 PeerAuthentication 资源只对乘客管理服务启用了双向 TLS 的 STRICT 模式,通过 selector 来指定工作负载。DestinationRule 资源的作用是控制从服务代理发送到乘客管理服务的流量。ISTIO_MUTUAL 模式的含义是启用双向 TLS,但使用的是 Istio 自动生成的证书。

yaml
apiVersion: security.istio.io/v1beta1 
kind: PeerAuthentication 
metadata: 
  name: passenger-service 
  namespace: happyride 
spec: 
  selector: 
    matchLabels: 
      app.kubernetes.io/name: passenger-service 
  mtls: 
    mode: STRICT 
--- 
apiVersion: networking.istio.io/v1beta1 
kind: DestinationRule 
metadata: 
  name: passenger-service-mtls 
spec: 
  host: passenger-service.happyride.svc.cluster.local 
  trafficPolicy: 
    tls: 
      mode: ISTIO_MUTUAL

为了测试双向TLS的效果,我们可以创建一个部署,并且禁用 Istio 的边车容器的注入,也就是没有服务代理运行。当在这个部署的 Pod 的容器中访问乘客管理服务时,会出现错误。这是因为没有服务代理来自动建立双向 TLS 连接。下面的代码给出了使用 curl 访问的命令和结果。

java
$ curl -i http://passenger-service/actuator/health/liveness 
curl: (56) Recv failure: Connection reset by peer

请求认证

Istio 通过 JWT 来进行请求认证,可以与 OpenID Connect 提供者进行集成。这些提供者可以运行在集群内部,也可以是第三方的在线服务。

请求认证通过 RequestAuthentication 资源来声明。下表给出了 RequestAuthentication 资源中的字段:

字段说明
selector应用该策略的工作负载的选择器
jwtRules验证 JWT 的规则的列表

每个 JWT 令牌都需要通过验证,确保没有被篡改,这是对 JWT 令牌最基本的要求。除此之外,验证通过的 JWT 令牌中的申明的值还需要满足一些条件。jwtRules 字段中的 JWT 规则对象用来配置 JWT 令牌的提取方式和验证条件。 JWT 规则的字段如下表所示:

字段说明
issuerJWT 令牌的签发者需要满足的条件
audiencesJWT 令牌的接收者需要满足的条件
jwksUri验证 JWT 签名的公钥地址
jwks验证 JWT 签名的公钥
fromHeaders提取 JWT 令牌的 HTTP 头的名称和需要去除的前缀
fromParams提取 JWT 令牌的查询参数的名称
outputPayloadToHeader把验证通过的 JWT 令牌中的载荷以 HTTP 头的形式发送到目标服务
forwardOriginalToken在发送到目标服务时,保留原始的 JWT 令牌

在上表的字段中,issuer 和 audiences 表示的是 JWT 令牌应该满足的条件,分别对应于令牌中的 iss 和 aud 申明。如果指定了 issuer 字段的值,那么该字段的值会被用来验证请求中的 JWT 令牌的中的 iss 申明,audiences 也是相似的用法。

下面通过具体的示例来说明请求认证的用法。本课时中以 Keycloak 来进行说明。Keycloak 是开源的身份认证和访问控制软件,由 RedHat 提供支持。Keycloak 的优势在于可以部署在集群内部。如果不希望使用第三方的在线服务,Keycloak 是一个不错的选择。

首先要做的是在 Kubernetes 上部署 Keycloak。Keycloak 提供了容器镜像,只需要在 Kubernetes 上创建相应的部署和服务即可,也可以使用 Helm 来安装。

Keycloak 部署运行之后,通过 Keycloak 的界面可以创建新的用户和客户端。这里使用的是默认的领域 master,客户端表示的是进行访问的实体。下图是标识符为 web 的客户端的创建页面。在客户端的设置中,访问类型要设置为 confidential。

在客户端的凭据标签页中,可以查看产生的客户端的密钥,如下图所示:

在正常的流程中,当用户在应用界面登录之后,应用后台通过调用 Keycloak 的 API 来获取到表示当前用户身份的 JWT 令牌,并发送给客户端。客户端使用该 JWT 令牌进行后续的请求。本课时中只对与 Keycloak 交互的 HTTP 请求进行介绍。

Keycloak 中获取令牌的 API 的路径是 /auth/realms/ /protocol/openid-connect/token,把 realm 变量替换成实际使用的领域。调用 API 时需要提供下表中给出的参数。

参数说明
client_id客户端 IDweb
client_secret客户端密钥Keycloak 自动生成
grant_type授权类型password
scope作用域openid
username用户名创建的用户名
password密码创建的用户的密码

该 API 的返回值是一个 JSON 对象,其中的 id_token 字段中包含了用户的 JWT 令牌。下图给出了 API 调用的请求和响应的示例。

下面的代码给出了 Keycloak 产生的 JWT 令牌的示例,从中可以看到 iss 和 aud 申明的值。

json
{ 
  "exp": 1595300432, 
  "iat": 1595300372, 
  "auth_time": 0, 
  "jti": "2ce9ea7d-c78b-49cd-ab63-61a22830f379", 
  "iss": "http://happyride.com/auth/realms/master", 
  "aud": "web", 
  "sub": "29cf92e9-69b3-4142-9f4d-98cdc1cee079", 
  "typ": "ID", 
  "azp": "web", 
  "session_state": "22f18793-69fd-471b-ac33-dc21f2791029", 
  "acr": "1", 
  "email_verified": false, 
  "preferred_username": "test" 
}

在下面的代码中,RequestAuthentication 资源表示对乘客界面的 GraphQL API 服务的请求认证。在 jwtRules 字段中,issuer 的值表示需要匹配的 JWT 令牌的签发者;audiences 的值表示需要匹配的 JWT 令牌的接收者,对应于 Keycloak 中的客户端;jwksUri 则表示获取 JWT 公钥的 URI,由 Keycloak 提供------该公钥用来验证 JWT 令牌的合法性。当 JWT 令牌的验证通过之后,令牌中的 sub 申明的值会作为请求的主体,可以在后续的访问控制中使用。

yaml
apiVersion: security.istio.io/v1beta1 
kind: RequestAuthentication 
metadata: 
  name: passenger-api-graphql 
spec: 
  selector: 
    matchLabels: 
      app.kubernetes.io/name: passenger-api-graphql 
  jwtRules: 
    - issuer: "http://happyride.com/auth/realms/master" 
      audiences: 
        - web 
      jwksUri: http://keycloak.happyride.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/certs

在访问 GraphQL API 时,需要把从 Keycloak API 中获取的 JWT 令牌,以 HTTP 头 Authorization 来传递,并以"Bearer "作为前缀。如果以其他的 HTTP 头来传递 JWT 令牌,则需要通过 JWT 规则对象中的 fromHeaders 字段来配置。

访问控制

Istio 的访问控制由 AuthorizationPolicy 资源来表示,下表给出了 AuthorizationPolicy 资源中定义的字段:

字段说明
selector应用该策略的工作负载的选择器。当选择器为空时,策略应用于当前名称空间的全部工作负载
rules匹配请求的规则的列表
action当请求匹配时的动作

AuthorizationPolicy 资源中最复杂的是规则的声明。每个规则由 3 个部分组成,分别是来源、操作和条件,与这 3 个部分对应的字段分别是 from、to 和 when。规则的每个组成部分都是一个列表,只要这 3 个列表中都至少有一个元素匹配成功,那么该规则就匹配成功。在进行匹配时,匹配的目标是请求中的不同属性,而匹配的方式则包括完全匹配和使用"*"的通配符匹配。

下表给出了来源中可以进行正向匹配的字段。对于每个字段,可以添加 not 前缀来使用逆向匹配。比如,notPrincipals 表示的就是身份标识不能匹配的值。所有这些字段的值都是字符串列表。

字段匹配的属性说明
principalssource.principal来源的身份标识
requestPrincipalsrequest.auth.principal请求的身份标识
namespacessource.namespace来源所在的名称空间
ipBlockssource.ip来源所在的 IP 块,可以使用单个 IP 地址或 CIDR

下表给出了操作中可以进行正向匹配的字段。与来源中的字段相同的是,这些字段同样可以添加 not 前缀来使用逆向匹配。

字段匹配的属性说明
hostsrequest.host主机名
portsdestination.port端口号
methodsrequest.methodHTTP 方法
pathsrequest.url_path路径

条件表示需要检查的属性。下表给出了相关的字段:

字段说明
keyIstio 属性的名称
values属性允许的值
notValues属性不允许的值

条件中可以检查的属性很多,常用的如下表所示:

属性说明
request.headers请求的 HTTP 头中的值,头的名称包含在中括号中
request.auth.claims请求中的 JWT 令牌中的申明,申明的名称包含在中括号中

在下面代码的 AuthorizationPolicy 资源中,when 字段使用 request.headers 属性作为条件,要求请求中 HTTP 头 x-version 的值必须是 v1 或 v2。

yaml
apiVersion: security.istio.io/v1beta1 
kind: AuthorizationPolicy 
metadata: 
  name: address-service 
spec: 
  action: ALLOW 
  selector: 
    matchLabels: 
      app.kubernetes.io/name: address-service 
  rules: 
    - when: 
        - key: request.headers[x-version] 
          values: 
            - v1 
            - v2

在添加了上面的 AuthorizationPolicy 资源之后,在访问时需要添加值为 v1 或 v2 的 HTTP 头 x-version,如下面的代码所示,否则的话会出现 HTTP 403 错误。

java
curl -H "x-version: v2" -i http://address-service/actuator/health/liveness

action 字段的可选值有 ALLOW 和 DENY 两种,分别表示允许和拒绝。如果不指定 action 字段,则默认值为 ALLOW。

下面代码中的 AuthorizationPolicy 资源限制了只允许 GraphQL API 服务来访问地址管理服务,并且允许的 HTTP 方法只有 GET 和 POST。

yaml
apiVersion: security.istio.io/v1beta1 
kind: AuthorizationPolicy 
metadata: 
  name: address-service 
spec: 
  action: ALLOW 
  selector: 
    matchLabels: 
      app.kubernetes.io/name: address-service 
  rules: 
    - from: 
        - source: 
            principals: 
              - "cluster.local/ns/happyride/sa/passenger-api-graphql" 
      to: 
        - operation: 
            methods: 
              - GET 
              - POST

在 GraphQL API 服务器启用了请求认证之后,需要添加相对应的访问控制。下面代码中的 AuthorizationPolicy 资源声明了请求中必须包含用户主体,通过 notRequestPrincipals 字段来进行匹配,如果匹配成功,则说明请求中不包含用户主体,所应用的动作是 DENY。

yaml
apiVersion: security.istio.io/v1beta1 
kind: AuthorizationPolicy 
metadata: 
  name: passenger-api-graphql 
spec: 
  selector: 
    matchLabels: 
      app.kubernetes.io/name: passenger-api-graphql 
  action: DENY 
  rules: 
    - from: 
        - source: 
            notRequestPrincipals: ["*"]

当访问被拒绝之后,HTTP 请求会返回 403 错误,并且消息为 RBAC: access denied。

总结

身份认证和访问控制是应用安全机制的基本组成部分。Istio 可以简化与安全相关的配置,并减少开发的工作量。通过本课时的学习,你可以了解到如何通过 Istio 来实现双向 TLS 和基于 JWT 的请求认证,以及如何添加访问控制策略。