Authentication Policy控制集群mTLS双向认证

Posted by     "Pyker" on Tuesday, April 20, 2021

TOC

本任务涵盖了启用、配置和使用Istio authentication policies时可能需要执行的主要活动。在身份验证概述中了解有关底层概念的更多信息。

开始之前

$ istioctl install --set profile=default

开始

我们的示例用到两个命名空间 foo 和 bar,以及两个服务 httpbin 和 sleep,这两个服务都带有 Envoy sidecar proxy 一起运行。我们也会用到两个运行在 legacy 命名空间下不带 sidecar 的 httpbin 和 sleep 实例。如果您想要使用相同的示例尝试任务,执行如下命令

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

您可以通过从名称空间foo、bar或legacy中的任何sleep pod发送一个带有curl的HTTP请求到httpbin来验证设置。httpbin.foo, httpbin.bar或者httpbin.legacy。使用HTTP代码200,所有请求都应该成功。 例如,下面是sleep.bar请求httpbin.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"

这个一行命令方便地遍历所有可达性组合:

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 -s "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

使用如下命令验证系统中没有peer authentication policy:

$ kubectl get peerauthentication --all-namespaces
No resources found

最后(但并非最不重要),确认没有应用于示例服务的destination rules。您可以通过检查现有destination rules的host: value来做到这一点,并确保它们不匹配。例如:

$ kubectl get destinationrules.networking.istio.io --all-namespaces -o yaml | grep "host:"

根据Istio的版本,您可能会看到其他主机的destination rules。但是,在foo、bar和legacy名称空间中应该没有host,也不应该有匹配所有的通配符*

自动mTLS

默认情况下,Istio关联迁移到Istio代理的服务工作负载,并在配置client proxy自动向这些工作负载发送mTLS通信流,并在没有sidecars的情况下向工作负载发送纯文本(plain text)通信流。 因此,具有代理的工作负载之间的所有通信都使用相互TLS,而无需您做任何事情。例如,从请求到httpbin/header的响应。当使用相互TLS时,代理将X-Forwarded-Client-Cert头注入到向后端发送的上游请求。报头的存在就是使用了mTLS的证据。例如:

$ kubectl exec "$(kubectl get pod -l app=sleep -n foo -o jsonpath={.items..metadata.name})" -c sleep -n foo -- curl -s http://httpbin.foo:8000/headers -s | grep X-Forwarded-Client-Cert | sed 's/Hash=[a-z0-9]*;/Hash=<redacted>;/'
    "X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=<redacted>;Subject=\"\";URI=spiffe://cluster.local/ns/foo/sa/sleep"

当服务没有sidecar时,X-Forwarded-Client-Cert头不在那里,这意味着请求是纯文本的。

$ kubectl exec "$(kubectl get pod -l app=sleep -n foo -o jsonpath={.items..metadata.name})" -c sleep -n foo -- curl http://httpbin.legacy:8000/headers -s | grep X-Forwarded-Client-Cert

在STRICT模式下全局启用Istio mutual TLS

虽然Istio自动将代理和工作负载之间的所有通信升级为mTLS,但工作负载仍然可以接收纯文本通信。为了防止整个mesh的非双向TLS通信,可以设置全mesh-wide对端认证策略,mutual TLS模式设置为STRICT。mesh-wide peer authentication policy不应该有selecter,必须应用在根命名空间中,例如:

$ kubectl apply -f - <<EOF
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "default"
  namespace: "istio-system"
spec:
  mtls:
    mode: STRICT
EOF

该示例假定istio-system是根名称空间。如果在安装过程中使用了不同的值,请使用所使用的值替换istio-system。

此peer authentication policy将工作负载配置为只接受使用TLS加密的请求。因为它没有为selector字段指定值,所以该策略应用于mesh中的所有工作负载,例如:

$ 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: 000  # 失败
command terminated with exit code 56
sleep.legacy to httpbin.legacy: 200

