Rego Based Policies in Cyral

From time to time, I end up documenting very specific details like my Integrating Cyral with Jira Cloud post. This is another case where I recently had to develop some customer Rego based policies for a specific use case at Cyral. When I first started working at Cyral, I did not know the Rego language. I’ve had to learn it over the years since it’s what drives many of the policy related decisions for our platform. We recently announced a change in our 4.5 release that allows for customers to be able to write their own policies using the Rego language. This post covers an example of building a policy with Rego using this new framework.

Before you continue too far into this post, you should probably first understand the Rego language. I recently added a post Kickstart Your Journey With Rego. If you don’t know what Rego is, then I would suggest checking out this article first.

Cyral Policies

Cyral’s platform has two ways to enforce policy on connections through the sidecar. One version is referred to as Global Policies or internally as YAML Policies. The other version is referred to as Local Policies or referred to internally as Rego Policies.

Global Policies is the name used in the Cyral Control Plane. We also refer to them as YAML Policies because this is the format used to create them. Documentation regarding these policies can be found in the Overview | Cyral Docs . For those already familiar with these policies, they make use of a data map within each repo. Customers then write their policies against the labels in each data map.

Local Policies is used because the policies are configured within each repo rather than globally like Global Policies. We also refer to them as Rego Policies because they are created using the Rego Language.

Details regarding the the Rego Language are outside the scope of this document. We suggest reviewing the Rego Language documentation for syntax and examples.
Rego works on a basic principle of testing whether something is true.
In addition, you can use the Rego Playground to test your policy template before using.

Policy Template Structure

While the Rego language allows for users to write their policies in a flexible language, our policy evaluation engine requires those policies to provide a certain set of conditions in order for policies to be properly evaluated. As noted above, Rego can be used to test for certain conditions to be true. The first set of conditions our policy evaluation engine uses are the policyConditions and policyDecision.

The policyConditions determines whether a particular policy should be evaluated or not. This basic test can be used as a quick way to determine whether a particular request should be evaluated. The policyDecisionis used to indicate the result of evaluating the policy for a certain request or activity.

As this document continues to explain the structure of policy templates and instances, the explanations are presented in a JSON style format for better understanding of the structure of each condition.

policyConditions

The policyConditions captures the conditions under which the policy is applicable for an activity. A Rego policy template can (optionally) return this information as the result of the policyConditions query. This is an optimization that avoids unnecessary Rego evaluation in the data path. An example of the policyConditions structure is below:

"policyConditions": {
    {
        "activityTypes": ["query", ...],
        "labels": ["CCN","SSN", ...],
        "tags": ["GDPR", ...],
        "attributes": ["schema.table.column", ...]
        "inclusions": [
            {
                "userId": "user1",
                "userEmail": "[email protected]",
                "group": "Developers",
                "repoAccount": "ro_db_user",
                "repo": "repo1"
            },
            ...
        ],
        "exclusions": [
            {
                "userId": "user2",
                "userEmail": "[email protected]",
                "group": "Administrators",
                "repoAccount": "admin",
                "repo": "repo2"
            },
            ...
        ],
    }
}

These policyConditions are mapped against various fields in the logs. The below table provides a description of the condition and the log field used for the condition check. Additional details regarding Cyral logs can be found in the Log specification | Cyral Docs .

Name Description Log Path (expressed as a JSON path)
activityTypes This is an array of the different activityTypes that this policy should evaluate. $.activityTypes
labels These are labels added via a data map for the repo. $.request.fieldsAccessed[].label
tags ??? ???
attributes ??? ???
inclusions This is an array of identities/repos that the policy should ONLY apply to. $.identity.endUser$.identity.userEmail$.identity.repoUser$.identity.group$.repo.name
exclusions This is an array of identities/repos that the policy should NOT apply to. $.identity.endUser$.identity.userEmail$.identity.repoUser$.identity.group$.repo.name

The policyConditions should be used so that only the requests that need to be evaluated should be evaluated. For instance, to control access to certain data, you should use the activityTypes used to match on query like shown above so that connection requests are not evaluated.

policyDecision

The policyDecision captures the decision of a single policy template for an activity. This decision is used to determine what should be done if the policy template’s criteria has matched. An example of the policyDecisions structure is below

