Policy Primer via Examples

This page covers how to write policies for the content of the requests that are passed to OPA by Envoy’s External Authorization filter.

Writing Policies

Let’s start with an example policy that restricts access to an endpoint based on a user’s role and permissions.

  1. package envoy.authz
  2. import input.attributes.request.http
  3. default allow = false
  4. token = {"valid": valid, "payload": payload} {
  5. [_, encoded] := split(http.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. now := time.now_ns() / 1000000000
  15. token.payload.nbf <= now
  16. now < token.payload.exp
  17. }
  18. action_allowed {
  19. http.method == "GET"
  20. token.payload.role == "guest"
  21. glob.match("/people/*", ["/"], http.path)
  22. }
  23. action_allowed {
  24. http.method == "GET"
  25. token.payload.role == "admin"
  26. glob.match("/people/*", ["/"], http.path)
  27. }
  28. action_allowed {
  29. http.method == "POST"
  30. token.payload.role == "admin"
  31. glob.match("/people", [], http.path)
  32. lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub)
  33. }

The first line package envoy.authz declaration gives the (hierarchical) name envoy.authz to the rules in the remainder of the policy. If the OPA-Envoy configuration does not specify the path field, envoy/authz/allow will be considered as the default policy decision path. data.envoy.authz.allow will be the name of the policy decision to query in the default case.

The above policy uses the io.jwt.decode_verify builtin function to parse and verify the JWT containing information about the user making the request. It uses other builtins like glob.match, lower, base64url.decode etc. OPA has 150+ builtins detailed at openpolicyagent.org/docs/policy-reference.

The dot notation seen in multiple places in the policy for ex. input.parsed_body.firstname simply descends through the hierarchy to access the requested value. The dot (.) operator never throws any errors; if the path does not exist the value of the expression is undefined.

Policy Primer via Examples - 图1

  1. data.envoy.authz.allow

Sample input received by OPA is shown below:

Policy Primer via Examples - 图2

  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:

Policy Primer via Examples - 图3

  1. true

Example Policy with Object Response

The allow rule in the above policy returns a boolean decision to indicate whether a request should be allowed or not. If you’d like your rule to not only indicate if a request is allowed or not but also provide optional response headers, body and HTTP status that can be sent to the downstream client or upstream, the below allow rule generates an object that provides additional details along with the status of the request (ie. allowed or denied).

  1. package envoy.authz
  2. default allow = {
  3. "allowed": false,
  4. "headers": {"x-ext-auth-allow": "no"},
  5. "body": "Unauthorized Request",
  6. "http_status": 301
  7. }
  8. allow = response {
  9. input.attributes.request.http.method == "GET"
  10. response := {
  11. "allowed": true,
  12. "headers": {"x-ext-auth-allow": "yes"}
  13. }
  14. }

Policy Primer via Examples - 图4

  1. data.envoy.authz.allow

Sample input received by OPA is shown below:

Policy Primer via Examples - 图5

  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:

Policy Primer via Examples - 图6

  1. {
  2. "allowed": true,
  3. "headers": {
  4. "x-ext-auth-allow": "yes"
  5. }
  6. }

Input Document

In OPA, input is a reserved, global variable whose value is the request sent by the Envoy External Authorization filter to OPA. The OPA-Envoy plugin supports both v2 and v3 versions of the CheckRequest which is used to pass the request to OPA.

For v3 requests, the specified JSON mapping for protobuf is used for making the incoming envoy.service.auth.v3.CheckRequest available in input. It differs from the encoding used for v2 requests. In v3, all keys are lower camelcase. Also, needless nesting of oneof values is removed.

For example, source address data that looks like this in v2,

  1. "source": {
  2. "address": {
  3. "Address": {
  4. "SocketAddress": {
  5. "PortSpecifier": {
  6. "PortValue": 59052
  7. },
  8. "address": "127.0.0.1"
  9. }
  10. }
  11. }
  12. }

