Using Variables to Simplify CEL Expressions
This guide demonstrates how to use Variable CRs to separate compliance data from compliance expressions, making CustomRules more maintainable and easier to customize.
The Problem
When writing CustomRules with CEL expressions, you might need to check
specific values that change across deployments or environments. One good
example of this would be allow-lists or deny-lists. Hardcoding these lists
directly in the CEL expression makes them difficult to maintain:
Potential issues when using this approach:
- Updating the list requires modifying the CustomRule expression
- No separation between the compliance logic and the data being checked
- Difficult for organizations to customize without understanding CEL syntax
- Changes require re-validating the entire CustomRule
Separating Data from Expressions
The Compliance Operator already has a Variable custom resource, which users
have leveraged in the past to tweak the behavior of SCAP/OVAL rules. The same
concept applies with CustomRule objects, where we need to separate the data
from the logic.
Step 1: Create Variable CRs
Create separate Variable resources for each list of values you need to check.
In this example, the Variable value will be a comma-delimited list. Because
we're using a CRD inside the CEL expression, we need to keep the following in
mind:
Variablemetadata.namemust follow Kubernetes RFC 1123 naming (lowercase alphanumeric, hyphens, and dots only)Variableidis used in CEL expressions and must be a valid CEL identifier (alphanumeric only, no hyphens)
Example Variable:
apiVersion: compliance.openshift.io/v1alpha1
kind: Variable
metadata:
name: cluster-admin-users-var
namespace: openshift-compliance
id: cluster-admin-users-var
title: Allowed users for cluster-admin role
description: |-
Comma-delimited list of user names that are permitted to be
bound to the cluster-admin ClusterRoleBinding. Organizations should
customize this list according to their security policies.
Format: ,user1,user2,user3, (comma at start and end for exact matching)
type: string
value: ',kubeadmin,system:admin,alice@my-company.com,'
Notice that the value is prefixed and postfixed with commas, which allows us to do exact matching in the expression.
Step 2: Add Variables as Inputs to Your CustomRule
The CustomRule already accepts Kubernetes inputs via the
kubernetesInputSpec. We can use that here since a Variable is just a custom
resource:
apiVersion: compliance.openshift.io/v1alpha1
kind: CustomRule
metadata:
name: my-custom-rule
namespace: openshift-compliance
spec:
inputs:
- kubernetesInputSpec:
apiVersion: rbac.authorization.k8s.io/v1
resource: clusterrolebindings
name: crbs
- kubernetesInputSpec:
apiVersion: compliance.openshift.io/v1alpha1
resource: variables
resourceName: cluster-admin-users-var
resourceNamespace: openshift-compliance
name: allowedusers
- kubernetesInputSpec:
apiVersion: compliance.openshift.io/v1alpha1
resource: variables
resourceName: cluster-admin-groups-var
resourceNamespace: openshift-compliance
name: allowedgroups
Make sure you're looking for Variable instances in the openshift-compliance
namespace, since they're owned by the Compliance Operator.
Step 3: Reference Variables in Your CEL Expression
Access the Variable value using .value and use .contains() for checking membership:
expression: |-
crbs.items.filter(crb, crb.metadata.name == 'cluster-admin')[0]
.subjects.all(subject,
(subject.kind == 'User' && allowedusers.value.contains(',' + subject.name + ',')) ||
(subject.kind == 'Group' && allowedgroups.value.contains(',' + subject.name + ','))
)
Make sure you're referencing the variable in the expression using the name
from the kubernetesInputSpec. You can access the value directly in the
expression using allowedusers.value. Using the CEL contains() filter with
comma wrapping provides exact matching (e.g., admin won't match kubeadmin).
Step 4: Add Variables to Your Kustomization (Optional)
This gist includes a Kustomization, making it easier to apply the custom rules, variables, tailored profiles, and bindings with a single command.
resources:
- cluster-admin-allowed-users-variable.yaml
- cluster-admin-allowed-groups-variable.yaml
- cluster-admin-allowed-serviceaccounts-variable.yaml
- cluster-admin-allow-list.yaml
- tailored-profile.yaml
- scan-setting-binding.yaml
Apply the example in a cluster with Compliance Operator version 1.8.0 or greater:
See the cluster-admin-allow-list.yaml CustomRule in this gist for a
complete working example that uses three Variables:
allowedusers- allowed usersallowedgroups- allowed groupsallowedserviceaccounts- allowed service accounts (innamespace/nameformat)
Summary
This example walks through how you can use the Variable resource in
conjunction with the new CustomRule resource introduced in Compliance
Operator 1.8.0 to keep compliance data and logic separate.
The primary benefits include:
- Separation of concerns by keeping the compliance logic in the
CustomRuleand data in theVariable - Organizations can update allow-lists by editing
Variables, not CEL expressions - Changing values doesn't risk introducing syntax errors in the CEL expression
Variablescan potentially be referenced by multipleCustomRulesVariabledescriptions explain what values are allowed and how to format them
Best Practices
- Use descriptive
Variablenames it clear what theVariablecontains - Document the format and type of the variable (e.g., string) in the description of the
Variable - Test your expressions by updating the
Variableto ensure they pass and fail with various inputs - Keep
Variablesfocused to one distinct list of values - Keep
Variablesin the same namespace as theCustomRule - Use functions and macros defined by the CEL language definition
Common Patterns
Allow-list Pattern
Check if a value is in an approved list:
Deny-list Pattern
Check if a value is NOT in a prohibited list:
Multiple Conditions
Combine multiple Variable checks:
allowedusers.value.contains(',' + user.name + ',') &&
!deniedgroups.value.contains(',' + user.group + ',')
Updating Variables
To update an allow-list, simply edit the Variable:
Change the value maintaining the comma-delimited format:
The next compliance scan will automatically use the updated values without
requiring any changes to the CustomRule.