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 policyDecision
is 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 .
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.
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 true
like 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 tofalse
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 remainfalse
. - 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": "http://json-schema.org/schema#",
"$id": "http://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\": \"http://json-schema.org/schema#\",\n \"$id\": \"http://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\": \"http://json-schema.org/schema#\",\n \"$id\": \"http://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.