becomes, in v3,

  1. "source": {
  2. "address": {
  3. "socketAddress": {
  4. "address": "127.0.0.1",
  5. "portValue": 59052
  6. }
  7. }
  8. }

The following table shows the rego code for common data, in v2 and v3:

informationrego v2rego v3
source addressinput.attributes.source.address.Address.SocketAddress.addressinput.attributes.source.address.socketAddress.address
source portinput.attributes.source.address.Address.SocketAddress.PortSpecifier.PortValueinput.attributes.source.address.socketAddress.portValue
destination addressinput.attributes.destination.address.Address.SocketAddress.addressinput.attributes.destination.address.socketAddress.address
destination portinput.attributes.destination.address.Address.SocketAddress.PortSpecifier.PortValueinput.attributes.destination.address.socketAddress.portValue
dynamic metadatainput.attributes.metadata_context.filter_metadatainput.attributes.metadataContext.filterMetadata

Due to those differences, it’s important to know which version is used when writing policies. Thus, this information is passed into the OPA evaluation under input.version, where you’ll either find, for v2,

  1. input.version == { "ext_authz": "v2", "encoding": "encoding/json" }

or, for v3,

  1. input.version == { "ext_authz": "v3", "encoding": "protojson" }

To have Envoy use the v3 version of the service, the http_filters entry in the Envoy configuration should look like below (minimal version):

  1. http_filters:
  2. - name: envoy.ext_authz
  3. typed_config:
  4. '@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
  5. transport_api_version: V3
  6. grpc_service:
  7. google_grpc: # or envoy_grpc
  8. target_uri: "127.0.0.1:9191"

Example Input

Example v3 Input

  1. {
  2. "attributes": {
  3. "source": {
  4. "address": {
  5. "socketAddress": {
  6. "address": "172.17.0.1",
  7. "portValue": 61402
  8. }
  9. }
  10. },
  11. "destination": {
  12. "address": {
  13. "socketAddress": {
  14. "address": "172.17.06",
  15. "portValue": 8000
  16. }
  17. }
  18. },
  19. "request": {
  20. "time": "2020-11-20T09:47:47.722473Z",
  21. "http": {
  22. "id":"13519049518330544501",
  23. "method": "POST",
  24. "headers": {
  25. ":authority":"192.168.99.206:30164",
  26. ":method":"POST",
  27. ":path":"/people?lang=en",
  28. "accept": "*/*",
  29. "authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJZbTlpIiwibmJmIjoxNTE0ODUxMTM5LCJleHAiOjE2NDEwODE1Mzl9.WCxNAveAVAdRCmkpIObOTaSd0AJRECY2Ch2Qdic3kU8",
  30. "content-length":"41",
  31. "content-type":"application/json",
  32. "user-agent":"curl/7.54.0",
  33. "x-forwarded-proto":"http",
  34. "x-request-id":"7bca5c86-bf55-432c-b212-8c0f1dc999ec"
  35. },
  36. "host":"192.168.99.206:30164",
  37. "path":"/people?lang=en",
  38. "protocol":"HTTP/1.1",
  39. "body":"{\"firstname\":\"Charlie\", \"lastname\":\"Opa\"}",
  40. "size":41
  41. }
  42. },
  43. "metadataContext": {}
  44. },
  45. "parsed_body":{"firstname": "Charlie", "lastname": "Opa"},
  46. "parsed_path":["people"],
  47. "parsed_query": {"lang": ["en"]},
  48. "truncated_body": false,
  49. "version": {
  50. "encoding":"protojson",
  51. "ext_authz":"v3"
  52. }
  53. }

