更好的外部授权方式集成

AuthorizationPolicy 现在支持以 CUSTOM 自定义方式委托外部系统进行授权操作。

Feb 9, 2021 | By Yangmin Zhu - Google

背景

Istio 的授权策略为网格中的服务提供访问控制。它速度快、功能强大且使用广泛。 自 Istio 1.4 首次发布以来,我们不断改进策略以使其更加灵活, 包括 DENY 操作排除语义X-Forwarded-For 头信息支持嵌套 JWT 声明支持等等。 这些特性提高了授权策略的灵活性,但仍有许多场景无法通过该模型支持,例如:

解决方案

在 Istio 1.9 中,我们通过引入 CUSTOM 操作实现了授权策略的可扩展性, 它允许您将访问控制决策委托给外部授权服务。

CUSTOM 操作允许您将 Istio 与外部授权系统集成, 该系统实现了自己的自定义授权逻辑。下图展示了此集成方式的顶层架构:

外部授权架构
外部授权架构

在进行配置时,网格管理员使用 CUSTOM 操作对授权策略进行配置, 用于在代理(网关或 Sidecar)上启用外部授权。 管理员应确认外部身份验证服务已启动且正在运行。

在运行时中:

  1. 请求被代理拦截,代理将根据用户在授权策略中的配置向外部授权服务发送检查请求。

  2. 外部授权服务将决定是否允许请求通过。

  3. 如果允许,请求将继续,并将由 ALLOW/DENY 操作定义的任意本地授权强制执行。

  4. 如果被拒绝,请求将立即被终止。

让我们看一下带有 CUSTOM 操作的示例授权策略:

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: ext-authz
  namespace: istio-system
spec:
  # The selector applies to the ingress gateway in the istio-system namespace.
  selector:
    matchLabels:
      app: istio-ingressgateway
  # The action "CUSTOM" delegates the access control to an external authorizer, this is different from
  # the ALLOW/DENY action that enforces the access control right inside the proxy.
  action: CUSTOM
  # The provider specifies the name of the external authorizer defined in the meshconfig, which tells where and how to
  # talk to the external auth service. We will cover this more later.
  provider:
    name: "my-ext-authz-service"
  # The rule specifies that the access control is triggered only if the request path has the prefix "/admin/".
  # This allows you to easily enable or disable the external authorization based on the requests, avoiding the external
  # check request if it is not needed.
  rules:
  - to:
    - operation:
        paths: ["/admin/*"]

此示例引用了一个在网格配置中定义的、名为 my-ext-authz-service 的提供程序:

extensionProviders:
# The name "my-ext-authz-service" is referred to by the authorization policy in its provider field.
- name: "my-ext-authz-service"
  # The "envoyExtAuthzGrpc" field specifies the type of the external authorization service is implemented by the Envoy
  # ext-authz filter gRPC API. The other supported type is the Envoy ext-authz filter HTTP API.
  # See more in https://www.envoyproxy.io/docs/envoy/v1.16.2/intro/arch_overview/security/ext_authz_filter.
  envoyExtAuthzGrpc:
    # The service and port specifies the address of the external auth service, "ext-authz.istio-system.svc.cluster.local"
    # means the service is deployed in the mesh. It can also be defined out of the mesh or even inside the pod as a separate
    # container.
    service: "ext-authz.istio-system.svc.cluster.local"
    port: 9000

授权策略中的 CUSTOM 操作表示在运行时中启用外部授权, 可以配置为根据请求有条件地触发外部授权, 并且使用您已经用于其他操作的相同规则进行外部授权。

外部授权服务当前在 meshconfig API 中定义并通过其名称进行引用。它可以部署在任何使用或不使用代理的网格环境中。 如果使用代理,您可以进一步使用 PeerAuthentication 配置在代理和外部授权服务之间开启 mTLS。

CUSTOM 操作目前仍然处于实验阶段;API 可能会基于用户反馈针对后续版本进行不兼容的修改。当授权策略规则与 CUSTOM 操作一起使用时,其目前不支持身份验证字段(例如源主体或 JWT 声明)。 在单独的工作负载中只允许使用一个提供程序,但您仍然可以在不同的工作负载上使用不同的提供程序。

有关详细信息,请参阅 Better External Authorization 设计文档

OPA 示例

在本节中,我们将演示如何使用 CUSTOM 操作以及 Open Policy Agent 作为入口网关上的外部授权程序。我们将有条件地在除 /ip 之外的所有路径上启用外部授权。

您还可以参考外部授权任务来获得使用 ext-authz 服务器示例的更基础介绍。

创建 OPA 策略示例

运行以下命令创建一个 OPA 策略,如果路径的前缀与 JWT 令牌中的声明“path”(base64 编码)匹配,则允许该请求:

$ cat > policy.rego <<EOF
package envoy.authz

import input.attributes.request.http as http_request

default allow = false

