diff --git a/doc/tutorials/workshop/content/exercises/09-writing-rules.md b/doc/tutorials/workshop/content/exercises/09-writing-rules.md index cd77219e8..cae198a50 100644 --- a/doc/tutorials/workshop/content/exercises/09-writing-rules.md +++ b/doc/tutorials/workshop/content/exercises/09-writing-rules.md @@ -18,8 +18,8 @@ so let's focus instead on writing content for OpenShift. There is a handy tool in the `utils` directory that will help you create such rules and test them locally or against an existing cluster: [` -./utils/add_platform_rule.py`]( -https://github.com/ComplianceAsCode/content/blob/master/utils/add_platform_rule.py). +./utils/add_kubernetes_rule.py`]( +https://github.com/ComplianceAsCode/content/blob/master/utils/add_kubernetes_rule.py). Let's take it into use! @@ -52,7 +52,7 @@ value. ``` $ source .pyenv.sh # Configure PYTHONPATH for CaC modules -$ ./utils/add_platform_rule.py create platform\ +$ ./utils/add_kubernetes_rule.py create platform\ --rule must_have_compliant_cm \ --name my-compliance-configmap --namespace openshift --type configmap \ --title "Must have compliant CM" \ @@ -116,7 +116,7 @@ $ oc project openshift-compliance We can test the rule as follows: ``` -$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm +$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm ``` This command will: @@ -139,7 +139,7 @@ accept values such as `yessss` or `yeah`. In this case, we'll need to adjust our little: ``` -$ ./utils/add_platform_rule.py create platform\ +$ ./utils/add_kubernetes_rule.py create platform\ --rule must_have_compliant_cm \ --name my-compliance-configmap --namespace openshift --type configmap \ --title "Must have compliant CM" \ @@ -162,7 +162,7 @@ $ grep operation applications/openshift/must_have_compliant_cm/rule.yml Let's test it out and see that the pattern still matches: ``` -$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm +$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm ... * The result is 'COMPLIANT' ``` @@ -180,7 +180,7 @@ $ oc patch -n openshift configmap my-compliance-configmap \ And let's verify that it still matches: ``` -$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm +$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm ... * The result is 'COMPLIANT' ``` @@ -190,12 +190,16 @@ For completeness, lets modify the `ConfigMap` to have a non-compliant value and ``` $ oc patch -n openshift configmap my-compliance-configmap \ -p '{"data": {"compliant": "hehehe nope"}}' --type=merge -$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm +$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm ... * The result is 'NON-COMPLIANT' ``` Once you've tested your rule and feel its in a good shape, you should fill in the missing parameters from the template so they'll appear nicely in the report. Finally, you can add -the rule to a relevant profile in the `ocp4/profiles/` directory, build it, upload it to a +the rule to a relevant profile in the `ocp4/profiles/` directory, or a control file in +`controls/` directory, build it, upload it to a `ProfileBundle` and take it into use as part of your regular compliance scans! + +In the next section we will learn how to make a rule more flexible to our needs +[with variables](10-rule-parametrization.md). diff --git a/doc/tutorials/workshop/content/exercises/10-rule-parametrization.md b/doc/tutorials/workshop/content/exercises/10-rule-parametrization.md index b0eae3740..5a740f8fb 100644 --- a/doc/tutorials/workshop/content/exercises/10-rule-parametrization.md +++ b/doc/tutorials/workshop/content/exercises/10-rule-parametrization.md @@ -70,7 +70,7 @@ EOF Then run the following command to change the rule to use the variable we just created: ``` -$ ./utils/add_platform_rule.py create platform\ +$ ./utils/add_kubernetes_rule.py create platform\ --rule must_have_compliant_cm \ --name my-compliance-configmap --namespace openshift --type configmap \ --title "Must have compliant CM" \ @@ -90,14 +90,14 @@ $ oc patch -n openshift configmap my-compliance-configmap \ -p '{"data": {"compliant": "yep"}}' --type=merge ``` ``` -$ ./utils/add_platform_rule.py cluster-test --rule must_have_compliant_cm +$ ./utils/add_kubernetes_rule.py cluster-test --rule must_have_compliant_cm ... * The result is 'COMPLIANT' ``` ### Testing Rules with Profile Tailorings -So far we have been using the `./utils/add_platform_rule.py` script to test +So far we have been using the `./utils/add_kubernetes_rule.py` script to test our rule. It creates very specific `ComplianceScans` that cannot cover all the use cases. @@ -187,3 +187,25 @@ my-own-profile DONE COMPLIANT Our rule is ready to be enabled in multiple profiles checking different values in each `Profile`. + +To check that rule fails as expected, let's change the `ConfigMap` to an incompliant value: +``` +$ oc patch -n openshift configmap my-compliance-configmap \ + -p '{"data": {"compliant": "nope"}}' --type=merge +configmap/my-compliance-configmap patched +``` + +Then manually start a re-scan: +`$ oc annotate compliancescan/my-own-profile compliance.openshift.io/rescan=` + +And follow the scan status and check that is indeed not compliant: +``` +$ oc get scan -w +NAME PHASE RESULT +my-own-profile RUNNING NOT-AVAILABLE +my-own-profile AGGREGATING NOT-AVAILABLE +my-own-profile AGGREGATING NOT-AVAILABLE +my-own-profile DONE NON-COMPLIANT +``` + +Next we will learn what are [node rules](11-node-rules.md) and how do they differ from platform rules. diff --git a/doc/tutorials/workshop/content/exercises/11-node-rules.md b/doc/tutorials/workshop/content/exercises/11-node-rules.md index 02f819dea..cc3eed569 100644 --- a/doc/tutorials/workshop/content/exercises/11-node-rules.md +++ b/doc/tutorials/workshop/content/exercises/11-node-rules.md @@ -27,7 +27,7 @@ owned by root in our cluster's nodes. To create this node rule, execute the following `create node` command: ``` -$ ./utils/add_platform_rule.py create node \ +$ ./utils/add_kubernetes_rule.py create node \ --rule file_owner_etc_system_release \ --title "File /etc/system-release must be owned by root" \ --description "We need to ensure that root owns the system release file" \ @@ -47,7 +47,7 @@ input arguments. Here is another example of how to quickly generate a a node rule that checks the sysctl `kernel.randomize_va_space` value: ``` -$ ./utils/add_platform_rule.py create node \ +$ ./utils/add_kubernetes_rule.py create node \ --rule sysctl_kernel_randomize_va_space \ --title "Ensure ASLR is fully enabled" \ --description "Make it harder to exploit vulnerabilities by employing full address space layout randomization" \ @@ -57,7 +57,7 @@ $ ./utils/add_platform_rule.py create node \ ### Selecting the nodes to check -The node rules created with `./utils/add_platform_rule.py create node ...` +The node rules created with `./utils/add_kubernetes_rule.py create node ...` are by default applicable to all nodes in the cluster, i.e.: `worker` and `master` nodes. @@ -85,3 +85,5 @@ One such example is the Kubelet configuration in each node. The Check the rule [kubelet_enable_cert_rotation](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/kubelet/kubelet_enable_cert_rotation/rule.yml) for an example of how the `yamlfile_value` template is used. + +Let's now take a look at the [most common types of rules](12-common-rules.md) and the templates used when writing rules for Kubernetes. diff --git a/doc/tutorials/workshop/content/exercises/12-common-rules.md b/doc/tutorials/workshop/content/exercises/12-common-rules.md index 12c2f74d5..1c71ba6a9 100644 --- a/doc/tutorials/workshop/content/exercises/12-common-rules.md +++ b/doc/tutorials/workshop/content/exercises/12-common-rules.md @@ -1,7 +1,7 @@ --- Title: All types of rules PrevPage: 11-node-rules -NextPage: ../finish +NextPage: 13-complex-yaml.md --- Common types of rules =================== @@ -31,7 +31,7 @@ This type of check is handled by the [yamlfile_value](https://complianceascode.r template. You can write a rule and make use of the template by yourself, or -use the `./utils/add_platform_rule.py` script showcased in past sections. +use the `./utils/add_kubernetes_rule.py` script showcased in past sections. Note that more advanced uses of the template will require you to write the input data manualy, check the template's documentation. @@ -110,3 +110,6 @@ template: datatype: int ``` Check rule [kubelet_enable_protect_kernel_sysctl_kernel_panic_on_oops](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/kubelet/kubelet_enable_protect_kernel_sysctl_kernel_panic_on_oops/rule.yml) for a complete example. + +In the next section we will look at a way to handle [more complex checks](13-complex-yaml.md) +and resources. diff --git a/doc/tutorials/workshop/content/exercises/13-complex-yaml.md b/doc/tutorials/workshop/content/exercises/13-complex-yaml.md new file mode 100644 index 000000000..0351e58fb --- /dev/null +++ b/doc/tutorials/workshop/content/exercises/13-complex-yaml.md @@ -0,0 +1,141 @@ +--- +Title: Checking complex yaml structures +PrevPage: 12-common-rules.md +NextPage: ../finish +--- + +Checking complex yaml structures +==================== + +The `yamlfile_value` template is great to check if a yaml key exists or not, +and it does exist, if it has a specific value. + +But some of the resources and configurations of a Kubernetes cluster can be +defined in quite a complex way, and just the yaml path syntax used by the +template may not be sufficient to get to the value we want to assess. + +For this reason the CaC/content rules used by the Compliance Operator can +leverage [`jq`](https://jqlang.github.io/jq/) to better select what needs to be assessed. +A `jq` filter allows us to pre-process the resources and configurations so that +they are easier to check with the `yamlfile_value` template. + +There are two use cases where a `jq` filter is necessary. + +## 1. Checking a key in a nested yaml or json + +Some Kuberentes configurations contain a yaml or json formatted value in them, +and checking for these values requires the use of `jq` filter. + +For example, looking at an `openshift-kube-api-server` `ConfigMap` we can see +a `data."config.yaml"` key whose value is yaml formatted. + +``` +$ oc get configmap config -n openshift-kube-apiserver -oyaml +apiVersion: v1 +data: + config.yaml: '{"admission":{"pluginConfig":{"PodSecurity":{"configuration":{"apiVersion":"pod-security.admission.conf +ig.k8s.io/v1","defaults":{"audit":"restricted","audit-version":"latest","enforce":"privileged","enforce-version":"lates +t","warn":"restricted","warn-version":"latest"},"exemptions":{"usernames":["system:serviceaccount:openshift-infra:build +-controller"]},"kind":"PodSecurityConfiguration"}},"network.openshift.io/ExternalIPRanger":{"configuration":{"allowIngr +essIP":false,"apiVersion":"network.openshift.io/v1","externalIPNetworkCIDRs":null,"kind":"ExternalIPRangerAdmissionConf +ig"},"location":""},"network.openshift.io/RestrictedEndpointsAdmission":{"configuration":{"apiVersion":"network.openshi +ft.io/v1","kind":"RestrictedEndpointsAdmissionConfig","restrictedCIDRs":["10.128.0.0/14","172.30.0.0/16"]}}}},"apiServe +rArguments":{"allow-privileged":["true"],"anonymous-auth":["true"],"api-audiences":["https://kubernetes.default.svc"]," +audit-log-format":["json"],"audit-log-maxbackup":["10"],"audit-log-maxsize":["200"],"audit-log-path":["/var/log/kube-ap +iserver/audit.log"],"audit-policy-file":["/etc/kubernetes/static-pod-resources/configmaps/kube-apiserver-audit-policies +... +kind: ConfigMap +metadata: + creationTimestamp: "2023-10-06T12:59:31Z" + name: config + namespace: openshift-kube-apiserver + resourceVersion: "21469" + uid: fc192e71-282a-4af6-9492-87324ea83410 +``` + +To check the value of `audit-log-maxbackup`, which is in the `config.yaml` key, +we need to use the `jq` query to select the value and "extract" it for us. + +Rule [api_server_audit_log_maxbackup](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/api-server/api_server_audit_log_maxbackup/rule.yml) +is evaluating the resource listed above. +The rule's `jq` filter is `.data."config.yaml" | fromjson'` and its `yamlpath` is `.apiServerArguments["audit-log-maxbackup"][:]`. +The Compliance Operator will fetch the resource and pass down for checking only the data that came out from the `jq` filter. + +![Diagram of resource colection and check with and without a jq filter](images/jqfilter_preprocessing.png) + +For this section, we will create a very simple `ConfigMap` with embedded yaml and check one of its keys. + +So let's create a machine config that has a nested yaml value: +``` +$ cat << EOF | oc create -f - +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-nested-compliance-configmap + namespace: openshift +data: + my-config.yaml: '{foo: bar, nested-key: nested-compliant}' +EOF +``` + +Now let's write a rule to check whether the value of `nested-key` is `nested-compliant`. + +The script `utils/add_kubernetes_rule.py` provides an easy way to create a rule that uses `jq` filters. +Just pass the option `--jqfilter` with the desired filter. +``` +$ ./utils/add_kubernetes_rule.py create platform \ + --rule check_nested_yaml \ + --name my-nested-compliance-configmap --namespace openshift --type configmap \ + --title "Check value of nested-key in my-config-yaml" \ + --description "It is important that the nested configmap has a nested-key with value nested-compliant." \ + --match-entity "at least one" \ + --match "nested-compliant" \ + --yamlpath ".nested-key" \ + --jqfilter '.data."my-config.yaml"' +``` + +Just like when we were creating the other rules, the tool lays down a `rule.yml` file with the same keys filled out. +There are two diffences this time around: +* First, is the warning messsage. Which is now defined by a different macro. + This macro adds the `jq` filter to the rule, which is parsed by the operator when collecting the resource. +* Second difference is the `filepath` key in the template, which is define with the help of macro too. + This macro ensures that a unique `filepath` is set for this resource when it is collected. + +You can test the rule with: +``` +$ ./utils/add_kubernetes_rule.py cluster-test --rule check_nested_yaml +* Testing rule check_nested_yaml in-cluster +* Ensuring openshift-compliance namespace exists. +... +* Running scan with rule 'check_nested_yaml' +> Output from last phase check: LAUNCHING NOT-AVAILABLE +... +> Output from last phase check: RUNNING NOT-AVAILABLE +... +> Output from last phase check: AGGREGATING NOT-AVAILABLE +> Output from last phase check: DONE COMPLIANT +* The result is 'COMPLIANT' +``` + +If you'd like to test that the rule fails with incompliant values, patch the `ConfigMap` with an incompliant value, and run the test again. +``` +$ oc patch -n openshift configmap my-nested-compliance-configmap -p '{"data": {"my-config.yaml": "{foo: bar, nested-key: nested-not-compliant}"}} +configmap/my-nested-compliance-configmap patched +$ ./utils/add_kubernetes_rule.py cluster-test --rule check_nested_yaml +... +* The result is 'NON-COMPLIANT' +``` + +## 2. Filtering the data to have simpler yamlpaths + +This is a generalization of the first use case, `jq` filters can be used to filter and select the data +fetched by the operator. + +When the resource being checked is extensive or complex, `jq` is invaluable for simplifying the data before it is +passed down to be evaluated with `yamlpath` in `yamlfile_value` template. + +Rule [api_server_encryption_provider_cipher](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/api-server/api_server_encryption_provider_cipher/rule.yml) +is one example of a rule that filters the data for a simpler `yamlpath`. + +And rule [configure_network_policies_hypershift_hosted](https://github.com/ComplianceAsCode/content/blob/master/applications/openshift/networking/configure_network_policies_hypershift_hosted/rule.yml) +is an example of a rule that uses `jq` filters to select attributes from an array to be evaluated. diff --git a/doc/tutorials/workshop/content/exercises/images/jqfilter_preprocessing.png b/doc/tutorials/workshop/content/exercises/images/jqfilter_preprocessing.png new file mode 100644 index 000000000..27273aa4b Binary files /dev/null and b/doc/tutorials/workshop/content/exercises/images/jqfilter_preprocessing.png differ