Skip to main content

Protecting Applications and APIs in Cloudentity Using Open Policy Agent

Instructions for developers on how to use Rego policies to protect APIs and applications in Cloudentity.

Prerequisites

Create Policy

The video below shows how to create a Rego policy and run it in test mode.

  1. In your workspace, go to Authorization > Policies > + CREATE POLICY.

  2. In the Create Policy popup window

    1. Select the Policy type from the dropdown menu.

    2. Specify the Policy name.

    3. Select REGO as the Policy language.

    4. Select Create.

      Result: The OPA policy editor opens.

  3. To define your policy, enter the policy code in the OPA language into the editor. Check the below request templates for help - they are also available in the right-hand policy menu.

    When ready, Save your policy. You can now assign it to a valid execution point (see the Policy Types table above).

Cloudentity Request Schema

The following schema is valid for all requests coming from Cloudentity. For that reason, it is also used in the policy test mode. Note the three top-level objects:

  • authn_ctx contains the authentication context claims, including scopes.

  • contexts contains dynamic scopes in contexts.scopes.users.*.

  • request contains data specific to the HTTP request itself.

Cloudentity would typically send input resembling the one below to the policy engine:

{
  "authn_ctx": {
    "scp": [
      "scope_name"
    ],
    "sub": "joe",
    "groups": [
      "group_name"
    ],
    "email": "testjoe@cloudentity.com",
    "email_verified": "testjoe@cloudentity.com",
    "phone_number": "+1-555-6616-899",
    "phone_number_verified": "+1-555-6616-899",
    "address": {
      "formatted": "",
      "street_address": "1463  Perry Street",
      "locality": "Dayton",
      "region": "Kentucky",
      "country": "US",
      "postal_code": "41074"
    },
    "name": "Joe Test",
    "given_name": "Joe",
    "middle_name": "",
    "family_name": "Test",
    "nickname": "joe",
    "preferred_username": "testjoe",
    "profile": "",
    "picture": "",
    "website": "",
    "gender": "male",
    "birthdate": "1960-10-09",
    "zoneinfo": "",
    "locale": "",
    "updated_at": ""
  },
  "contexts": {
    "scopes": {
      "users.*": [
        {
          "params": [
            "joe"
          ],
          "requested_name": "users.joe"
        }
      ]
    }
  },
  "request": {
    "headers": {
      "Content-Type": [
        "application/json"
      ],
      "X-Custom-Header": [
        "BOT_DETECTED"
      ]
    },
    "method": "POST",
    "path_params": {
      "users": "admins"
    },
    "query_params": {
      "limit": [
        "1000"
      ],
      "offset": [
        "100"
      ]
    },
    "path": "/doawesomethings"
  }
}

Your policies can verify all data passed in the above schema and validate requests based on it. Check the policy tips below and start writing!

Scope Check Policy

To write a policy checking for a scope in the request, you can use the following template:

package acp.authz

default allow = false
scope := "sample_service:write"

allow {
  input.authn_ctx.scp[_] == scope
}

This policy validates the request when the required scope ("sample_service:write") is found in the authentication context (input.authn_ctx.scp[_]).

Dynamic Scope Check Policy

To write a policy checking for a dynamic scope in the request, you can use the following template:

package acp.authz

default allow = false

allow {
  input.scopes["users.*"][_].params[0] == input.authn_ctx.sub
}

This policy validates the request when the required value is found in the input.scopes object, where dynamic scopes are stored.

HTTP Request Check Policy

To write a policy checking the HTTP request parameters, you can use the following template:

package acp.authz

default allow = false

allow {
  input.request.method == "POST"
  input.request.headers["X-Custom-Header"][_] == "REGULAR_USER"
}

This policy validates the request only for a POST request containing a specific header.

HTTP Header Names Format

REGO policies by their definition are case-sensitive when matching HTTP header names, but Cloudentity authorizers follow the RFC-2616 specification which states that header names are case-insensitive. To allow authorizers to correctly validate REGO policies, header names are normalized to follow the canonical format.

Canonicalization converts the first letter and any letter following the hyphen to upper case and the rest of the letters are converted to lower case.

It means that if a request is to be validated and contains a header like, for example, x-custom-header, before the header is validated, the header is converted to follow the canonical format X-Custom-Header.

As the policy check is case sensitive for REGO policies, your REGO policy that checks request headers must have the header in the canonical format as you can see in the HTTP request check policy example above.

MFA Enforcement Policy

To write a policy checking the MFA validation status of the user, you can use the following template:

package acp.authz

default allow = false

allow {
    input.login.verified_recovery_methods[_] = "mfa"
}

recovery = ["mfa"]

This policy validates the request only if the user has completed MFA. Otherwise, the user is prompted for an OTP code in accordance with the tenant's MFA setup.

HTTP Call Status Check Policy

To write a policy executing an HTTP call and checking the status, you can use the following template:

package acp.authz

default allow = false

allow {
  response := http.send({
    "method" : "GET",
    "url": "https://cloudentity.com/developers/"
  })
  response.status_code == 200

}

This policy validates the request only if the request returns a given status (200 in the above policy).

You have the option to cache the results of this HTTP request, to improve performance, by setting the optional parameters force_cache and force_cache_duration_seconds:

response := http.send({
      "force_cache": true,
      "force_cache_duration_seconds": 30,
      "method" : "GET",
      "url": "https://cloudentity.com/developers/"
    })

Cloudentity authorizers provide an inter-query cache that persists across policy evaluations, which enables calls to http.send() to access cached responses from previous policy checks. The size of this cache can be set in the authorizer's configuration file:

enforcement:
    rego_inter_query_cache_size: 1000000 # maximum size for the Rego inter-query builtin cache

For more information about http.send(), refer to openpolicyagent.org/docs.

Group Membership Check Policy

To write a policy checking for user's group membership, you can use the following template:

package acp.authz

default allow = false
group := "admins"

allow {
  input.authn_ctx.groups[_] == group
}

This policy validates the request only if the admins value is found in the authn_ctx.group object inside the authentication context (i.e. the user is an admin).

Secret Check Policy

You can retrieve a secret value for comparison via Rego policy. The below policy compares the secret value from SECRET_NAME against the name parameter passed in the authentication context:

package acp.authz

default allow = false

allow {
   input.secrets.SECRET_NAME == input.authn_ctx.name

}

This policy validates the request only if the value of a secret called SECRET_NAME matches the value of the name attribute from the authentication context.

Header Injection For Istio Policies

Note

The technique described here works for the Istio authorizer only.

When a policy for the Istio authorizer is resolved, all globally defined policy variables are injected as headers. Such a policy can only be assigned to APIs behind the Istio gateway bound to Cloudentity, therefore it must always have the API request type. Considering we have the following policy:

package acp.authz

default allow = false
subject := input.authn_ctx.sub
expiration := input.authn_ctx.exp
issuer := input.authn_ctx.iss
scopes := input.authn_ctx.scp
tenantid := input.authn_ctx.tid

allow {
  true
}

Upon policy validation, the authentication context values defined as global variables (outside of the allow document) are extracted and injected as headers in the request received by the target service (the values below are encoded):

X-Output-Issuer: Imh0dHBzOi8vYWNwLmFjcC1zeXN0ZW06ODQ0My9kZWZhdWx0L2RlZmF1bHQi
X-Output-Expiration: MTYzNTk2OTQ2OA==
X-Output-Tenantid: ImRlZmF1bHQi
X-Output-Scopes: WyJlbWFpbCIsImludHJvc3BlY3RfdG9rZW5zIiwibGlzdF9jbGllbnRzX3dpdGhfYWNjZXNzIiwibWFuYWdlX2NvbnNlbnRzIiwib2ZmbGluZV9hY2Nlc3MiLCJvcGVuaWQiLCJwcm9maWxlIiwicmV2b2tlX2NsaWVudF9hY2Nlc3MiLCJyZXZva2VfdG9rZW5zIiwidmlld19jb25zZW50cyJd
X-Output-Subject: InVzZXIi

The Istio sidecar configuration and the default Cloudentity headers (X-Output-Allow, X-Auth-Ctx) are injected as well.

Embedded Policies

For REGO policies that are embedded within a Cloudentity policy, if the output contains the same keys, it is merged and the keys are overwritten. The key is set to the key of the last resolved REGO policy.

For example, in your Cloudentity policy there are two embedded REGO policies, A and B. The policy A has headers X and Y, and the B policy has headers Y and Z. The Y header is common for both policies. It's value is set to the value of Y header of the B policy as B is the last REGO policy embedded in the Cloudentity policy. Both the X and the Z headers remain the same.

Policy With Inter-Query Cache

Fetching data from external systems can impact the system's performance because every single call to a protected API includes a new HTTP request. So when your policy requires fetching additional data, you can apply response caching to avoid this impact.

REGO policies that authorize access to APIs via the MicroPerimeter Authorizers can take advantage of the Open Policy Agent (OPA) inter-query cache for HTTP responses. For example, the following REGO policy enables the caching features of the GET HTTP calls and sets the cache response freshness duration to 30 seconds:

package acp.authz

default allow = false

allow {
  response := http.send({
    "force_cache": true,
    "force_cache_duration_seconds": 30,
    "method" : "GET",
    "url": "https://www.google.com/"
  })
  response.status_code == 200
}

For full information on the parameters used, check Open Policy Agent documentation.

If such a policy is used, the authorizer's configuration must include the cache size limit. You can override the default limit by configuring the authorizer's rego_inter_query_cache_size parameter.

enforcement:
  rego_inter_query_cache_size: 1000000