token = {"valid": valid, "payload": payload} {
    [_, encoded] := split(http_request.headers.authorization, " ")
    [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
}

allow {
    is_token_valid
    action_allowed
}

is_token_valid {
  token.valid
  now := time.now_ns() / 1000000000
  token.payload.nbf <= now
  now < token.payload.exp
}

action_allowed {
  startswith(http_request.path, base64url.decode(token.payload.path))
}
EOF
$ kubectl create secret generic opa-policy --from-file policy.rego

部署 httpbin 和 OPA

启用 Sidecar 注入:

$ kubectl label ns default istio-injection=enabled

运行以下命令部署 httpbin 示例应用程序和 OPA。 OPA 可以作为单独的容器部署在 httpbin Pod 中,也可以完全独立部署在单独的 Pod 中:

$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: httpbin-with-opa
  labels:
    app: httpbin-with-opa
    service: httpbin-with-opa
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin-with-opa
---
# Define the service entry for the local OPA service on port 9191.
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: local-opa-grpc
spec:
  hosts:
  - "local-opa-grpc.local"
  endpoints:
  - address: "127.0.0.1"
  ports:
  - name: grpc
    number: 9191
    protocol: GRPC
  resolution: STATIC
---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: httpbin-with-opa
  labels:
    app: httpbin-with-opa
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin-with-opa
  template:
    metadata:
      labels:
        app: httpbin-with-opa
    spec:
      containers:
        - image: docker.io/kennethreitz/httpbin
          imagePullPolicy: IfNotPresent
          name: httpbin
          ports:
          - containerPort: 80
        - name: opa
          image: openpolicyagent/opa:latest-envoy
          securityContext:
            runAsUser: 1111
          volumeMounts:
          - readOnly: true
            mountPath: /policy
            name: opa-policy
          args:
          - "run"
          - "--server"
          - "--addr=localhost:8181"
          - "--diagnostic-addr=0.0.0.0:8282"
          - "--set=plugins.envoy_ext_authz_grpc.addr=:9191"
          - "--set=plugins.envoy_ext_authz_grpc.query=data.envoy.authz.allow"
          - "--set=decision_logs.console=true"
          - "--ignore=.*"
          - "/policy/policy.rego"
          livenessProbe:
            httpGet:
              path: /health?plugins
              scheme: HTTP
              port: 8282
            initialDelaySeconds: 5
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /health?plugins
              scheme: HTTP
              port: 8282
            initialDelaySeconds: 5
            periodSeconds: 5
      volumes:
        - name: proxy-config
          configMap:
            name: proxy-config
        - name: opa-policy
          secret:
            secretName: opa-policy
EOF

定义外部授权程序

运行以下命令来编辑 meshconfig

$ kubectl edit configmap istio -n istio-system

将以下 extensionProviders 添加到 meshconfig 中:

apiVersion: v1
data:
  mesh: |-
    # Add the following contents:
    extensionProviders:
    - name: "opa.local"
      envoyExtAuthzGrpc:
        service: "local-opa-grpc.local"
        port: "9191"

使用 CUSTOM 操作创建 AuthorizationPolicy

运行以下命令创建授权策略,在除 /ip 之外的所有路径上启用外部授权:

$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: httpbin-opa
spec:
  selector:
    matchLabels:
      app: httpbin-with-opa
  action: CUSTOM
  provider:
    name: "opa.local"
  rules:
  - to:
    - operation:
        notPaths: ["/ip"]
EOF

测试 OPA 策略

  1. 创建一个客户端 Pod 来发送请求:

    Zip
    $ kubectl apply -f @samples/sleep/sleep.yaml@
    $ export SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
    
  2. 使用由 OPA 签发的测试 JWT 令牌:

    $ export TOKEN_PATH_HEADERS="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXRoIjoiTDJobFlXUmxjbk09IiwibmJmIjoxNTAwMDAwMDAwLCJleHAiOjE5MDAwMDAwMDB9.9yl8LcZdq-5UpNLm0Hn0nnoBHXXAnK4e8RSl9vn6l98"
    

    测试 JWT 令牌具有以下声明:

    {
      "path": "L2hlYWRlcnM=",
      "nbf": 1500000000,
      "exp": 1900000000
    }
    

    path 声明的值为 L2hlYWRlcnM=,它是 /headers 的 base64 编码格式。

  3. 在不携带令牌时向路径 /headers 发送请求。 因为没有 JWT 令牌,请求会以 403 状态方式被拒绝:

    $ kubectl exec ${SLEEP_POD} -c sleep  -- curl http://httpbin-with-opa:8000/headers -s -o /dev/null -w "%{http_code}\n"
    403
    
  4. 携带有效令牌向路径 /get 发送请求。因为路径为 /get 与令牌中 /headers 路径不匹配,请求也会以 403 状态方式被拒绝:

    $ kubectl exec ${SLEEP_POD} -c sleep  -- curl http://httpbin-with-opa:8000/get -H "Authorization: Bearer $TOKEN_PATH_HEADERS" -s -o /dev/null -w "%{http_code}\n"
    403
    
  5. 携带有效令牌向路径 /headers 发送请求。 由于路径与令牌匹配,请求会以 200 状态被允许:

    $ kubectl exec ${SLEEP_POD} -c sleep  -- curl http://httpbin-with-opa:8000/headers -H "Authorization: Bearer $TOKEN_PATH_HEADERS" -s -o /dev/null -w "%{http_code}\n"
    200
    
  6. 不携带令牌向路径 /ip 发送请求。由于路径 /ip 被排除在授权之外,请求也会以 200 状态被允许:

    $ kubectl exec ${SLEEP_POD} -c sleep  -- curl http://httpbin-with-opa:8000/ip -s -o /dev/null -w "%{http_code}\n"
    200
    
  7. 检查代理和 OPA 日志以确认结果。

总结

在 Istio 1.9 中,授权策略中的 CUSTOM 操作允许您轻松地将 Istio 与任何外部授权系统集成,并具备以下优势:

我们正努力在后续版本中将此功能提升到更稳定的阶段, 并欢迎您在 discuss.istio.io 上提供反馈。

致谢

感谢 Craig BoxChristian PostaLimin Wang 对本博客的初稿进行审核。