Part 2: Policy
What Will You Do¶
In this part of the self-paced exercise, you will create and enforce a "policy" comprised of two OPA Gatekeeper constraints. Ensure that
- Container resource limits to requests does not surpass a specified ratio
- Only container images from authorized/approved repositories are allowed to operate on clusters
OPA Constraint Framework¶
Gatekeeper uses the OPA Constraint Framework to describe and enforce policy. Look there for more detailed information on their semantics and advanced usage.
Constraint¶
A constraint is a declaration that its author wants a system to meet a given set of requirements. Each Constraint is written with Rego, a declarative query language used by OPA to enumerate instances of data that violate the expected state of the system. All Constraints are evaluated as a Logical AND. If one Constraint is not satisfied, then the whole request is rejected.
In the example below, the constraint requires users to specify labels for k8s namespaces.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: ns-must-have-label-owner
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters:
labels: ["owner"]
Constraint Template¶
Before defining a Constraint, you need to create a Constraint Template that allows people to declare new Constraints. Each template describes both the Rego logic that enforces the Constraint and the schema for the Constraint, which includes the schema of the CRD and the parameters that can be passed into a Constraint, much like arguments to a function.
Example:
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
listKind: K8sRequiredLabelsList
plural: k8srequiredlabels
singular: k8srequiredlabels
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
labels:
type: array
items: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("you must provide labels: %v", [missing])
}
Step 1: Create Constraint Templates¶
In this step, we will create two "custom" constraint templates. The constraint templates will control the allowed repositories where images can be obtained from and sets the maximum ratio for container resource limits to requests.
Creating a Constraint Template to allow container images from approved repositories¶
- Login to the Controller and select Constraint Templates under OPA Gatekeeper specification
- Click New Template
- Provide a name for the template (e.g. rafay-gatekeeper-allowedrepos-constraint-template)
- Select the Upload files manually option and upload the below yaml file
- Click Save & Exit
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: opak8sallowedrepos
annotations:
description: Requires container images to begin with a repo string from a specified
list.
spec:
crd:
spec:
names:
kind: OPAK8sAllowedRepos
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = re_match(repo, container.image)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = re_match(repo, container.image)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
}
Creating a Constraint Template to check for container resource limits to requests ratio¶
- Login to the Controller and select Constraint Templates under OPA Gatekeeper specification
- Click New Template
- Provide a name for the template (e.g. rafay-request-limit-ratio-constraint-template)
- Select the Upload files manually option and upload the below yaml file
- Click Save & Exit
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: opak8scontainerratios
annotations:
description: Sets a maximum ratio for container resource limits to requests.
spec:
crd:
spec:
names:
kind: OPAK8sContainerRatios
validation:
openAPIV3Schema:
properties:
ratio:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8scontainerratios
missing(obj, field) = true {
not obj[field]
}
missing(obj, field) = true {
obj[field] == ""
}
canonify_cpu(orig) = new {
is_number(orig)
new := orig * 1000
}
canonify_cpu(orig) = new {
not is_number(orig)
endswith(orig, "m")
new := to_number(replace(orig, "m", ""))
}
canonify_cpu(orig) = new {
not is_number(orig)
not endswith(orig, "m")
re_match("^[0-9]+$", orig)
new := to_number(orig) * 1000
}
canonify_cpu(orig) = new {
not is_number(orig)
not endswith(orig, "m")
re_match("^[0-9]+[.][0-9]+$", orig)
new := to_number(orig) * 1000
}
# 10 ** 21
mem_multiple("E") = 1000000000000000000000 { true }
# 10 ** 18
mem_multiple("P") = 1000000000000000000 { true }
# 10 ** 15
mem_multiple("T") = 1000000000000000 { true }
# 10 ** 12
mem_multiple("G") = 1000000000000 { true }
# 10 ** 9
mem_multiple("M") = 1000000000 { true }
# 10 ** 6
mem_multiple("k") = 1000000 { true }
# 10 ** 3
mem_multiple("") = 1000 { true }
# Kubernetes accepts millibyte precision when it probably shouldn't.
# https://github.com/kubernetes/kubernetes/issues/28741
# 10 ** 0
mem_multiple("m") = 1 { true }
# 1000 * 2 ** 10
mem_multiple("Ki") = 1024000 { true }
# 1000 * 2 ** 20
mem_multiple("Mi") = 1048576000 { true }
# 1000 * 2 ** 30
mem_multiple("Gi") = 1073741824000 { true }
# 1000 * 2 ** 40
mem_multiple("Ti") = 1099511627776000 { true }
# 1000 * 2 ** 50
mem_multiple("Pi") = 1125899906842624000 { true }
# 1000 * 2 ** 60
mem_multiple("Ei") = 1152921504606846976000 { true }
get_suffix(mem) = suffix {
not is_string(mem)
suffix := ""
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) > 0
suffix := substring(mem, count(mem) - 1, -1)
mem_multiple(suffix)
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) > 1
suffix := substring(mem, count(mem) - 2, -1)
mem_multiple(suffix)
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) > 1
not mem_multiple(substring(mem, count(mem) - 1, -1))
not mem_multiple(substring(mem, count(mem) - 2, -1))
suffix := ""
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) == 1
not mem_multiple(substring(mem, count(mem) - 1, -1))
suffix := ""
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) == 0
suffix := ""
}
canonify_mem(orig) = new {
is_number(orig)
new := orig * 1000
}
canonify_mem(orig) = new {
not is_number(orig)
suffix := get_suffix(orig)
raw := replace(orig, suffix, "")
re_match("^[0-9]+$", raw)
new := to_number(raw) * mem_multiple(suffix)
}
violation[{"msg": msg}] {
general_violation[{"msg": msg, "field": "containers"}]
}
violation[{"msg": msg}] {
general_violation[{"msg": msg, "field": "initContainers"}]
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
cpu_orig := container.resources.limits.cpu
not canonify_cpu(cpu_orig)
msg := sprintf("container <%v> cpu limit <%v> could not be parsed", [container.name, cpu_orig])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
mem_orig := container.resources.limits.memory
not canonify_mem(mem_orig)
msg := sprintf("container <%v> memory limit <%v> could not be parsed", [container.name, mem_orig])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
cpu_orig := container.resources.requests.cpu
not canonify_cpu(cpu_orig)
msg := sprintf("container <%v> cpu request <%v> could not be parsed", [container.name, cpu_orig])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
mem_orig := container.resources.requests.memory
not canonify_mem(mem_orig)
msg := sprintf("container <%v> memory request <%v> could not be parsed", [container.name, mem_orig])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not container.resources
msg := sprintf("container <%v> has no resource limits", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not container.resources.limits
msg := sprintf("container <%v> has no resource limits", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
missing(container.resources.limits, "cpu")
msg := sprintf("container <%v> has no cpu limit", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
missing(container.resources.limits, "memory")
msg := sprintf("container <%v> has no memory limit", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not container.resources.requests
msg := sprintf("container <%v> has no resource requests", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
missing(container.resources.requests, "cpu")
msg := sprintf("container <%v> has no cpu request", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
missing(container.resources.requests, "memory")
msg := sprintf("container <%v> has no memory request", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
cpu_limits_orig := container.resources.limits.cpu
cpu_limits := canonify_cpu(cpu_limits_orig)
cpu_requests_orig := container.resources.requests.cpu
cpu_requests := canonify_cpu(cpu_requests_orig)
cpu_ratio := input.parameters.ratio
to_number(cpu_limits) > to_number(cpu_ratio) * to_number(cpu_requests)
msg := sprintf("container <%v> cpu limit <%v> is higher than the maximum allowed ratio of <%v>", [container.name, cpu_limits_orig, cpu_ratio])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
mem_limits_orig := container.resources.limits.memory
mem_requests_orig := container.resources.requests.memory
mem_limits := canonify_mem(mem_limits_orig)
mem_requests := canonify_mem(mem_requests_orig)
mem_ratio := input.parameters.ratio
to_number(mem_limits) > to_number(mem_ratio) * to_number(mem_requests)
msg := sprintf("container <%v> memory limit <%v> is higher than the maximum allowed ratio of <%v>", [container.name, mem_limits_orig, mem_ratio])
}
Step 2: Create Constraints¶
In this step, we will create two constraints that are associated with the previously created constraint templates.
Creating a Constraint to allow container images from approved repositories¶
- Login to the Controller and select Constraints under OPA Gatekeeper specification
- Click New Constraint
- Provide a name for the constraint (e.g. rafay-gatekeeper-allowedrepos-constraint) and select the Constraint Template (rafay-gatekeeper-allowedrepos-constraint-template)
- Specify the version name (e.g. v1)
- Select the Upload files manually option and upload the below yaml file
- Click Save Changes
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: OPAK8sAllowedRepos
metadata:
name: opa-allowed-repos
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "amazonaws.com" # ECR registry for EKS cluster
- "k8s.gcr.io" # Kubernetes registry
- "docker.io" # bitnami registry
Creating a Constraint to check for container resource limits to requests ratio¶
- Login to the Controller and select Constraints under OPA Gatekeeper specification
- Click New Constraint
- Provide a name for the constraint (e.g. rafay-request-limit-ratio-constraint) and select the Constraint Template (rafay-request-limit-ratio-constraint-template)
- Specify the version name (e.g. v1)
- Select the Upload files manually option and upload the below yaml file
- Click Save Changes
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: OPAK8sContainerRatios
metadata:
name: opa-container-must-meet-ratio
spec:
enforcementAction: warn
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
ratio: "1"
Step 3: Create Policy¶
In this step, we will create a policy which will assemble together multiple constraints that were created in the prior steps.
- Login to the Controller and select Policies under OPA Gatekeeper section
- Click New Policy, provide a name (e.g. opa-gs-policy) and a version (e.g. v1)
- Click Add Constraint and select the constraints previously created along with their versions
- Click Save Changes
Recap¶
Congratulations! At this point, you have successfully created a policy with two custom constraints. In the next step, you will add this policy to a cluster blueprint and deploy it on a fleet of managed clusters.
Step 1: Create Constraint Templates¶
In this step, we will create two "custom" constraint templates. The constraint templates will control the allowed repositories where images can be obtained from and sets the maximum ratio for container resource limits to requests.
- Open Terminal (on macOS/Linux) or Command Prompt (Windows) and navigate to the folder where you forked the Git repository
- Navigate to the folder "
/getstarted/opa_gatekeeper/constraint_templates"
The "constraint-template-1.yaml" and "constraint-template-2.yaml" files contain the declarative specifications for our two constraint templates. These specification files point to the constraint template definitions located in the "rafay-gatekeeper-allowedrepos-constraint-template.yaml" and "rafay-request-limit-ratio-constraint-template.yaml" files.
The below YAML shows the declarative specification details found in the "constraint-template-1.yaml" file.
Important
Ensure you update the "project: defaultproject" with the name of the project in your Org
apiVersion: opa.k8smgmt.io/v3
kind: OPAConstraintTemplate
metadata:
labels:
rafay.dev/opa: template
name: rafay-gatekeeper-allowedrepos-constraint-template
project: defaultproject
spec:
artifact:
artifact:
paths:
- name: file://rafay-gatekeeper-allowedrepos-constraint-template.yaml
options: {}
type: Yaml
The below YAML shows the constraint template definition found in the "rafay-gatekeeper-allowedrepos-constraint-template.yaml" file
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: opak8sallowedrepos
annotations:
description: Requires container images to begin with a repo string from a specified
list.
spec:
crd:
spec:
names:
kind: OPAK8sAllowedRepos
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = re_match(repo, container.image)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = re_match(repo, container.image)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
}
- Type the commands below to create the constraint templates
rctl apply -f constraint-template-1.yaml
rctl apply -f constraint-template-2.yaml
If you did not encounter any errors, you can optionally verify if everything was created correctly on the controller.
- Navigate to the "defaultproject" project in your Org
- Select OPA Gatekepper -> Constraint Templates
Step 2: Create Constraints¶
In this step, we will create two constraints that are assocaited with the previously created constraint templates.
- Open Terminal (on macOS/Linux) or Command Prompt (Windows) and navigate to the folder where you forked the Git repository
- Navigate to the folder "
/getstarted/opa_gatekeeper/constraints"
The "constraint-1.yaml" and "constraint-2.yaml" files contain the declarative specification for our two constraints. These specification files point to the constraint definitions located in the "rafay-gatekeeper-allowedrepos-constraint.yaml" and "rafay-request-limit-ratio-constraint.yaml" files.
The below YAML shows the declarative specification details found in the "constraint-1.yaml" file
Important
Ensure you update the "project: defaultproject" with the name of the project in your Org
apiVersion: opa.k8smgmt.io/v3
kind: OPAConstraint
metadata:
labels:
rafay.dev/opa: constraint
name: rafay-gatekeeper-allowedrepos-constraint
project: defaultproject
spec:
artifact:
artifact:
paths:
- name: file://rafay-gatekeeper-allowedrepos-constraint.yaml
options: {}
type: Yaml
published: true
templateName: rafay-gatekeeper-allowedrepos-constraint-template
version: v1
The below YAML shows the constraint template definition found in the "rafay-gatekeeper-allowedrepos-constraint.yaml" file
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: OPAK8sAllowedRepos
metadata:
name: opa-allowed-repos
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "amazonaws.com" # ECR registry for EKS cluster
- "k8s.gcr.io" # Kubernetes registry
- "docker.io" # bitnami registry
- Type the commands below to create the constraints
rctl apply -f constraint-1.yaml
rctl apply -f constraint-2.yaml
If you did not encounter any errors, you can optionally verify if everything was created correctly on the controller.
- Navigate to the "defaultproject" project in your Org
- Select OPA Gatekepper -> Constraints
Step 3: Create Policy¶
In this step, we will create a policy which will assemble together multiple constraints that were created in the prior steps.
- Open Terminal (on macOS/Linux) or Command Prompt (Windows) and navigate to the folder where you forked the Git repository
- Navigate to the folder "
/getstarted/opa_gatekeeper/policy"
The "policy.yaml" file contains the declarative specification for the policy. This specification file refers to the previously created constraints as part of the defintition of the policy.
The below YAML shows the declarative specification details found in the "policy.yaml" file
Important
Ensure you update the "project: defaultproject" with the name of the project in your Org
apiVersion: opa.k8smgmt.io/v3
kind: OPAPolicy
metadata:
name: opa-gs-policy
project: defaultproject
spec:
constraintList:
- name: rafay-request-limit-ratio-constraint
version: v1
- name: rafay-gatekeeper-allowedrepos-constraint
version: v1
sharing:
enabled: true
projects:
- name: defaultproject
version: opa-gs-policy-version
- Type the command below to create the policy
rctl apply -f policy.yaml
If you did not encounter any errors, you can optionally verify if everything was created correctly on the controller.
- Navigate to the "defaultproject" project in your Org
- Select OPA Gatekeeper -> Policies
Recap¶
Congratulations! At this point, you have successfully created a policy with two custom constraints. In the next step, you will add this policy to a cluster blueprint and deploy it on a fleet of managed clusters.