Example v2 Input

  1. {
  2. "attributes":{
  3. "source":{
  4. "address":{
  5. "Address":{
  6. "SocketAddress":{
  7. "PortSpecifier":{
  8. "PortValue":61402
  9. },
  10. "address":"172.17.0.1"
  11. }
  12. }
  13. }
  14. },
  15. "destination":{
  16. "address":{
  17. "Address":{
  18. "SocketAddress":{
  19. "PortSpecifier":{
  20. "PortValue":8000
  21. },
  22. "address":"172.17.0.6"
  23. }
  24. }
  25. }
  26. },
  27. "request":{
  28. "http":{
  29. "id":"13519049518330544501",
  30. "method":"POST",
  31. "headers":{
  32. ":authority":"192.168.99.206:30164",
  33. ":method":"POST",
  34. ":path":"/people?lang=en",
  35. "accept":"*/*",
  36. "authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJZbTlpIiwibmJmIjoxNTE0ODUxMTM5LCJleHAiOjE2NDEwODE1Mzl9.WCxNAveAVAdRCmkpIObOTaSd0AJRECY2Ch2Qdic3kU8",
  37. "content-length":"41",
  38. "content-type":"application/json",
  39. "user-agent":"curl/7.54.0",
  40. "x-forwarded-proto":"http",
  41. "x-request-id":"7bca5c86-bf55-432c-b212-8c0f1dc999ec"
  42. },
  43. "host":"192.168.99.206:30164",
  44. "path":"/people?lang=en",
  45. "protocol":"HTTP/1.1",
  46. "body":"{\"firstname\":\"Charlie\", \"lastname\":\"Opa\"}",
  47. "size":41
  48. }
  49. }
  50. },
  51. "parsed_body":{"firstname": "Charlie", "lastname": "Opa"},
  52. "parsed_path":["people"],
  53. "parsed_query": {"lang": ["en"]},
  54. "truncated_body": false,
  55. "version": {
  56. "encoding":"encoding/json",
  57. "ext_authz":"v2"
  58. }
  59. }

The parsed_path field in the input is generated from the path field in the HTTP request which is included in the Envoy External Authorization CheckRequest message type. This field provides the request path as a string array which can help policy authors perform pattern matching on the HTTP request path. The below sample policy allows anyone to access the path /people.

  1. package envoy.authz
  2. default allow = false
  3. allow {
  4. input.parsed_path = ["people"]
  5. }

The parsed_query field in the input is also generated from the path field in the HTTP request. This field provides the HTTP URL query as a map of string array. The below sample policy allows anyone to access the path /people?lang=en&id=1&id=2.

  1. package envoy.authz
  2. default allow = false
  3. allow {
  4. input.parsed_path = ["people"]
  5. input.parsed_query.lang = ["en"]
  6. input.parsed_query.id = ["1", "2"]
  7. }

The parsed_body field in the input is generated from the body field in the HTTP request which is included in the Envoy External Authorization CheckRequest message type. This field contains the deserialized JSON request body which can then be used in a policy as shown below.

  1. package envoy.authz
  2. default allow = false
  3. allow {
  4. input.parsed_body.firstname == "Charlie"
  5. input.parsed_body.lastname == "Opa"
  6. }

The truncated_body field in the input represents if the HTTP request body is truncated. The body is considered to be truncated, if the value of the Content-Length header exceeds the size of the request body.

Example with JWT payload passed from Envoy

Envoy can be configured to pass validated JWT payload data into the ext_authz filter with metadata_context_namespaces and payload_in_metadata.

Example Envoy Configuration

  1. http_filters:
  2. - name: envoy.filters.http.jwt_authn
  3. typed_config:
  4. "@type": type.googleapis.com/envoy.config.filter.http.jwt_authn.v2alpha.JwtAuthentication
  5. providers:
  6. example:
  7. payload_in_metadata: verified_jwt
  8. <...>
  9. - name: envoy.ext_authz
  10. config:
  11. metadata_context_namespaces:
  12. - envoy.filters.http.jwt_authn
  13. <...>

Example OPA Input

This will result in something like the following dictionary being added to input.attributes (some common fields have been excluded for brevity):

  1. "metadata_context": {
  2. "filter_metadata": {
  3. "envoy.filters.http.jwt_authn": {
  4. "verified_jwt": {
  5. "email": "alice@example.com",
  6. "exp": 1569026124,
  7. "name": "Alice"
  8. }
  9. }
  10. }
  11. }