基础认证策略

此任务涵盖启用、配置和使用 Istio 身份验证策略时可能需要执行的主要活动。认证概述中可以了解更多相关的基本概念。

开始之前

  • 理解 Istio 认证策略和相关的 双向 TLS 认证概念。

  • 拥有一个安装好 Istio 的 Kubernetes 集群,并且禁用全局双向 TLS(可使用安装步骤中提供的示例配置 install/kubernetes/istio.yaml,或者使用 Helm 设置 global.mtls.enabled 为 false)。

安装

为了演示,需要创建两个命名空间 foobar,并且在两个空间中都部署带有 sidecar 的 httpbin 应用和 sleep 应用。同时运行另外一份不带有 Sidecar 的 httpbin 和 sleep 应用(为了保证独立性, 在 legacy 命名空间中运行它们)。如果您在尝试任务时想要使用相同的示例,运行以下内容:

ZipZipZipZipZipZip
$ kubectl create ns foo
$ kubectl apply -f <(istioctl kube-inject -f @samples/httpbin/httpbin.yaml@) -n foo
$ kubectl apply -f <(istioctl kube-inject -f @samples/sleep/sleep.yaml@) -n foo
$ kubectl create ns bar
$ kubectl apply -f <(istioctl kube-inject -f @samples/httpbin/httpbin.yaml@) -n bar
$ kubectl apply -f <(istioctl kube-inject -f @samples/sleep/sleep.yaml@) -n bar
$ kubectl create ns legacy
$ kubectl apply -f @samples/httpbin/httpbin.yaml@ -n legacy
$ kubectl apply -f @samples/sleep/sleep.yaml@ -n legacy

通过从任意客户端(例如 sleep.foosleep.barsleep.legacy)向任意服务端(httpbin.foohttpbin.barhttpbin.legacy)发送 HTTP 请求(可以使用 curl 命令)来验证以上设置。所有请求都应该成功进行并且返回的 HTTP 状态码为 200。

以下是一个从 sleep.barhttpbin.foo 可达性的检查命令示例:

$ kubectl exec $(kubectl get pod -l app=sleep -n bar -o jsonpath={.items..metadata.name}) -c sleep -n bar -- curl http://httpbin.foo:8000/ip -s -o /dev/null -w "%{http_code}\n"
200

以下单行命令可以方便对所有客户端和服务端的组合进行检查:

$ for from in "foo" "bar" "legacy"; do for to in "foo" "bar" "legacy"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
sleep.foo to httpbin.foo: 200
sleep.foo to httpbin.bar: 200
sleep.foo to httpbin.legacy: 200
sleep.bar to httpbin.foo: 200
sleep.bar to httpbin.bar: 200
sleep.bar to httpbin.legacy: 200
sleep.legacy to httpbin.foo: 200
sleep.legacy to httpbin.bar: 200
sleep.legacy to httpbin.legacy: 200

重要的是,验证系统目前没有认证策略:

$ kubectl get policies.authentication.istio.io --all-namespaces
No resources found.
$ kubectl get meshpolicies.authentication.istio.io
No resources found.

最后要进行的验证是,确保没有为示例服务定义匹配的目标规则。一个验证方法是:获取现存目标规则,查看 host: 字段值是否匹配示例服务的 FQDN。例如:

$ kubectl get destinationrules.networking.istio.io --all-namespaces -o yaml | grep "host:"
    host: istio-policy.istio-system.svc.cluster.local
    host: istio-telemetry.istio-system.svc.cluster.local

为网格中的所有服务启用双向 TLS 认证

