Envoy is a L7 proxy and communication bus designed for large modern service oriented architectures. Envoy (v1.7.0+) supports an which calls an authorization service to check if the incoming request is authorized or not.

This feature makes it possible to delegate authorization decisions to an external service and also makes the request context available to the service which can then be used to make an informed decision about the fate of the incoming request received by Envoy.

The tutorial shows how Envoy’s External authorization filter can be used with OPA as an authorization service to enforce security policies over API requests received by Envoy. The tutorial also covers examples of authoring custom policies over the HTTP request body.

This tutorial requires Kubernetes 1.14 or later. To run the tutorial locally, we recommend using minikube in version with Kubernetes 1.14 (which is the default).

2. Create ConfigMap containing configuration for Envoy

The Envoy configuration below defines an external authorization filter envoy.ext_authz for a gRPC authorization server.

Save the configuration as envoy.yaml:

  1. static_resources:
  2. listeners:
  3. - address:
  4. socket_address:
  5. address: 0.0.0.0
  6. port_value: 8000
  7. filter_chains:
  8. - filters:
  9. - name: envoy.http_connection_manager
  10. typed_config:
  11. "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
  12. codec_type: auto
  13. stat_prefix: ingress_http
  14. route_config:
  15. name: local_route
  16. virtual_hosts:
  17. - name: backend
  18. domains:
  19. - "*"
  20. routes:
  21. - match:
  22. prefix: "/"
  23. route:
  24. cluster: service
  25. http_filters:
  26. - name: envoy.ext_authz
  27. config:
  28. with_request_body:
  29. max_request_bytes: 8192
  30. allow_partial_message: true
  31. failure_mode_allow: false
  32. grpc_service:
  33. google_grpc:
  34. target_uri: 127.0.0.1:9191
  35. stat_prefix: ext_authz
  36. timeout: 0.5s
  37. - name: envoy.router
  38. typed_config: {}
  39. clusters:
  40. - name: service
  41. connect_timeout: 0.25s
  42. type: strict_dns
  43. lb_policy: round_robin
  44. load_assignment:
  45. cluster_name: service
  46. endpoints:
  47. - lb_endpoints:
  48. - endpoint:
  49. address:
  50. socket_address:
  51. address: 127.0.0.1
  52. port_value: 8080
  53. admin:
  54. access_log_path: "/dev/null"
  55. address:
  56. socket_address:
  57. address: 0.0.0.0
  58. port_value: 8001

