Services in a mesh can respond to call requests from each other only after the requests obtain the external authorization and pass the check of EnvoyFilter. This topic describes how to enable external authorization in Alibaba Cloud Service Mesh (ASM).

Prerequisites

Background information

Services in a mesh may send call requests to each other. When a service sends a request to call another service, the gRPC framework that runs outside the mesh decides whether to authorize the request based on the specified rules. Then, EnvoyFilter calls an external authorization service to check whether the inbound request is authorized. If the request is unauthorized, the request is denied.

Note We recommend that you put the EnvoyFilter resources in the first place in filter chains. This operation ensures that other filters only need to check the authorized requests.

For more information about external authorization of Envoy, visit External Authorization.

gRPC must define an operation to call the Check() method. The following code shows you the context for the request and response. For more information, visit 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);
}

Step 1: Configure an external authorization service

Based on the preceding defined gRPC operation, the following code provides an example on how to configure external authorization by calling the Check() method to check whether the HTTP header contains a bearer token that starts with asm-.
Note The defined gRPC operation supports more complex rules for external authorization.
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)
    }

}

You can directly use the image registry.cn-beijing.aliyuncs.com/istio-samples/ext-authz-grpc-service:latest to configure external authorization. Alternatively, you can build an image based on the Dockerfile in the following example: istio_ext_authz_filter_sample.

Step 2: Deploy an external authorization server

  1. Download the YAML file for deploying an external authorization server from GitHub.
  2. Connect the kubectl client to the ACK cluster that is added to the ASM instance and run the following command:
    kubectl apply -n istio-system -f extauth-sample-grpc-service.yaml
    If the following command output appears, an external authorization server is deployed:
    service/extauth-grpc-service created
    deployment.extensions/extauth-grpc-service created

Step 3: Deploy sample services

  1. Download the YAML file for deploying the sample service httpbin from GitHub.
  2. Connect the kubectl client to the ACK cluster that is added to the ASM instance and run the following command:
    kubectl apply -f httpbin.yaml
  3. Deploy the sample client service sleep for testing.
    Download the YAML file for deploying the sample service sleep from GitHub.
  4. Connect the kubectl client to the ACK cluster that is added to the ASM instance and run the following command:
    kubectl apply -f sleep.yaml

Step 4: Define an EnvoyFilter resource

Run the following commands to define an EnvoyFilter resource:
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"
      proxy:
        proxyVersion: "^1\.6.*"
    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
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
        name: virtualInbound
        filterChain:
          filter:
            name: "envoy.http_connection_manager"
      proxy:
        proxyVersion: "^1\.7.*"
    patch:
      operation: INSERT_BEFORE
      value:
        # Configure the envoy.ext_authz here:
        name: envoy.ext_authz
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz" 
          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
If the following command output appears, an EnvoyFilter resource is defined:
envoyfilter.networking.istio.io/extauth-sample created

Step 5: Verify external authorization

  1. Log on to the Sleep pod and run the following command:
    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'
    The following command output appears:
    Need an Authorization Header with a character bearer token using asm- as prefix!

    The command output indicates that the request from the sleep service failed to obtain the external authorization because the request header does not contain a bearer token that starts with asm-.

  2. Run the following command to add a header that contains a bearer token that starts with asm- to the request:
    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'
    The following command output appears:
    {
      "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"
      }
    }

    The command output indicates that the sleep service obtains the external authorization.