你可以提交如下网格范围的认证策略(MeshPolicy和目的地规则为网格中所有服务启用双向 TLS 认证:

$ kubectl apply -f - <<EOF
apiVersion: "authentication.istio.io/v1alpha1"
kind: "MeshPolicy"
metadata:
  name: "default"
spec:
  peers:
  - mtls: {}
EOF

此策略指定网格中的所有工作负荷仅接受使用 TLS 的加密请求。如您所见,此身份验证策略的类型是 MeshPolicy,这种策略的生效范围是整个网格内的所有服务,因此名称必须是 default,另外也不包含 targets 字段。

此时,只有接收方被配置为使用双向 TLS。如果在 Istio 服务(即那些带有 Sidecar 的服务)之间运行 curl 命令,所有请求都将失败并显示 503 错误代码,原因是客户端仍在使用明文请求。

$ for from in "foo" "bar"; do for to in "foo" "bar"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
sleep.foo to httpbin.foo: 503
sleep.foo to httpbin.bar: 503
sleep.bar to httpbin.foo: 503
sleep.bar to httpbin.bar: 503

要配置客户端也使用双向 TLS,就需要设置目标规则。可以使用多个目标规则,一个一个的为每个适用的服务(或命名空间)进行配置;然而在规则中使用 * 符号来匹配所有服务会更方便,这样也就跟网格范围的认证策略一致了。

$ kubectl apply -f - <<EOF
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "default"
  namespace: "default"
spec:
  host: "*.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
EOF

除了认证场合之外,目标规则还有其它方面的应用,例如金丝雀部署。但是所有的目标规则都适用相同的优先顺序。因此,如果一个服务需要配置其它目标规则(例如配置负载均衡),那么新规则定义中必须包含类似的 TLS 块来定义 ISTIO_MUTUAL 模式,否则它将覆盖网格或命名空间范围的 TLS 设置并禁用 TLS。

如上所述重新运行测试命令,您将看到 Istio 服务之间的所有请求现已成功完成:

$ for from in "foo" "bar"; do for to in "foo" "bar"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
sleep.foo to httpbin.foo: 200
sleep.foo to httpbin.bar: 200
sleep.bar to httpbin.foo: 200
sleep.bar to httpbin.bar: 200

从非 Istio 服务请求到 Istio 服务

非 Istio 服务,例如 sleep.legacy 没有 Sidecar,因此它无法启动与 Istio 服务通信所需的 TLS 连接。因此从 sleep.legacyhttpbin.foohttpbin.bar 的请求将失败:

$ for from in "legacy"; do for to in "foo" "bar"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
sleep.legacy to httpbin.foo: 000
command terminated with exit code 56
sleep.legacy to httpbin.bar: 000
command terminated with exit code 56
sleep.legacy to httpbin.legacy: 200

这一结果是符合预期的,但遗憾的是,如果不降低这些服务的身份验证要求,就无法完成这种访问。

从 Istio 服务请求非 Istio 服务

如果从 sleep.foo(或 sleep.bar)向 httpbin.legacy 发送请求,也会看到请求失败,因为 Istio 按照我们的指示配置客户端目标规则使用双向 TLS,但 httpbin.legacy 没有 Sidecar,所以它无法处理它。

$ for from in "foo" "bar"; do for to in "legacy"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
sleep.foo to httpbin.legacy: 503
sleep.bar to httpbin.legacy: 503

要解决此问题,我们可以添加目标规则来覆盖 httpbin.legacy 的 TLS 设置。例如:

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
 name: "httpbin-legacy"
spec:
 host: "httpbin.legacy.svc.cluster.local"
 trafficPolicy:
   tls:
     mode: DISABLE
EOF

从 Istio 服务请求到 Kubernetes API Server

Kubernetes API Server 没有 Sidecar,因此来自 sleep.foo 等 Istio 服务的请求会失败,出现像请求非 Istio 服务时同样的问题而失败。

$ TOKEN=$(kubectl describe secret $(kubectl get secrets | grep default | cut -f1 -d ' ') | grep -E '^token' | cut -f2 -d':' | tr -d '\t')
kubectl exec $(kubectl get pod -l app=sleep -n foo -o jsonpath={.items..metadata.name}) -c sleep -n foo -- curl https://kubernetes.default/api --header "Authorization: Bearer $TOKEN" --insecure -s -o /dev/null -w "%{http_code}\n"
000
command terminated with exit code 35

同样,我们可以通过覆盖 API 服务器的目标规则来更正此问题( kubernetes.default

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
 name: "api-server"
spec:
 host: "kubernetes.default.svc.cluster.local"
 trafficPolicy:
   tls:
     mode: DISABLE
EOF

重新运行上面的测试命令,确认在添加规则后能够成功返回 200:

$ TOKEN=$(kubectl describe secret $(kubectl get secrets | grep default | cut -f1 -d ' ') | grep -E '^token' | cut -f2 -d':' | tr -d '\t')
$ kubectl exec $(kubectl get pod -l app=sleep -n foo -o jsonpath={.items..metadata.name}) -c sleep -n foo -- curl https://kubernetes.default/api --header "Authorization: Bearer $TOKEN" --insecure -s -o /dev/null -w "%{http_code}\n"
200

清理第 1 部分

删除在上述步骤中创建的策略和目标规则:

$ kubectl delete meshpolicy default
$ kubectl delete destinationrules default httpbin-legacy api-server

为每个命名空间或服务启用双向 TLS

除了为整个网格指定身份验证策略之外,Istio 还允许您为特定命名空间或服务指定策略。一个 命名空间范围的策略优先于网格范围的策略,而特定于服务的策略仍具有更高的优先级。

命名空间范围的策略

下面的示例显示了为命名空间 foo 中的所有服务启用双向 TLS 的策略。正如你所看到的,它使用的类别是 Policy 而不是 MeshPolicy,并指定了一个命名空间,在本例中为 foo。如果未指定命名空间值,则策略将应用于默认命名空间。

$ kubectl apply -f - <<EOF
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "default"
  namespace: "foo"
spec:
  peers:
  - mtls: {}
EOF

添加相应的目的地规则:

$ kubectl apply -f - <<EOF
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "default"
  namespace: "foo"
spec:
  host: "*.foo.svc.cluster.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
EOF

由于这些策略和目的地规则只对命名空间 foo 中的服务有效,你应该看到只有从不带 Sidecar 的客户端(sleep.legacy)到 httpbin.foo 的请求会出现失败。

$ for from in "foo" "bar" "legacy"; do for to in "foo" "bar" "legacy"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
sleep.foo to httpbin.foo: 200
sleep.foo to httpbin.bar: 200
sleep.foo to httpbin.legacy: 200
sleep.bar to httpbin.foo: 200
sleep.bar to httpbin.bar: 200
sleep.bar to httpbin.legacy: 200
sleep.legacy to httpbin.foo: 000
command terminated with exit code 56
sleep.legacy to httpbin.bar: 200
sleep.legacy to httpbin.legacy: 200

特定于服务的策略

也可以为某个特定的服务设置认证策略和目的地规则。执行以下命令,只为 httpbin.bar 服务新增一项策略。

$ cat <<EOF | kubectl apply -n bar -f -
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "httpbin"
spec:
  targets:
  - name: httpbin
  peers:
  - mtls: {}
EOF

同时增加目的地规则:

$ cat <<EOF | kubectl apply -n bar -f -
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "httpbin"
spec:
  host: "httpbin.bar.svc.cluster.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
EOF

同样地,运行上文中提供的测试命令。和预期一致,从 sleep.legacyhttpbin.bar 的请求因为同样的原因开始出现失败。

...
sleep.legacy to httpbin.bar: 000
command terminated with exit code 56

如果在命名空间 bar 中还存在其他服务,我们会发现目标为这些服务的流量不会受到影响。验证这一行为有两种方法:一种是加入更多服务;另一种是把这一策略限制到某个端口。这里我们展示第二种方法:

$ cat <<EOF | kubectl apply -n bar -f -
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "httpbin"
spec:
  targets:
  - name: httpbin
    ports:
    - number: 1234
  peers:
  - mtls: {}
EOF

同时对目的地规则做出相应的改变:

$ cat <<EOF | kubectl apply -n bar -f -
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "httpbin"
spec:
  host: httpbin.bar.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE
    portLevelSettings:
    - port:
        number: 1234
      tls:
        mode: ISTIO_MUTUAL
EOF

新的策略只作用于 httpbin 服务的 1234 端口上。结果是,双向 TLS 在端口 8000 上(又)被禁用并且从 sleep.legacy 发出的请求会恢复工作。

$ kubectl exec $(kubectl get pod -l app=sleep -n legacy -o jsonpath={.items..metadata.name}) -c sleep -n legacy -- curl http://httpbin.bar:8000/ip -s -o /dev/null -w "%{http_code}\n"
200

策略优先级

服务级策略是优先于命名空间级策略的,为了证实这一行为,可以新建一条策略,禁止 httpbin.foo 的双向 TLS 设置;而此时网格中已经创建了一个命名空间级的策略,该策略为 foo 命名空间中的所有服务启用了双向 TLS,下面就来观察一下从 sleep.legacyhttpbin.foo 的请求是如何失败的:

$ cat <<EOF | kubectl apply -n foo -f -
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "overwrite-example"
spec:
  targets:
  - name: httpbin
EOF

另外添加对应的目的地规则:

$ cat <<EOF | kubectl apply -n foo -f -
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "overwrite-example"
spec:
  host: httpbin.foo.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE
EOF

重新从 sleep.legacy 发送请求,我们应当看到请求成功返回的状态码(200),表明服务级的策略覆盖了命名空间层级的策略。

$ kubectl exec $(kubectl get pod -l app=sleep -n legacy -o jsonpath={.items..metadata.name}) -c sleep -n legacy -- curl http://httpbin.foo:8000/ip -s -o /dev/null -w "%{http_code}\n"
200

清理第 2 部分

删除在上述步骤中创建的策略和目标规则:

$ kubectl delete policy default overwrite-example -n foo
$ kubectl delete policy httpbin -n bar
$ kubectl delete destinationrules default overwrite-example -n foo
$ kubectl delete destinationrules httpbin -n bar

设置终端用户认证

要验证这一功能,首先要有一个有效的 JWT。JWT 必须与本例中的 JWKS 端点相匹配。本例中我们使用 Istio 代码库中的 JWT test 以及 JWKS endpoint 进行演示。

另外为了方便起见,我们使用 ingressgateway 开放了 httpbin.foo 服务(这部分内容可以参看控制 Ingress 流量任务中的介绍)。

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
  namespace: foo
spec:
  selector:
    istio: ingressgateway # use Istio default gateway implementation
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
EOF
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
  namespace: foo
spec:
  hosts:
  - "*"
  gateways:
  - httpbin-gateway
  http:
  - route:
    - destination:
        port:
          number: 8000
        host: httpbin.foo.svc.cluster.local
EOF

获取入口 IP:

$ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

并且运行查询测试:

$ curl $INGRESS_HOST/headers -s -o /dev/null -w "%{http_code}\n"
200

现在添加一条策略,强制 httpbin.org 使用终端用户 JWT。下一个命令假设没有为 httpbin.org 指定服务级策略(也就是说成功运行了上面清理第 2 部分的内容)。可以用命令 kubectl get policies.authentication.istio.io -n foo 来验证这一假设:

$ cat <<EOF | kubectl apply -n foo -f -
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "jwt-example"
spec:
  targets:
  - name: httpbin
  origins:
  - jwt:
      issuer: "testing@secure.istio.io"
      jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.2/security/tools/jwt/samples/jwks.json"
  principalBinding: USE_ORIGIN
EOF

使用上面小节中同样的 curl 命令进行测试时会返回 401 错误状态码,这是因为服务端需要 JWT 进行认证但请求端并没有提供:

$ curl $INGRESS_HOST/headers -s -o /dev/null -w "%{http_code}\n"
401

用下面的 curl 命令生成 Token 并附加在请求中,然后执行请求就会返回成功信息:

$ TOKEN=$(curl https://raw.githubusercontent.com/istio/istio/release-1.2/security/tools/jwt/samples/demo.jwt -s)
$ curl --header "Authorization: Bearer $TOKEN" $INGRESS_HOST/headers -s -o /dev/null -w "%{http_code}\n"
200

要观察 JWT 验证的其他方面,请使用脚本 gen-jwt.py 生成新的令牌以测试不同的发行者、受众和到期日期等。例如下面的命令创建一个 5 秒后到期的 Token。Istio 首先成功通过该令牌的验证请求,但在 5 秒后就会拒绝这一 Token 了:

ZipZip
$ TOKEN=$(@security/tools/jwt/samples/gen-jwt.py@ @security/tools/jwt/samples/key.pem@ --expire 5)
$ for i in `seq 1 10`; do curl --header "Authorization: Bearer $TOKEN" $INGRESS_HOST/headers -s -o /dev/null -w "%{http_code}\n"; sleep 1; done
200
200
200
200
200
401
401
401
401
401

使用双向 TLS 进行最终用户身份验证

最终用户身份验证和双向 TLS 可以一起使用。修改上面的策略以定义双向 TLS 和最终用户 JWT 身份验证:

$ cat <<EOF | kubectl apply -n foo -f -
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "jwt-example"
spec:
  targets:
  - name: httpbin
  peers:
  - mtls: {}
  origins:
  - jwt:
      issuer: "testing@secure.istio.io"
      jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.2/security/tools/jwt/samples/jwks.json"
  principalBinding: USE_ORIGIN
EOF

并添加目标规则:

$ kubectl apply -f - <<EOF
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "httpbin"
  namespace: "foo"
spec:
  host: "httpbin.foo.svc.cluster.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL
EOF

在这些更改之后,来自 Istio 服务(包括 ingress gateway)到 httpbin.foo 的流量将使用双向 TLS。上面的测试命令仍然有效。在使用正确的令牌的情况下,Istio 服务直接向 httpbin.foo 发出的请求也可以正常工作:

$ kubectl exec $(kubectl get pod -l app=sleep -n foo -o jsonpath={.items..metadata.name}) -c sleep -n foo -- curl http://httpbin.foo:8000/ip -s -o /dev/null -w "%{http_code}\n" --header "Authorization: Bearer $TOKEN"
200

但是,来自非 Istio 服务的明文请求将失败:

$ kubectl exec $(kubectl get pod -l app=sleep -n legacy -o jsonpath={.items..metadata.name}) -c sleep -n legacy -- curl http://httpbin.foo:8000/ip -s -o /dev/null -w "%{http_code}\n" --header "Authorization: Bearer $TOKEN"
401

清理第 3 部分

  1. 删除身份验证策略:

    $ kubectl delete policy jwt-example
    
  2. 删除目标规则:

    $ kubectl delete policy httpbin
    
  3. 如果您不打算探索任何后续任务,则只需删除测试命名空间即可删除所有资源:

    $ kubectl delete ns foo bar legacy