Over the past few years, I have taken a keen interest in DevSecOps and software supply chain security in the Kubernetes space. One area that I have looked at from multiple tools is how to automate security practices and policies based on the resources being deployed. These investigations have led me to write multiple blogs on OPA [1] and Kyverno [2], [3], [4].
For this blog post, I will take a look at kube-linter and how a developer can write a custom template that will be used to
check if a Route has TLS configured.
For those new to kube-linter, let's firstly start off with a quick introduction and show an example.
So what is kube-linter? straight from the docs:
KubeLinter analyzes Kubernetes YAML files and Helm charts, and checks them against a variety of best practices, with a focus on production readiness and security.
KubeLinter runs sensible default checks, designed to give you useful information about your Kubernetes YAML files and Helm charts. This is to help teams check early and often for security misconfigurations and DevOps best practices. Some common examples of these include running containers as a non-root user, enforcing least privilege, and storing sensitive information only in secrets.
KubeLinter is configurable, so you can enable and disable checks, as well as create your own custom checks, depending on the policies you want to follow within your organization.
When a lint check fails, KubeLinter reports recommendations for how to resolve any potential issues and returns a non-zero exit code.
In simple terms; it's a CLI tool that can be run against your Helm chart to validate if you are following well-defined best practices.
Firstly, let's install it. If a go install method is offered, I typically use it. But, if you haven't got go installed and configured correctly
there are a number of different options, such as a brew or docker.
The below presumes you've already installed go and configured $GOBIN as part of your $PATH correctly.
GO111MODULE=on go install golang.stackrox.io/kube-linter/cmd/kube-linter@0.2.6
kube-linter versionNow, let's run an example that only runs the minimum-three-replicas check:
git clone https://github.com/garethahealy/kubelinter-extending-blog.git
cd kubelinter-extending-blog
kube-linter --config policy/kube-linter-config.yaml lint policy/pod-replicas-below-one/test_data/unit/list.ymlWhich should show the below:
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) object has 1 replica but minimum required replicas is 3 (check: minimum-three-replicas, remediation: Increase be number of replicas in the deployment to at least three to increase the fault tolerancy of the deployment.)
Error: found 1 lint errorsHopefully, from what kube-linter outputted you've worked out that the Deployment in our example only has .spec.replicas: 1
which if you want to run a highly available workload then this isn't the best idea.
Since kube-linter comes with a variety of policies out of the box, we can run all the checks against our example:
kube-linter lint policy/pod-replicas-below-one/test_data/unit/list.ymlWhich should show the below:
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) The container "bar" is using an invalid container image, "". Please use images that are not blocked by the `BlockList` criteria : [".*:(latest)$" "^[^:]*$" "(.*/[^:]+)$"] (check: latest-tag, remediation: Use a container image with a specific tag other than latest.)
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) object has no selector specified (check: mismatching-selector, remediation: Confirm that your deployment selector correctly matches the labels in its pod template.)
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) container "bar" does not have a read-only root file system (check: no-read-only-root-fs, remediation: Set readOnlyRootFilesystem to true in the container securityContext.)
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) container "bar" is not set to runAsNonRoot (check: run-as-non-root, remediation: Set runAsUser to a non-zero number and runAsNonRoot to true in your pod or container securityContext. Refer to https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ for details.)
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) container "bar" has cpu request 0 (check: unset-cpu-requirements, remediation: Set CPU requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) container "bar" has cpu limit 0 (check: unset-cpu-requirements, remediation: Set CPU requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) container "bar" has memory request 0 (check: unset-memory-requirements, remediation: Set memory requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)
policy/pod-replicas-below-one/test_data/unit/list.yml: (object: <no namespace>/replicaisone apps/v1, Kind=Deployment) container "bar" has memory limit 0 (check: unset-memory-requirements, remediation: Set memory requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)Again, from the output we can see that we don't have any requests or limits set, which again, could cause problems for the stability of the platform
if the application starts to become overloaded or thrash the system and starve the kubelet process.
Currently, kube-linter supports two concepts. A check and a template.
- a
checkis an input to atemplate. - a
templateis the parser that works on the resource to validate thecheck.
OK, but what does that look like in the real world? A check
could be that you only want to allow a Role to be able to create certain resources, such as a Deployment.
The template then looks at the specific Role
and checks that the input (i.e.: the check) is not being violated.
Currently, all templates need to be written in go so developers who want to write policies for kube-linter need to follow
a fork, commit, build and carry model. In the future,
it is planned to allow users to write policies using other methods.
If you want to see what comes out of the box, you can simply run:
kube-linter checks listAnd:
kube-linter templates listIn the intro, I said we'd extend kube-linter to support the OCP Route resource so that we could write a policy to check that TLS
was configured. For those that just want to jump into the repository and have a dig around themselves, all the code is pushed to:
For those that are new to go or unsure of the kube-linter codebase, I'll step through the changes to enable you to
write your own policies.
So let's get into it!
-
As we are adding a new schema resource to
kube-linter, we need to tell it how to parse theYAMLof theRoute. So let's add thegopackage to the schema builder via commit/516bcf3c3d407177bd0f995d4b72fdf04b8bc65d. -
kube-lintermakes use of object kind matcher functions that decides if the resource input matches what the template can work against. For our template, we'll create anIngressLikematcher via commit/4335e829d570a81ef8a9069cd9a4a91077b9b772. Currently, it only supports aRoutebut in the future, it could be extended to supportnetworking.k8s.io/v1/Ingressor if you are on the cutting edge, agateway.networking.k8s.io/v1alpha2/Gateway. -
Now the meat of the policy; the template that implements the check. We need to create two objects, the
paramsand thetemplatevia commit/112b7f2beb09714a1ce971ee894ec1f6b1e03307. Currently, we don't have any parameters to pass into our template as our policy is binary; we have TLS defined or not. The template follows a standard pattern of implementingcheck.Template. The main body of code goes in theInstantiateparameter. As you can see from theWrapInstantiateFuncbody, we: -
OK, so that looks quite simple so far. But how do we know it works correctly? we write tests! The unit tests via commit/f662b2968d51a42163301083d0d8ba274879c41b check for matching and non-matching inputs, as we don't want the policy firing incorrectly.
-
We are now at the point of knowing our new template works correctly but if we were to compile the binary and attempt to run the template via
kube-linter lint --include route-termination --do-not-auto-add-defaults route-termination.yml,kube-linterwould complain it does not include acheckcalledroute-termination. There is some minor plumbing work required via commit/b811a542fba36a7d99ab24abfba6b3c8cc4cbb01 to let the CLI know if a user wants to runroute-terminationthen it is for the template we have just written. -
Now the bulk of the code is done, we just need to write a simple
BATSintegration test to validate the end-to-end process and that if we provide a YAML file containing aRoutewithout TLS defined, we get the expected error message back via commit/37d267cfaa7ea2b2c95e7ec85a88a86d26eb20cd. -
And finally, let's build our new binary and execute the tests!
make go-generated-srcsmake testmake e2e-testmake e2e-bats
At this point, you've built a new template that has both unit and integration tests and is ready to be used within your DevSecOps
pipelines.
Have a policy that is not currently implemented by kube-linter that you think could be of benefit to the wider OCP/k8s community?
get contributing!