Create the ConfigMap:

    The following OPA policy restricts access to the /people endpoint exposed by our sample app:

    • Alice is granted a guest role and can perform a GET request to /people.
    • Bob is granted an admin role and can perform a GET and POST request to /people.

    The policy also restricts an admin user, in this case bob from creating an employee with the same firstname as himself.

    1. package envoy.authz
    2. import input.attributes.request.http as http_request
    3. default allow = false
    4. token = {"valid": valid, "payload": payload} {
    5. [_, encoded] := split(http_request.headers.authorization, " ")
    6. [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
    7. }
    8. allow {
    9. is_token_valid
    10. action_allowed
    11. }
    12. is_token_valid {
    13. token.valid
    14. token.payload.nbf <= time.now_ns() < token.payload.exp
    15. }
    16. action_allowed {
    17. http_request.method == "GET"
    18. token.payload.role == "guest"
    19. glob.match("/people*", [], http_request.path)
    20. }
    21. action_allowed {
    22. http_request.method == "GET"
    23. token.payload.role == "admin"
    24. glob.match("/people*", [], http_request.path)
    25. }
    26. action_allowed {
    27. http_request.method == "POST"
    28. token.payload.role == "admin"
    29. glob.match("/people", [], http_request.path)
    30. lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub)
    31. }

    Store the policy in Kubernetes as a Secret.

    1. kubectl create secret generic opa-policy --from-file policy.rego

    In the next step, OPA is configured to query for the data.envoy.authz.allow decision. If the response is true the operation is allowed, otherwise the operation is denied. Sample input received by OPA is shown below:

    1. {
    2. "attributes": {
    3. "request": {
    4. "http": {
    5. "method": "GET",
    6. "path": "/people",
    7. "headers": {
    8. "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU"
    9. }
    10. }
    11. }
    12. }
    13. }

    With the input value above, the answer is:

    1. true

    An example of the complete input received by OPA can be seen here.

    4. Create App Deployment with OPA and Envoy sidecars

    Our deployment contains a sample Go app which provides information about employees in a company. It exposes a /people endpoint to get and create employees. More information can on the app be found here.

    OPA is started with a configuration that sets the listening address of Envoy External Authorization gRPC server and specifies the name of the policy decision to query. More information on the configuration options can be found .

    Save the deployment as deployment.yaml:

    1. kind: Deployment
    2. apiVersion: apps/v1
    3. metadata:
    4. name: example-app
    5. labels:
    6. app: example-app
    7. spec:
    8. replicas: 1
    9. selector:
    10. matchLabels:
    11. app: example-app
    12. metadata:
    13. app: example-app
    14. spec:
    15. initContainers:
    16. - name: proxy-init
    17. image: openpolicyagent/proxy_init:v2
    18. args: ["-p", "8000", "-u", "1111"]
    19. securityContext:
    20. capabilities:
    21. add:
    22. - NET_ADMIN
    23. runAsNonRoot: false
    24. runAsUser: 0
    25. containers:
    26. - name: app
    27. image: openpolicyagent/demo-test-server:v1
    28. ports:
    29. - containerPort: 8080
    30. - name: envoy
    31. image: envoyproxy/envoy:v1.10.0
    32. securityContext:
    33. runAsUser: 1111
    34. volumeMounts:
    35. - readOnly: true
    36. mountPath: /config
    37. name: proxy-config
    38. args:
    39. - "envoy"
    40. - "--config-path"
    41. - "/config/envoy.yaml"
    42. - name: opa
    43. # Note: openpolicyagent/opa:latest-istio is created by retagging
    44. # the latest released image of OPA-Istio.
    45. image: openpolicyagent/opa:0.14.2-istio
    46. securityContext:
    47. runAsUser: 1111
    48. volumeMounts:
    49. - readOnly: true
    50. mountPath: /policy
    51. name: opa-policy
    52. args:
    53. - "--plugin-dir=/app"
    54. - "run"
    55. - "--server"
    56. - "--set=plugins.envoy_ext_authz_grpc.addr=:9191"
    57. - "--set=plugins.envoy_ext_authz_grpc.query=data.envoy.authz.allow"
    58. - "--set=decision_logs.console=true"
    59. - "--ignore=.*"
    60. - "/policy/policy.rego"
    61. volumes:
    62. - name: proxy-config
    63. configMap:
    64. name: proxy-config
    65. - name: opa-policy
    66. secret:
    67. secretName: opa-policy
    1. kubectl apply -f deployment.yaml

    The proxy-init container installs iptables rules to redirect all container traffic through the Envoy proxy sidecar. More information can be found here.

    minikube:

    1. export SERVICE_PORT=$(kubectl get service example-app-service -o jsonpath='{.spec.ports[?(@.port==8080)].nodePort}')
    2. export SERVICE_HOST=$(minikube ip)
    3. export SERVICE_URL=$SERVICE_HOST:$SERVICE_PORT
    4. echo $SERVICE_URL

    minikube (example):

    1. 192.168.99.113:31056

    6. Exercise the OPA policy

    For convenience, we’ll want to store Alice’s and Bob’s tokens in environment variables.

    1. export ALICE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU"
    2. export BOB_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJZbTlpIiwibmJmIjoxNTE0ODUxMTM5LCJleHAiOjE2NDEwODE1Mzl9.WCxNAveAVAdRCmkpIObOTaSd0AJRECY2Ch2Qdic3kU8"

    Check that Alice can get employees but cannot create one.

    1. curl -i -H "Authorization: Bearer "$ALICE_TOKEN"" http://$SERVICE_URL/people
    2. curl -i -H "Authorization: Bearer "$ALICE_TOKEN"" -d '{"firstname":"Charlie", "lastname":"OPA"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people

    Check that Bob can get employees and also create one.

    Check that Bob cannot create an employee with the same firstname as himself.

      Congratulations for finishing the tutorial !

      This tutorial showed how to use OPA as an External authorization service to enforce custom policies by leveraging Envoy’s External authorization filter.

      This tutorial also showed a sample OPA policy that returns a boolean decision to indicate whether a request should be allowed or not.