"policyDecision": {
    "requestViolation": {
        "cause": "message",
        "severity": "low",
        "actions": {
            "alert": true,
            "block": false
        }
    },
    "responseEvaluator": {
        "maxCount": 0,
        "responseViolation": {
            "cause": "message",
            "severity": "low",
            "actions": {
                "alert": true,
                "block": false
            }
        }
    },
    "requestModifiers": {
      "datasetRewrites": {
        "a": "b"
      },
      "masks": {
        "a.b.c": {
            "maskFunction": "constant_mask",
            "args": ["x", true, 57]
        }
      }
    },
}

The possible outcomes of the policyDecision are explained in the below table.

Outcome Description
requestViolation This triggers a block due to an evaluation of the request
responseEvaluator.maxCount This is used to help the policy evaluation keep an eye on rows returned and whether that’ve exceeded the limit.
responseEvaluator.responseViolation This triggers a block due to an evaluation of the response
requestModifiers This will not trigger a block but will make changes to the request such as masking or rewrites.

Policy Input

In addition to the policy template structure above, the Local Policies allow for inputs to them. The two inputs into the policy templates are either via input or data.policyParams. The input is something users are not able to change. This is generated by the sidecar and is structured similar to the below:

"input": {
  "activityLog" : {
    ...entire Cyral activity log...
  }
}

This allows for policy templates to be written against anything contained in the Cyral activity logs.

The dat.policyParams. input is used to provide user settings. These can be changes to the policyConditions or additional logic that you create in a template.

Policy Template Creation

The first step in creating a Local Policy is that you must first create a policy template. We’ll start with a simple policy template like the below that is written in Rego.

policyConditions = {
	"activityTypes": ["query"]
}

policyApplies {
	input.activityLogs.identity.endUser == "blocked_user"
}

default policyDecision = {}

policyDecision = {
	"requestViolation": {
		"actions": {
			"alert": true,
			"block": false,
		},
		"cause": "Blocked User Detected",
		"severity": "low"
	}
} {
	policyApplies
}

The policyConditions will make sure this policy template is only evaluated against requests that the sidecar considers a query.

The policyApplies test is checking whether the endUser issuing the query is set to blocked_user. If the query is from blocked_user, then policyApplies will evaluate to true. Otherwise, it will evaluate to false.

The policy evaluator is looking for policyDecision to contain data AND for that data to match the structure noted above. In the example above, default value is set to {} so the policy evaluator does not enforce the policy. We then perform the check of policyDecision = { ... } { policyApplies }to set policyDecision only if policyApplies evaluates to true.

The result of this basic policy template would be to generate a policy violation alert of Blocked User Detected with a severity of low. If we wanted this policy to block the user, then block should be changed to truelike the below example

policyConditions = {
	"activityTypes": ["query"]
}

policyApplies {
	input.activityLogs.identity.endUser == "blocked_user"
}

default policyDecision = {}

policyDecision = {
	"requestViolation": {
		"actions": {
			"alert": true,
			"block": true,
		},
		"cause": "Blocked User Detected",
		"severity": "low"
	}
} {
	policyApplies
}

Allow Parameters to the Policy Template

While the above is quite useful, it is very static and does not allow us to customize this for reuse in multiple policies. In order to make this more useful in policies, we’ll want to add some configurable items to the policy template:

policyConditions = {
	"activityTypes": ["query"]
}

default block = false

block {
	data.policyParams.block
}

policyApplies {
	input.activityLogs.identity.endUser == data.policyParams.blocked_user
}

default policyDecision = {}

policyDecision = {
	"requestViolation": {
		"actions": {
			"alert": true,
			"block": block,
		},
		"cause": "Blocked User Detected",
		"severity": data.policyParams.alertSeverity
	}
} {
	policyApplies
}

This template has the following updates:

  • Line 5 : This is defining a new variable called block that is set to false by default so that the policy does not block any traffic.
  • Lines 7 – 9 : This is reading the block parameter from the policy configuration. If the value is not provide, then it’ll remain false.
  • Line 12 : This is changing our endUser search to be read from the policy parameters.
  • Line 21 : Now the decision to block the request is controlled by the block variable.
  • Line 24 : This allows the severity level to be configured by the policy parameters.

The logic of the rule is not changed at this point but it does allow for reuse.