您会看到请求仍然成功,除了那些来自没有sidecar(sleep)的客户机的请求。sleep.legacy请求到有sidecar代理的服务httpbin。foo或httpbin.bar。这是预期的,因为现在严格要求相互TLS,但没有sidecar的工作负载无法遵守。

早在Istio1.1时,此请求无解。后面版本可通过tls.mode: DISABLE实现访问。

清除 Part1

删除会话中添加的全局peer authentication policy和destination rules:

$ kubectl delete peerauthentication -n istio-system default

对每个namespace或工作负载启用mTLS

namespace范围的policy

要为特定名称空间内的所有工作负载更改mTLS,请使用名称空间范围的策略。策略的规范与mesh-wide策略的规范相同,但是要在元数据下指定它应用的名称空间。例如,以下对等身份验证策略为foo命名空间启用严格的mTLS:

$ kubectl apply -f - <<EOF
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "default"
  namespace: "foo"
spec:
  mtls:
    mode: STRICT
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

每个工作负载启用mTLS

要为特定工作负载设置peer authentication policy,您必须配置selector部分,并指定匹配所需工作负载的label。但是,Istio不能将出站mTLS通信流的工作负载级策略聚合到服务。配置destination rules来管理该行为。 例如,下面的为httpbin.bar负载启用peer authentication policy和destination rules:

$ cat <<EOF | kubectl apply -n bar -f -
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "httpbin"
  namespace: "bar"
spec:
  selector:
    matchLabels:
      app: httpbin
  mtls:
    mode: STRICT
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.legacy到httpbin.bar失败也是因为同样的原因。

$ 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: 000
command terminated with exit code 56
sleep.legacy to httpbin.legacy: 200
...
sleep.legacy to httpbin.bar: 000
command terminated with exit code 56

要细化每个端口的相互TLS设置,您必须配置portLevelMtls部分。例如,以下对端认证策略要求在除80端口外的所有端口上相互TLS:

$ cat <<EOF | kubectl apply -n bar -f -
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "httpbin"
  namespace: "bar"
spec:
  selector:
    matchLabels:
      app: httpbin
  mtls:
    mode: STRICT
  portLevelMtls:
    80:
      mode: DISABLE
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
    portLevelSettings:
    - port:
        number: 8000
      tls:
        mode: DISABLE
EOF
  • PeerAuthentication中的端口值是container的端口。DestinationRule的值是service的端口。
  • 如果端口绑定到服务则只能使用portLevelMtls。否则Istio会忽略它.
$ 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的mTLS,您可以测试此行为。例如Foo工作负载。注意,您已经创建了一个名称空间范围的策略,该策略为名称空间foo中的所有服务启用相互TLS,并观察来自sleep.legacy请求httpbin.foo 正在失败(见上文)。

$ cat <<EOF | kubectl apply -n foo -f -
apiVersion: "security.istio.io/v1beta1"
kind: "PeerAuthentication"
metadata:
  name: "overwrite-example"
  namespace: "foo"
spec:
  selector:
    matchLabels:
      app: httpbin
  mtls:
    mode: DISABLE
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

清除Part2

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

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

最终用户身份验证

要试验这个特性,您需要一个有效的JWT。JWT必须与您希望用于演示的JWKS端点相对应。本教程使用Istio代码库中的测试令牌JWT测试和JWKS端点。此外,为了方便起见,请将httpbin.foo设置ingressgateway(有关更多详细信息,请参阅ingressgateway任务)。

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和端口的说明定义INGRESS_HOSTINGRESS_PORT环境变量。然后运行一个测试查询:

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

现在,添加一个请求身份验证策略,它要求终端用户JWT作为ingressgateway。

$ kubectl apply -f - <<EOF
apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
  name: "jwt-example"
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway
  jwtRules:
  - issuer: "[email protected]"
    jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.9/security/tools/jwt/samples/jwks.json"
