服务网格各服务间的调用请求需要经过外部授权并通过授权过滤器的检查,才能给予响应。本文介绍了如何在ASM中实现自定义外部授权服务。

前提条件

背景信息

在服务网格中,服务间存在着调用请求。运行在网格外部的gRPC服务会根据制定的判断规则来决定是否对这些请求进行授权。然后,外部授权过滤器将调用授权服务来检查传入的请求是否被授权。如果外部授权过滤器发现某请求未被授权服务器授权,则该请求将被拒绝响应。

说明 建议将这些授权过滤器配置为过滤器链中的第一个过滤器,以便在其余过滤器处理请求之前确保该请求已被授权。

更多Envoy外部授权的内容,请参见External Authorization

gRPC外部服务需要相应的接口,实现该Check()方法。请求响应上下文定义如下,详情请参见external_auth.proto

// A generic interface for performing authorization check on incoming
// requests to a networked service.
service Authorization {
  // Performs authorization check based on the attributes associated with the
  // incoming request, and returns status `OK` or not `OK`.
  rpc Check(v2.CheckRequest) returns (v2.CheckResponse);
}

步骤一:实现外部授权服务

基于上述gRPC服务接口定义,本文以在Check()方法中判断Bearer Token值是否以asm-开头为例,实现外部授权服务如下。
说明 只要符合该接口定义,您还可以添加更为复杂的处理逻辑进行检查。
package main

import (
    "context"
    "log"
    "net"
    "strings"

    "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
    auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
    envoy_type "github.com/envoyproxy/go-control-plane/envoy/type"
    "github.com/gogo/googleapis/google/rpc"
    "google.golang.org/grpc"
)

// empty struct because this isn't a fancy example
type AuthorizationServer struct{}

// inject a header that can be used for future rate limiting
func (a *AuthorizationServer) Check(ctx context.Context, req *auth.CheckRequest) (*auth.CheckResponse, error) {
    authHeader, ok := req.Attributes.Request.Http.Headers["authorization"]
    var splitToken []string
    if ok {
        splitToken = strings.Split(authHeader, "Bearer ")
    }
    if len(splitToken) == 2 {
        token := splitToken[1]
        // Normally this is where you'd go check with the system that knows if it's a valid token.

        if strings.HasPrefix(token, "asm-") {
            return &auth.CheckResponse{
                Status: &rpc.Status{
                    Code: int32(rpc.OK),
                },
                HttpResponse: &auth.CheckResponse_OkResponse{
                    OkResponse: &auth.OkHttpResponse{
                        Headers: []*core.HeaderValueOption{
                            {
                                Header: &core.HeaderValue{
                                    Key:   "x-custom-header-from-authz",
                                    Value: "some value",
                                },
                            },
                        },
                    },
                },
            }, nil
        }
    }
    return &auth.CheckResponse{
        Status: &rpc.Status{
            Code: int32(rpc.UNAUTHENTICATED),
        },
        HttpResponse: &auth.CheckResponse_DeniedResponse{
            DeniedResponse: &auth.DeniedHttpResponse{
                Status: &envoy_type.HttpStatus{
                    Code: envoy_type.StatusCode_Unauthorized,
                },
                Body: "Need an Authorization Header with a character bearer token using asm- as prefix!",
            },
        },
    }, nil
}

func main() {
    // create a TCP listener on port 4000
    lis, err := net.Listen("tcp", ":4000")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    log.Printf("listening on %s", lis.Addr())

    grpcServer := grpc.NewServer()
    authServer := &AuthorizationServer{}
    auth.RegisterAuthorizationServer(grpcServer, authServer)

    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }

}

可以直接使用镜像:registry.cn-beijing.aliyuncs.com/istio-samples/ext-authz-grpc-service:latest。或者可以基于以下Dockerfile构建镜像,具体代码参见istio_ext_authz_filter_sample

步骤二:启动外部授权服务器

  1. 进入Github项目库,下载部署YAML文件。
  2. 通过kubectl连接到ASM实例中新添加的ACK集群,执行以下命令。
    kubectl apply -n istio-system -f extauth-sample-grpc-service.yaml
    看到以下输出显示部署成功。
    service/extauth-grpc-service created
    deployment.extensions/extauth-grpc-service created

步骤三:部署示例应用

  1. 进入Github项目库,下载部署示例httpbin服务的YMAL文件。
  2. 通过kubectl连接到ASM实例中新添加的ACK集群,执行以下命令。
    kubectl apply -f httpbin.yaml
  3. 部署用于测试的客户端示例应用sleep。
    进入Github项目库,下载部署示例sleep服务的YAML文件。
  4. 通过kubectl连接到ASM实例中新添加的ACK集群,执行以下命令。
    kubectl apply -f sleep.yaml

步骤四:定义EnvoyFilter

执行以下命令,定义Istio EnvoyFilter。
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  # This needs adjusted to be the app name
  name: extauth-sample
spec:
  workloadSelector:
    labels:
      # This needs adjusted to be the app name
      app: httpbin

  # Patch the envoy configuration
  configPatches:

  # Adds the ext_authz HTTP filter for the ext_authz API
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
        name: virtualInbound
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
    patch:
      operation: INSERT_BEFORE
      value:
        # Configure the envoy.ext_authz here:
        name: envoy.ext_authz
        config:
          grpc_service:
            # NOTE: *SHOULD* use envoy_grpc as ext_authz can use dynamic clusters and has connection pooling
            google_grpc:
              target_uri: extauth-grpc-service.istio-system:4000
              stat_prefix: ext_authz
            timeout: 0.2s
          failure_mode_allow: false
          with_request_body:
            max_request_bytes: 8192
            allow_partial_message: true
EOF
看到以下输出,表示过滤器已成功定义。
envoyfilter.networking.istio.io/extauth-sample created

步骤五:验证外部授权

  1. 登录到Sleep Pod容器中,执行以下命令。
    export SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
    kubectl exec -it $SLEEP_POD -c sleep -- sh -c 'curl  http://httpbin:8000/headers'
    返回以下结果:
    Need an Authorization Header with a character bearer token using asm- as prefix!

    可以看到,示例应用程序的请求没有通过外部授权的许可,原因是请求头中并没有满足Bearer Token值以asm-开头。

  2. 执行以下命令,在请求中添加以asm-开头的Bearer Token请求头。
    export SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
    kubectl exec -it $SLEEP_POD -c sleep -- sh -c 'curl -H "Authorization: Bearer asm-token1" http://httpbin:8000/headers'
    返回以下结果:
    {
      "headers": {
        "Accept": "*/*",
        "Authorization": "Bearer asm-token1",
        "Content-Length": "0",
        "Host": "httpbin:8000",
        "User-Agent": "curl/7.64.0",
        "X-B3-Parentspanid": "dab85d9201369071",
        "X-B3-Sampled": "1",
        "X-B3-Spanid": "c29b18886e98a95f",
        "X-B3-Traceid": "66875d955ac13dfcdab85d9201369071",
        "X-Custom-Header-From-Authz": "some value"
      }
    }

    可以看到,示例应用程序的请求已通过外部授权的许可。