Creating a Policy Parameters Schema

Now that the policy is allowing parameters, you will also need to make sure that there’s validation of those parameters for policy creation. Below is an example schema that can be used for the above

{
    "$schema": "https://json-schema.org/schema#",
    "$id": "https://www.cyral.com/schemas/example-paramschema.json",
    "type": "object",
    "properties": {
        "block": {
            "type": "boolean",
            "default": false
        },
        "blocked_user" : {
            "type": "string",
            "minLength": 1
        },
        "alertSeverity": {
            "type": "string",
            "enum": [
                "low",
                "medium",
                "high"
            ]
        }
    },
    "allOf": [
        {
            "required": [
                "alertSeverity",
                "blocked_user"
            ]
        }
    ]
}

The $schema and type are fixed and should be the same across any policy templates that are created. The $id should be unique to each policy template as a best practice and does not need to be a valid location.

The properties block is required and is used to define what parameters are accepted into the policy template. The keys in the properties object should be the name of each parameter. We’ve defined the block, blocked_user, and alertSeverity parameters that we’re using in the policy template example.

The allOf is defining which parameters are required in a policy that uses the template. In this case, we’re requiring the blocked_user and alertSeverity parameters to be defined in any created policy.

Additional details regarding this schema are outside the scope of this document. You can review the Understanding JSON Schema — Understanding JSON Schema 2020-12 documentation page for additional details.

Adding the Policy Template to the Cyral Control Plane

With the rego policy and schema defined, you are now able to put the policy template into the Cyral Control Plane to be used for the creation of policies. In order to create the policy template in the Cyral Control Plane, you need to make a POST request to the /v1/regopolicies/templates/{{localPolicyCategory}}/{{template_name}} endpoint. As of this document’s writing, the only supported localPolicyCategory is SECURITY. The template_name can be any - spaced name. For the purposes of our example, we’ll use the block-user-access as the template_name.

Issuing a PUT request instead of POST can be used to update an existing policy template. Issuing a DELETE can be used to delete a policy template.

The payload of the request should follow the below format

{
  "codeBlob": "string",
  "description": "string",
  "name": "string",
  "override": false,
  "parametersSchema": "string",
  "tags": [
    "string"
  ],
  "version": "string"
}

Most of the fields are self explanatory except for the codeBlob and parametersSchema fields. These fields are JSON encoded strings of our Rego code (codeBlob) and the JSON parameter schema (parametersSchema) defined above. Below is an example JSON payload using our example code from above:

{
  "codeBlob": "policyConditions = {\n\t\"activityTypes\": [\"query\"]\n}\n\ndefault block = false\n\nblock {\n\tdata.policyParams.block\n}\n\npolicyApplies {\n\tinput.activityLogs.identity.endUser == data.policyParams.blocked_user\n}\n\ndefault policyDecision = {}\n\npolicyDecision = {\n\t\"requestViolation\": {\n\t\t\"actions\": {\n\t\t\t\"alert\": true,\n\t\t\t\"block\": block,\n\t\t},\n\t\t\"cause\": \"Blocked User Detected\",\n\t\t\"severity\": data.policyParams.alertSeverity\n\t}\n} {\n\tpolicyApplies\n}\n",
  "description": "This is an example policy template that can be used to block a specific user",
  "name": "Policy to Block a Specific User",
  "override": false,
  "parametersSchema": "{\n    \"$schema\": \"https://json-schema.org/schema#\",\n    \"$id\": \"https://www.cyral.com/schemas/example-paramschema.json\",\n    \"type\": \"object\",\n    \"properties\": {\n        \"block\": {\n            \"type\": \"boolean\",\n            \"default\": false\n        },\n        \"blocked_user\" : {\n            \"type\": \"string\",\n            \"minLength\": 1\n        },\n        \"alertSeverity\": {\n            \"type\": \"string\",\n            \"enum\": [\n                \"low\",\n                \"medium\",\n                \"high\"\n            ]\n        }\n    },\n    \"allOf\": [\n        {\n            \"required\": [\n                \"alertSeverity\",\n                \"blocked_user\"\n            ]\n        }\n    ]\n}\n",
  "tags": [
    "Security"
  ],
  "version": "v1.0.0"
}

The POST request would look something like