EOF

将策略应用于它选择的工作负载的名称空间,在本例中是ingressgateway。您需要指定的名称空间是isio-system。如果您在授权头中提供了一个令牌(隐式默认位置),Istio将使用公钥集验证令牌,并在持牌人令牌无效时拒绝请求。但是,没有令牌的请求将被接受。要观察这种行为,请在不使用令牌、使用错误令牌和使用有效令牌的情况下重试请求:

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

要观察JWT验证的其他方面,可以使用gen-jwt.py脚本生成新的令牌,以使用不同的发行者、受众、到期日期等进行测试。该脚本可以从Istio存储库下载:

$ wget --no-verbose https://raw.githubusercontent.com/istio/istio/release-1.9/security/tools/jwt/samples/gen-jwt.py

你还需要钥匙。pem文件:

$ wget --no-verbose https://raw.githubusercontent.com/istio/istio/release-1.9/security/tools/jwt/samples/key.pem

如果您还没有在系统上安装jwcrypto库,请下载它。 JWT认证具有60秒的时钟偏差,这意味着JWT令牌将比其配置的nbf早60秒生效,并在其配置的exp后60秒仍然有效。 例如,下面的命令创建一个在5秒内过期的令牌。如您所见,Istio首先成功地使用该令牌验证请求,但在65秒后拒绝它们:

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

也可以将JWT策略添加到入口网关(例如service istio-ingressgateway.istio-system.svc.cluster.local)。这通常用于为绑定到网关的所有服务(而不是单个服务)定义JWT策略。

需要有一个效令牌

要拒绝没有有效令牌的请求,请添加一个授权策略,其中包含一个规则,该规则为没有请求主体的请求指定拒绝操作,在下面的示例中显示为notRequestPrincipals:[“*”]。请求主体仅在提供有效的JWT令牌时可用。因此,该规则拒绝没有有效令牌的请求。

$ kubectl apply -f - <<EOF
apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "frontend-ingress"
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway
  action: DENY
  rules:
  - from:
    - source:
        notRequestPrincipals: ["*"]
EOF

在不使用令牌的情况下重试请求。请求现在失败,错误代码为403:

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

要求每个路径都有有效的令牌

要用每个主机、路径或方法的令牌要求来细化授权,请将授权策略更改为只需要在/headers上使用JWT。当此授权规则生效时,请求$INGRESS_HOST:$INGRESS_PORT/headers失败,错误码为403。对所有其他路径的请求都成功,例如$INGRESS_HOST:$INGRESS_PORT/ip。

kubectl apply -f - <<EOF
apiVersion: "security.istio.io/v1beta1"
kind: "AuthorizationPolicy"
metadata:
  name: "frontend-ingress"
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway
  action: DENY
  rules:
  - from:
    - source:
        notRequestPrincipals: ["*"]
    to:
    - operation:
        paths: ["/headers"]
EOF
$ curl "$INGRESS_HOST:$INGRESS_PORT/headers" -s -o /dev/null -w "%{http_code}\n"
403
$ curl "$INGRESS_HOST:$INGRESS_PORT/ip" -s -o /dev/null -w "%{http_code}\n"
200

清除Part3

删除authentication policy:

$ kubectl -n istio-system delete requestauthentication jwt-example

删除authorization policy:

$ kubectl -n istio-system delete authorizationpolicy frontend-ingress

删除令牌生成器脚本和密钥文件:

$ rm -f ./gen-jwt.py ./key.pem

如果您不打算研究任何后续任务,您可以通过删除测试名称空间来删除所有资源:

$ kubectl delete ns foo bar legacy

本文来至官方:https://istio.io/latest/docs/tasks/security/authentication/authn-policy/#end-user-authentication

「真诚赞赏,手留余香」

云原生与运维

真诚赞赏,手留余香

使用微信扫描二维码完成支付


comments powered by Disqus