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
Understanding of the Rego syntax
Create Policy
The video below shows how to create a Rego policy and run it in test mode.
In your workspace, go to Authorization > Policies > + CREATE POLICY.
In the Create Policy popup window
Select the Policy type from the dropdown menu.
Specify the Policy name.
Select REGO as the Policy language.
Select Create.
Result: The OPA policy editor opens.
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 incontexts.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