curl -X POST https://customer.control.plane/v1/regopolicies/templates/SECURITY/block-user-access -d '{
  "codeBlob": "policyConditions = {\n\t\"activityTypes\": [\"query\"]\n}\n\ndefault block = false\n\nblock {\n\tdata.policyParams.block\n}\n\npolicyApplies {\n\tinput.activityLogs.identity.endUser == data.policyParams.blocked_user\n}\n\ndefault policyDecision = {}\n\npolicyDecision = {\n\t\"requestViolation\": {\n\t\t\"actions\": {\n\t\t\t\"alert\": true,\n\t\t\t\"block\": block,\n\t\t},\n\t\t\"cause\": \"Blocked User Detected\",\n\t\t\"severity\": data.policyParams.alertSeverity\n\t}\n} {\n\tpolicyApplies\n}\n",
  "description": "This is an example policy template that can be used to block a specific user",
  "name": "Policy to Block a Specific User",
  "override": false,
  "parametersSchema": "{\n    \"$schema\": \"https://json-schema.org/schema#\",\n    \"$id\": \"https://www.cyral.com/schemas/example-paramschema.json\",\n    \"type\": \"object\",\n    \"properties\": {\n        \"block\": {\n            \"type\": \"boolean\",\n            \"default\": false\n        },\n        \"blocked_user\" : {\n            \"type\": \"string\",\n            \"minLength\": 1\n        },\n        \"alertSeverity\": {\n            \"type\": \"string\",\n            \"enum\": [\n                \"low\",\n                \"medium\",\n                \"high\"\n            ]\n        }\n    },\n    \"allOf\": [\n        {\n            \"required\": [\n                \"alertSeverity\",\n                \"blocked_user\"\n            ]\n        }\n    ]\n}\n",
  "tags": [
    "Security"
  ],
  "version": "v1.0.0"
}'

The result from the Control Plane would be similar to:

{
    "id": "block-user-access",
    "category": "SECURITY"
}

Creating a Policy Using the Template

With the policy template added in the Control Plane, you can now create policies that will enforce what is in the template. Creating a policy requires a POST request to the /v1/regopolicies/instances/{{localPolicyCategory}} endpoint. The payload of the request should be in the following format

{
  "instance": {
    "description": "string",
    "enabled": true,
    "name": "string",
    "parameters": "string",
    "scope": {
      "repoIds": [
        "string"
      ]
    },
    "tags": [
      "string"
    ],
    "templateId": "string"
  }
}

Again, most of these fields are self explanatory. The parameters should be a JSON encode string that contains the policy parameters that are defined in the policy template. The scope.repoIds array will be a list of repository Ids where this policy should be applied. The templateId should be the template_name defined for the policy template. Continuing with our example, our POST would look something similar to the below

{
  "instance": {
    "description": "This policy will block the restricted_user in two of our repos",
    "enabled": true,
    "name": "Block restricted_user",
    "parameters": "{\"block\": true,\"blocked_user\": \"restricted_user\",\"alertSeverity\":\"medium\"}",
    "scope": {
      "repoIds": [
        "abcd...1234",
        "wxyz...6789"
      ]
    },
    "tags": [
      "Security"
    ],
    "templateId": "block-user-access"
  }
}

The POST request would look something like

curl -X POST https://customer.control.plane/v1/regopolicies/instances/SECURITY -d '{
  "instance": {
    "description": "This policy will block the restricted_user in two of our repos",
    "enabled": true,
    "name": "Block restricted_user",
    "parameters": "{\"block\": true,\"blocked_user\": \"restricted_user\",\"alertSeverity\":\"medium\"}",
    "scope": {
      "repoIds": [
        "abcd...1234",
        "wxyz...6789"
      ]
    },
    "tags": [
      "Security"
    ],
    "templateId": "block-user-access"
  }
}'

The result from the Control Plane would be similar to:

{
    "id": "2R...IU",
    "category": "SECURITY"
}

The id included in this response is the policyID and can be used in future updates to this policy. In order to update the policy, you can issue a PUT request to /v1/regopolicies/instances/SECURITY/{{policyID}} using a similar payload as above including any changes. Issuing a DELETE to the /v1/regopolicies/instances/SECURITY/{{policyID}}will delete the local policy.