diff --git a/k8s/kyverno-namespace-setup.yaml b/k8s/kyverno-namespace-setup.yaml new file mode 100644 index 00000000..45e41383 --- /dev/null +++ b/k8s/kyverno-namespace-setup.yaml @@ -0,0 +1,63 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: setup-namespace +spec: + rules: + - name: setup-limitrange + match: + resources: + kinds: + - Namespace + generate: + kind: LimitRange + name: default-limitrange + namespace: "{{request.object.metadata.name}}" + data: + spec: + limits: + - type: Container + min: + cpu: 0.1 + memory: 0.1 + max: + cpu: 2 + memory: 2Gi + default: + cpu: 0.25 + memory: 500Mi + defaultRequest: + cpu: 0.25 + memory: 250Mi + - name: setup-resourcequota + match: + resources: + kinds: + - Namespace + generate: + kind: ResourceQuota + name: default-resourcequota + namespace: "{{request.object.metadata.name}}" + data: + spec: + hard: + requests.cpu: "10" + requests.memory: 10Gi + limits.cpu: "20" + limits.memory: 20Gi + - name: setup-networkpolicy + match: + resources: + kinds: + - Namespace + generate: + kind: NetworkPolicy + name: default-networkpolicy + namespace: "{{request.object.metadata.name}}" + data: + spec: + podSelector: {} + ingress: + - from: + - podSelector: {} + diff --git a/k8s/kyverno-pod-color-1.yaml b/k8s/kyverno-pod-color-1.yaml new file mode 100644 index 00000000..6752f042 --- /dev/null +++ b/k8s/kyverno-pod-color-1.yaml @@ -0,0 +1,22 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: pod-color-policy-1 +spec: + validationFailureAction: enforce + rules: + - name: ensure-pod-color-is-valid + match: + resources: + kinds: + - Pod + selector: + matchExpressions: + - key: color + operator: Exists + - key: color + operator: NotIn + values: [ red, green, blue ] + validate: + message: "If it exists, the label color must be red, green, or blue." + deny: {} diff --git a/k8s/kyverno-pod-color-2.yaml b/k8s/kyverno-pod-color-2.yaml new file mode 100644 index 00000000..6262c276 --- /dev/null +++ b/k8s/kyverno-pod-color-2.yaml @@ -0,0 +1,21 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: pod-color-policy-2 +spec: + validationFailureAction: enforce + background: false + rules: + - name: prevent-color-change + match: + resources: + kinds: + - Pod + validate: + message: "Once label color has been added, it cannot be changed." + deny: + conditions: + - key: "{{ request.oldObject.metadata.labels.color }}" + operator: NotEqual + value: "{{ request.object.metadata.labels.color }}" + diff --git a/k8s/kyverno-pod-color-3.yaml b/k8s/kyverno-pod-color-3.yaml new file mode 100644 index 00000000..1a15e8be --- /dev/null +++ b/k8s/kyverno-pod-color-3.yaml @@ -0,0 +1,25 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: pod-color-policy-3 +spec: + validationFailureAction: enforce + background: false + rules: + - name: prevent-color-removal + match: + resources: + kinds: + - Pod + selector: + matchExpressions: + - key: color + operator: DoesNotExist + validate: + message: "Once label color has been added, it cannot be removed." + deny: + conditions: + - key: "{{ request.oldObject.metadata.labels.color }}" + operator: NotIn + value: [] + diff --git a/slides/k8s/kyverno.md b/slides/k8s/kyverno.md new file mode 100644 index 00000000..d50e2082 --- /dev/null +++ b/slides/k8s/kyverno.md @@ -0,0 +1,599 @@ +# Policy Management with Kyverno + +- The Kubernetes permission management system is very flexible ... + +- ... But it can't express *everything!* + +- Examples: + + - forbid using `:latest` image tag + + - enforce that each Deployment, Service, etc. has an `owner` label +
(except in e.g. `kube-system`) + + - enforce that each container has at least a `readinessProbe` healthcheck + +- How can we address that, and express these more complex *policies?* + +--- + +## Admission control + +- The Kubernetes API server provides a generic mechanism called *admission control* + +- Admission controllers will examine each write request, and can: + + - approve/deny it (for *validating* admission controllers) + + - additionally *update* the object (for *mutating* admission controllers) + +- These admission controllers can be: + + - plug-ins built into the Kubernetes API server +
(selectively enabled/disabled by e.g. command-line flags) + + - webhooks registered dynamically with the Kubernetes API server + +--- + +## What's Kyverno? + +- Policy management solution for Kubernetes + +- Open source (https://github.com/kyverno/kyverno/) + +- Compatible with all clusters + + (doesn't require to reconfigure the control plane, enable feature gates...) + +- We don't endorse / support it in a particular way, but we think it's cool + +- It's not the only solution! + + (see e.g. [Open Policy Agent](https://www.openpolicyagent.org/docs/v0.12.2/kubernetes-admission-control/)) + +--- + +## What can Kyverno do? + +- *Validate* resource manifests + + (accept/deny depending on whether they conform to our policies) + +- *Mutate* resources when they get created or updated + + (to add/remove/change fields on the fly) + +- *Generate* additional resources when a resource gets created + + (e.g. when namespace is created, automatically add quotas and limits) + +- *Audit* existing resources + + (warn about resources that violate certain policies) + +--- + +## How does it do it? + +- Kyverno is implemented as a *controller* or *operator* + +- It typically runs as a Deployment on our cluster + +- Policies are defined as *custom resource definitions* + +- They are implemented with a set of *dynamic admission control webhooks* + +-- + +🤔 + +-- + +- Let's unpack that! + +--- + +## Custom resource definitions + +- When we install Kyverno, it will register new resource types: + + - Policy and ClusterPolicy (per-namespace and cluster-scope policies) + + - PolicyViolation and ClusterPolicyViolation (used in audit mode) + + - GenerateRequest (used internally when generating resources asynchronously) + +- We will be able to do e.g. `kubectl get policyviolations --all-namespaces` + + (to see policy violations across all namespaces) + +- Policies will be defined in YAML and registered/updated with e.g. `kubectl apply` + +--- + +## Dynamic admission control webhooks + +- When we install Kyverno, it will register a few webhooks for its use + + (by creating ValidatingWebhookConfiguration and MutatingWebhookConfiguration resources) + +- All subsequent resource modifications are submitted to these webhooks + + (creations, updates, deletions) + +--- + +## Controller + +- When we install Kyverno, it creates a Deployment (and therefore, a Pod) + +- That Pod runs the server used by the webhooks + +- It also runs a controller that will: + + - run optional checks in the background (and generate PolicyViolation objects) + + - process GenerateRequest objects asynchronously + +--- + +## Kyverno in action + +- We're going to install Kyverno on our cluster + +- Then, we will use it to implement a few policies + +--- + +## Installing Kyverno + +- Kyverno can be installed with a (big) YAML manifest + +- ... or with Helm charts (which allows to customize a few things) + +.exercise[ + +- Install Kyverno: + ```bash + kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno\ + /master/definitions/release/install.yaml + ``` + +] + +--- + +## Kyverno policies in a nutshell + +- Which resources does it *select?* + + - can specify resources to *match* and/or *exclude* + + - can specify *kinds* and/or *selector* and/or users/roles doing the action + +- Which operation should be done? + + - validate, mutate, or generate + +- For validation, whether it should *enforce* or *audit* failures + +- Operation details (what exactly to validate, mutate, or generate) + +--- + +## Immutable primary colors, take 1 + +- Our pods can have an optional `color` label + +- If the label exists, it *must* be `red`, `green`, or `blue` + +- One possible approach: + + - *match* all pods that have a `color` label that is not `red`, `green`, or `blue` + + - *deny* these pods + +- We could also *match* all pods, then *deny* with a condition + +--- + +## Testing without the policy + +- First, let's create a pod with an "invalid" label + + (while we still can!) + +- We will use this later + +.exercise[ + +- Create a pod: + ```bash + kubectl run test-color-0 --image=nginx + ``` + +- Apply a color label: + ```bash + kubectl label test-color-0 color=purple + ``` + +] + +--- + +## Our first Kyverno policy + +.small[ +```yaml +@@INCLUDE[k8s/kyverno-pod-color-1.yaml] +``` +] + +--- + +## Load and try the policy + +.exercise[ + +- Load the policy: + ```bash + kubectl apply -f ~/container.training/k8s/kyverno-pod-color-1.yaml + ``` + +- Create a pod: + ```bash + kubectl run test-color-1 --image=nginx + ``` + +- Try to apply a few color labels: + ```bash + kubectl label test-color-1 color=purple + kubectl label test-color-1 color=red + kubectl label test-color-1 color- + ``` + +] + +--- + +## Immutable primary colors, take 2 + +- New rule: once a `color` label has been added, it cannot be changed + + (i.e. if `color=red`, we can't change it to `color=blue`) + +- Our approach: + + - *match* all pods + + - *deny* these pods if their `color` label has changed + +- "Old" and "new" versions of the pod can be referenced through + + `{{ request.oldObject }}` and `{{ request.object }}` + +- Our label is available through `{{ request.object.metadata.labels.color }}` + +- Again, other approaches are possible! + +--- + +## Our second Kyverno policy + +.small[ +```yaml +@@INCLUDE[k8s/kyverno-pod-color-2.yaml] +``` +] + +--- + +## Load and try the policy + +.exercise[ + +- Load the policy: + ```bash + kubectl apply -f ~/container.training/k8s/kyverno-pod-color-2.yaml + ``` + +- Create a pod: + ```bash + kubectl run test-color-2 --image=nginx + ``` + +- Try to apply a few color labels: + ```bash + kubectl label test-color-2 color=purple + kubectl label test-color-2 color=red + kubectl label test-color-2 color=blue --overwrite + ``` + +] + +--- + +## `background` + +- What is this `background: false` option, and why do we need it? + +-- + +- Admission controllers are only invoked when we change an object + +- Existing objects are not affected + + (e.g. if we have a pod with `color=pink` *before* installing our policy) + +- Kyvero can also run checks in the background, and report violations + + (we'll see later how they are reported) + +- `background: false` disables that + +-- + +- Alright, but ... *why* do we need it? + +--- + +## Accessing `AdmissionRequest` context + +- In this specific policy, we want to prevent an *update* + + (as opposed to a mere *create* operation) + +- We want to compare the *old* and *new* version + + (to check if a specific label was removed) + +- The `AdmissionRequest` object has `object` and `oldObject` fields + + (the `AdmissionRequest` object is the thing that gets submitted to the webhook) + +- Kyverno lets us access the `AdmissionRequest` object + + (and in particular, `{{ request.object }}` and `{{ request.oldObject }}`) + +-- + +- Alright, but ... what's the link with `background: false`? + +--- + +## `{{ request }}` + +- The `{{ request }}` context is only available when there is an `AdmissionRequest` + +- When a resource is "at rest", there is no `{{ request }}` (and no old/new) + +- Therefore, a policy that uses `{{ request }}` cannot validate existing objects + + (it can only be used when an object is actually created/updated/deleted) + +--- + +## Immutable primary colors, take 3 + +- New rule: once a `color` label has been added, it cannot be removed + +- Our approach: + + - *match* all pods that *do not* have a `color` label + + - *deny* these pods if they had a `color` label before + + - "before" can be referenced through `{{ request.oldObject }}` + +- Again, other approaches are possible! + +--- + +## Our third Kyverno policy + +.small[ +```yaml +@@INCLUDE[k8s/kyverno-pod-color-3.yaml] +``` +] + +--- + +## Load and try the policy + +.exercise[ + +- Load the policy: + ```bash + kubectl apply -f ~/container.training/k8s/kyverno-pod-color-3.yaml + ``` + +- Create a pod: + ```bash + kubectl run test-color-3 --image=nginx + ``` + +- Try to apply a few color labels: + ```bash + kubectl label test-color-3 color=purple + kubectl label test-color-3 color=red + kubectl label test-color-3 color- + ``` + +] + +--- + +## Background checks + +- What about the `test-color-0` pod that we create initially? + + (remember: we did set `color=purple`) + +- Kyverno generated a ClusterPolicyViolation to indicate it + +.exercise[ + +- Check that the pod still an "invalid" color: + ```bash + kubectl get pods -L color + ``` + +- List ClusterPolicyViolations: + ```bash + kubectl get clusterpolicyviolations + kubectl get cpolv + ``` + +] + +--- + +## Generating objects + +- When we create a Namespace, we also want to automatically create: + + - a LimitRange (to set default CPU and RAM requests and limits) + + - a ResourceQuota (to limit the resources used by the namespace) + + - a NetworkPolicy (to isolate the namespace) + +- We can do that with a Kyverno policy with a *generate* action + + (it is mutually exclusive with the *validate* action) + +--- + +## Overview + +- The *generate* action must specify: + + - the `kind` of resource to generate + + - the `name` of the resource to generate + + - its `namespace`, when applicable + + - *either* a `data` structure, to be used to populate the resource + + - *or* a `clone` reference, to copy an existing resource + +Note: the `apiVersion` field appears to be optional. + +--- + +## In practice + +- We will use the policy @@LINK[k8s/kyverno-namespace-setup.yaml] + +- We need to generate 3 resources, so we have 3 rules in the policy + +- Excerpt: + ```yaml + generate: + kind: LimitRange + name: default-limitrange + namespace: "{{request.object.metadata.name}}" + data: + spec: + limits: + ``` + +- Note that we have to specify the `namespace` + + (and we infer it from the name of the resource being created, i.e. the Namespace) + +--- + +## Lifecycle + +- After generated objects have been created, we can change them + + (Kyverno won't update them) + +- Except if we use `clone` together with the `synchronize` flag + + (in that case, Kyverno will watch the cloned resource) + +- This is convenient for e.g. ConfigMaps shared between Namespaces + +- Objects are generated only at *creation* (not when updating an old object) + +--- + +## Asynchronous creation + +- Kyverno creates resources asynchronously + + (by creating a GenerateRequest resource first) + +- This is useful when the resource cannot be created + + (because of permissions or dependency issues) + +- Kyverno will periodically loop through the pending GenerateRequests + +- Once the ressource is created, the GenerateRequest is marked as Completed + +--- + +## Footprint + +- 5 CRDs: 4 user-facing, 1 internal (GenerateRequest) + +- 5 webhooks + +- 1 Service, 1 Deployment, 1 ConfigMap + +- Internal resources (GenerateRequest) "parked" in a Namespace + +- Kyverno packs a lot of features in a small footprint + +--- + +## Strengths + +- Kyverno is very easy to install + + (it's harder to get easier than one `kubectl apply -f`) + +- The setup of the webhooks is fully automated + + (including certificate generation) + +- It offers both namespaced and cluster-scope policies + + (same thing for the policy violations) + +- The policy language leverages existing constructs + + (e.g. `matchExpressions`) + +--- + +## Caveats + +- By default, the webhook failure policy is `Ignore` + + (meaning that there is a potential to evade policies if we can DOS the webhook) + +- Advanced policies (with conditionals) have unique, exotic syntax: + ```yaml + spec: + =(volumes): + =(hostPath): + path: "!/var/run/docker.sock" + ``` + +- The `{{ request }}` context is powerful, but difficult to validate + + (Kyverno can't know ahead of time how it will be populated) + +- Policy validation is difficult + +??? + +:EN:- Policy Management with Kyverno +:FR:- Gestion de *policies* avec Kyverno diff --git a/slides/kube-selfpaced.yml b/slides/kube-selfpaced.yml index 0797b991..45a42b20 100644 --- a/slides/kube-selfpaced.yml +++ b/slides/kube-selfpaced.yml @@ -114,6 +114,7 @@ content: - k8s/admission.md - k8s/operators.md - k8s/operators-design.md + - k8s/kyverno.md - k8s/owners-and-dependents.md - - k8s/dmuc.md diff --git a/slides/markmaker.py b/slides/markmaker.py index d18890d0..5173cd93 100755 --- a/slides/markmaker.py +++ b/slides/markmaker.py @@ -139,6 +139,20 @@ def generatefromyaml(manifest, filename): html = html.replace("@@HTML@@", manifest["html"]) html = html.replace("@@TITLE@@", manifest["title"].replace("\n", " ")) html = html.replace("@@SLIDENUMBERPREFIX@@", manifest.get("slidenumberprefix", "")) + + # Process @@LINK[file] and @@INCLUDE[file] directives + local_anchor_path = ".." + # FIXME use dynamic repo and branch? + online_anchor_path = "https://github.com/jpetazzo/container.training/tree/master" + for atatlink in re.findall(r"@@LINK\[[^]]*\]", html): + logging.debug("Processing {}".format(atatlink)) + file_name = atatlink[len("@@LINK["):-1] + html = html.replace(atatlink, "[{}]({}/{})".format(file_name, online_anchor_path, file_name )) + for atatinclude in re.findall(r"@@INCLUDE\[[^]]*\]", html): + logging.debug("Processing {}".format(atatinclude)) + file_name = atatinclude[len("@@INCLUDE["):-1] + file_path = os.path.join(local_anchor_path, file_name) + html = html.replace(atatinclude, open(file_path).read()) return html