diff --git a/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/post-create.sh b/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/post-create.sh index 4b81324a..a2b77609 100755 --- a/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/post-create.sh +++ b/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/post-create.sh @@ -15,4 +15,4 @@ track_codespace_created --kubens-version v0.11.0 \ --k9s-version v0.50.18 \ --helm-version v4.1.4 -"$REPO_ROOT/lib/kyverno/init.sh" --version 3.7.1 --cli-version v1.17.1 +"$REPO_ROOT/lib/kyverno/init.sh" --version 3.8.1 --cli-version v1.18.1 diff --git a/adventures/planned/00-lex-imperfecta/beginner/Makefile b/adventures/planned/00-lex-imperfecta/beginner/Makefile index b40317a1..5994ab28 100644 --- a/adventures/planned/00-lex-imperfecta/beginner/Makefile +++ b/adventures/planned/00-lex-imperfecta/beginner/Makefile @@ -6,7 +6,7 @@ help: @echo " make verify Run the verification script" apply: - @kubectl delete pod compliant missing-labels privileged privileged-init-container --ignore-not-found > /dev/null 2>&1; true + @kubectl delete pod compliant missing-labels privileged privileged-init-container peregrinus --ignore-not-found > /dev/null 2>&1; true @echo "Applied Policies:"; \ for f in manifests/policies/*.yaml; do \ name=$$(grep '^ name:' "$$f" | awk '{print $$2}'); \ diff --git a/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/peregrinus.yaml b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/peregrinus.yaml new file mode 100644 index 00000000..c8966ebf --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/peregrinus.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: peregrinus + labels: + republic.rome/gens: forum-romanum + republic.rome/traveler: peregrinus +spec: + restartPolicy: Never + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false diff --git a/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/no-privileged-containers.yaml b/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/no-privileged-containers.yaml index a0264b71..c9651ffb 100644 --- a/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/no-privileged-containers.yaml +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/no-privileged-containers.yaml @@ -1,5 +1,5 @@ -apiVersion: kyverno.io/v1 -kind: ClusterPolicy +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy metadata: name: no-privileged-containers annotations: @@ -12,23 +12,19 @@ metadata: grants access to all Linux kernel capabilities and host resources, bypassing namespace isolation. spec: - validationFailureAction: Enforce - background: true - rules: - - name: privileged-containers - match: - any: - - resources: - kinds: - - Pod - validate: - message: "The Senate forbids unchecked power: privileged containers seize full control of the host and are not permitted in the Republic." - # Privileged containers are forbidden — this applies to regular, ephemeral and init containers. - pattern: - spec: - "=(ephemeralContainers)": - - "=(securityContext)": - "=(privileged)": "false" - "=(initContainers)": - - "=(securityContext)": - "=(privileged)": "false" \ No newline at end of file + validationActions: + - Deny + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["pods"] + validations: + # Privileged containers are forbidden — this applies to regular, ephemeral and init containers. + - expression: |- + ( + object.spec.?initContainers.orValue([]) + + object.spec.?ephemeralContainers.orValue([]) + ).all(c, !c.?securityContext.?privileged.orValue(false)) + message: "The Senate forbids unchecked power: privileged containers seize full control of the host and are not permitted in the Republic." diff --git a/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/require-labels.yaml b/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/require-labels.yaml index a69443c9..d40e2f0e 100644 --- a/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/require-labels.yaml +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/require-labels.yaml @@ -1,5 +1,5 @@ -apiVersion: kyverno.io/v1 -kind: ClusterPolicy +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy metadata: name: require-labels annotations: @@ -11,18 +11,14 @@ metadata: Every citizen must declare their gens. Pods without the 'republic.rome/gens' label are unregistered and cannot be admitted to the Republic's cluster. spec: - validationFailureAction: Audit - background: true - rules: - - name: check-for-labels - match: - any: - - resources: - kinds: - - Pod - validate: - message: All workloads must declare their gens. Unregistered citizens are not permitted in the Republic. - pattern: - metadata: - labels: - republic.rome/gens: "?*" + validationActions: + - Audit + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["pods"] + validations: + - expression: "has(object.metadata.labels) && 'republic.rome/gens' in object.metadata.labels" + message: "All workloads must declare their gens. Unregistered citizens are not permitted in the Republic." diff --git a/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/stamp-travel-permit.yaml b/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/stamp-travel-permit.yaml new file mode 100644 index 00000000..fce0e465 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/stamp-travel-permit.yaml @@ -0,0 +1,33 @@ +apiVersion: policies.kyverno.io/v1 +kind: MutatingPolicy +metadata: + name: stamp-travel-permit + annotations: + policies.kyverno.io/title: Stamp Travel Permit + policies.kyverno.io/category: Admission Control + policies.kyverno.io/severity: low + policies.kyverno.io/subject: Pod, Label + policies.kyverno.io/description: >- + Peregrini — foreign travelers — must carry a travel permit to move freely through the Republic. + Any workload declaring itself as a peregrinus is automatically issued a travel permit upon arrival. +spec: + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + matchConditions: + - name: is-peregrinus + expression: >- + has(object.metadata.labels) && + 'republic.rome/traveler' in object.metadata.labels && + object.metadata.labels['republic.rome/traveler'] == 'peregrinus' + mutations: + - patchType: ApplyConfiguration + applyConfiguration: + expression: | + Object{ + metadata: Object.metadata{ + } + } diff --git a/adventures/planned/00-lex-imperfecta/beginner/verify.sh b/adventures/planned/00-lex-imperfecta/beginner/verify.sh index 1a8a5f61..caf27f62 100755 --- a/adventures/planned/00-lex-imperfecta/beginner/verify.sh +++ b/adventures/planned/00-lex-imperfecta/beginner/verify.sh @@ -10,6 +10,7 @@ OBJECTIVE="By the end of this level, you should have: - All workloads missing the 'republic.rome/gens' label blocked at admission with a clear policy violation message - All workloads running as privileged containers blocked at admission with a clear policy violation message +- All pods declaring 'republic.rome/traveler: peregrinus' automatically receiving the 'republic.rome/travel-permit: granted' label - Confirmed that all other workloads deploy and run successfully in the cluster" DOCS_URL="https://dynatrace-oss.github.io/open-ecosystem-challenges/00-lex-imperfecta/beginner" @@ -139,10 +140,58 @@ EOF # ============================================================================= -# Objective 3: Compliant workloads deploy and run successfully +# Objective 3: Peregrini receive a travel permit via mutation # ============================================================================= print_new_line -print_sub_header "3. Checking that compliant workloads are admitted..." +print_sub_header "3. Checking that peregrini receive a travel permit..." + +check_label_exists \ + "Peregrinus pod" \ + "republic.rome/travel-permit" \ + "granted" \ + "Check the 'stamp-travel-permit' policy — what does the mutation expression add?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-peregrinus + labels: + republic.rome/gens: forum-romanum + republic.rome/traveler: peregrinus +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_label_not_exists \ + "Non-peregrinus pod" \ + "republic.rome/travel-permit" \ + "Check the 'stamp-travel-permit' policy — does the matchCondition target only peregrini?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-citizen + labels: + republic.rome/gens: forum-romanum +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + + + +# ============================================================================= +# Objective 4: Compliant workloads deploy and run successfully +# ============================================================================= +print_new_line +print_sub_header "4. Checking that compliant workloads are admitted..." check_admission_allowed \ "Fully compliant pod (label + non-privileged)" \ diff --git a/adventures/planned/00-lex-imperfecta/docs/beginner.md b/adventures/planned/00-lex-imperfecta/docs/beginner.md index 70351030..04fb7e99 100644 --- a/adventures/planned/00-lex-imperfecta/docs/beginner.md +++ b/adventures/planned/00-lex-imperfecta/docs/beginner.md @@ -4,7 +4,7 @@ The Republic's legal scholars have been busy — perhaps too busy. In their haste to codify the Twelve Tables, the foundation of the Republic's legal system, they introduced errors that now threaten the city's order. Workloads that should be blocked are running freely, and workloads that should be allowed are being turned away at the gates. -Another scholar left a note: "I tried to set up policies for privileged containers and required labels, but something's off — I can't figure out why the wrong things are getting through." +Another scholar left a note: "I tried to set up policies for privileged containers and required labels, but something's off — I can't figure out why the wrong things are getting through. There was also supposed to be a system for automatically issuing travel permits to foreign visitors, but that one is broken too." Your mission: investigate the Kyverno policies and restore proper admission control before chaos reaches the city. @@ -12,7 +12,7 @@ Your mission: investigate the Kyverno policies and restore proper admission cont The defining principle of the Twelve Tables was that Roman law was enforced **at the gates** — before a citizen could act, not after the damage was done. Kubernetes admission control works exactly the same way: Kyverno intercepts every request to create or update a workload and checks it against your policies *before* it reaches the cluster. A misconfigured policy doesn't just fail to enforce — it fails silently, letting non-compliant workloads slip through unnoticed while you assume everything is fine. -That's the situation you've inherited. Your Codespace comes with a Kubernetes cluster and Kyverno pre-installed. Two `ClusterPolicy` resources are already deployed — but both are misconfigured. The policies live in `manifests/policies/`. You will edit them directly and re-apply with `kubectl`. +That's the situation you've inherited. Your Codespace comes with a Kubernetes cluster and Kyverno pre-installed. Three policies are already deployed — two `ValidatingPolicy` resources that validate workloads, and one `MutatingPolicy` that automatically stamps incoming pods with the right labels. All three are misconfigured. The policies live in `manifests/policies/`. You will edit them directly and re-apply with `kubectl`. The pods in `manifests/pods/` are there for reference only — **you don't need to edit them**. @@ -24,14 +24,15 @@ By the end of this level, you should have: - All workloads **missing the `republic.rome/gens` label** blocked at admission with a clear policy violation message - All workloads **running as privileged containers** blocked at admission with a clear policy violation message +- All pods declaring **`republic.rome/traveler: peregrinus`** automatically receiving the **`republic.rome/travel-permit: granted`** label - Confirmed that **all other workloads** deploy and run successfully in the cluster ## 🧠 What You'll Learn -- How Kyverno [`ClusterPolicies`](https://kyverno.io/docs/writing-policies/) and [`validate`](https://kyverno.io/docs/writing-policies/validate/) rules work -- The difference between [`Audit` and `Enforce`](https://kyverno.io/docs/writing-policies/policy-settings/#validation-failure-action) enforcement modes -- How to write and interpret Kyverno [`deny` conditions](https://kyverno.io/docs/writing-policies/validate/#deny-rules) +- How Kyverno [`ValidatingPolicy`](https://kyverno.io/docs/policy-types/validating-policy/) resources and [CEL validation expressions](https://kubernetes.io/docs/reference/using-api/cel/) work +- The difference between [`Audit`, `Deny`, and `Warn`](https://kyverno.io/docs/policy-types/validating-policy/) validation actions - How to use [custom label keys](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to enforce workload identity standards +- How Kyverno [`MutatingPolicy`](https://kyverno.io/docs/policy-types/mutating-policy/) resources automatically patch incoming workloads at admission ## 🧰 Toolbox @@ -85,9 +86,12 @@ kubectl describe pod Check the policies that are in place: ```bash -kubectl get clusterpolicies -kubectl get clusterpolicy require-labels -o yaml -kubectl get clusterpolicy no-privileged-containers -o yaml +kubectl get validatingpolicies +kubectl get validatingpolicy require-labels -o yaml +kubectl get validatingpolicy no-privileged-containers -o yaml + +kubectl get mutatingpolicies +kubectl get mutatingpolicy stamp-travel-permit -o yaml ``` You can also launch **k9s** for a terminal UI view of all cluster resources: @@ -96,13 +100,13 @@ You can also launch **k9s** for a terminal UI view of all cluster resources: k9s ``` -Navigate to `ClusterPolicy` resources with `:clusterpolicies` to inspect both policies. +Navigate to `ValidatingPolicy` resources with `:validatingpolicies` and `MutatingPolicy` resources with `:mutatingpolicies` to inspect all three policies. ### 3. Fix the Policies Review the [🎯 Objective](#objective) and investigate what's wrong in `manifests/policies/`. -Both broken policies are in `manifests/policies/`. Read them carefully — each has a different kind of misconfiguration. +All three broken policies are in `manifests/policies/`. Read them carefully — each has a different kind of misconfiguration. #### Test Locally with the Kyverno CLI @@ -111,6 +115,7 @@ Before applying to the cluster, you can use the `kyverno` CLI to test your polic ```bash kyverno apply manifests/policies/require-labels.yaml --resource manifests/pods/missing-labels.yaml kyverno apply manifests/policies/no-privileged-containers.yaml --resource manifests/pods/privileged.yaml +kyverno apply manifests/policies/stamp-travel-permit.yaml --resource manifests/pods/peregrinus.yaml ``` This gives you fast feedback without touching the cluster. @@ -127,9 +132,10 @@ This re-applies the policies and re-deploys all workloads so you immediately see #### Helpful Documentation -- [Kyverno Policy Validation](https://kyverno.io/docs/writing-policies/validate/) -- [Kyverno Enforcement Modes](https://kyverno.io/docs/writing-policies/policy-settings/#validation-failure-action) -- [Kyverno Deny Rules](https://kyverno.io/docs/writing-policies/validate/#deny-rules) +- [Kyverno ValidatingPolicy](https://kyverno.io/docs/policy-types/validating-policy/) +- [Kyverno MutatingPolicy](https://kyverno.io/docs/policy-types/mutating-policy/) +- [CEL Validation Expressions](https://kubernetes.io/docs/reference/using-api/cel/) +- [Kyverno Playground](https://playground.kyverno.io/#/) — test your CEL expressions interactively against sample resources before applying them to the cluster ### 4. Verify Your Solution diff --git a/ideas/.implemented/lex-imperfecta.md b/ideas/.implemented/lex-imperfecta.md index 06ccdb09..2d2128a4 100644 --- a/ideas/.implemented/lex-imperfecta.md +++ b/ideas/.implemented/lex-imperfecta.md @@ -13,7 +13,7 @@ chaos takes hold. - Manage and organize policies at scale across teams and environments - Respond to runtime threats that bypass static policies -**Technologies:** Kyverno, Falco, Policy Reporter, Argo CD, Kubernetes +**Technologies:** Kyverno, Falco, Policy Reporter, OpenReports, Argo CD, Kubernetes --- @@ -35,7 +35,7 @@ Your mission: investigate the Kyverno policies and restore proper admission cont #### The Problem -Several Kyverno `ClusterPolicy` resources are misconfigured. They are intended to block non-compliant workloads and allow compliant ones through — but they are failing to do so correctly. Some policies are not enforcing when they should, others are rejecting workloads they should allow. +Several Kyverno `ValidatingPolicy` resources are misconfigured. They are intended to block non-compliant workloads and allow compliant ones through — but they are failing to do so correctly. Some policies are not enforcing when they should, others are rejecting workloads they should allow. #### Objective @@ -47,8 +47,8 @@ By the end of this level, the learner should: #### What You'll Learn -- How Kyverno `ClusterPolicies` and `validate` rules work -- The difference between `Audit` and `Enforce` enforcement modes +- How Kyverno `ValidatingPolicy` resources and CEL validation expressions work +- The difference between `Audit`, `Deny`, and `Warn` validation actions - How to read and interpret Kyverno policy violations #### Tools & Infrastructure @@ -62,7 +62,7 @@ By the end of this level, the learner should: #### Description -Fix a misconfigured Kyverno policy setup and use Policy Reporter to restore proper governance across teams and namespaces. +Fix a misconfigured Kyverno policy setup and use Policy Reporter and the OpenReports format to restore proper governance across teams and namespaces. #### Story @@ -74,7 +74,7 @@ Your mission: investigate the policy estate, fix the scoping issues, and ensure #### The Problem -Several Kyverno `ClusterPolicy` and `Policy` resources are misconfigured. They are intended to enforce specific rules across different teams and namespaces — but they are failing to do so correctly. Policies are applying to the wrong provinces, some namespaces are left ungoverned, and exceptions that were meant to be narrow are broader than intended. +Several Kyverno `ValidatingPolicy` and `NamespacedValidatingPolicy` resources are misconfigured. They are intended to enforce specific rules across different teams and namespaces — but they are failing to do so correctly. Policies are applying to the wrong provinces, some namespaces are left ungoverned, and exceptions that were meant to be narrow are broader than intended. #### Objective @@ -87,9 +87,9 @@ By the end of this level, the learner should: #### What You'll Learn -- How to scope Kyverno policies to specific namespaces and teams +- How to scope policies using `ValidatingPolicy` (cluster-wide) and `NamespacedValidatingPolicy` (per-namespace) - How to write and manage policy exceptions correctly -- How to use Policy Reporter to audit and debug the policy estate +- How to use Policy Reporter and the OpenReports format to audit and debug the policy estate #### Tools & Infrastructure diff --git a/lib/scripts/kubernetes.sh b/lib/scripts/kubernetes.sh old mode 100644 new mode 100755 index 79e46a32..5d840bf8 --- a/lib/scripts/kubernetes.sh +++ b/lib/scripts/kubernetes.sh @@ -283,3 +283,72 @@ check_admission_allowed() { FAILED_CHECKS+=("admission_allowed:$display_name") fi } + +# Check that a manifest is admitted and has a specific label in the server response. +# Uses server-side dry-run, which triggers mutation webhooks. +# Reads manifest YAML from stdin. +# Usage: check_label_exists "display name" "label-key" "expected-value" "hint" </dev/null); then + print_error_indent "$display_name was rejected at admission (expected to be admitted)" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("label_exists:$display_name") + return + fi + + local actual_value + actual_value=$(echo "$result" | jq -r ".metadata.labels[\"$label_key\"] // empty") + + if [[ "$actual_value" == "$expected_value" ]]; then + print_success_indent "$display_name has label $label_key=$expected_value" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + print_error_indent "$display_name was admitted but label '$label_key' was not set to '$expected_value' (got: '${actual_value:-}')" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("label_exists:$display_name") + fi +} + +# Check that a manifest is admitted and does NOT have a specific label in the server response. +# Uses server-side dry-run, which triggers mutation webhooks. +# Reads manifest YAML from stdin. +# Usage: check_label_not_exists "display name" "label-key" "hint" </dev/null); then + print_error_indent "$display_name was rejected at admission (expected to be admitted)" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("label_not_exists:$display_name") + return + fi + + local label_value + label_value=$(echo "$result" | jq -r ".metadata.labels[\"$label_key\"] // empty") + + if [[ -z "$label_value" ]]; then + print_success_indent "$display_name was admitted without label $label_key (correct)" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + print_error_indent "$display_name was admitted but unexpectedly has label $label_key=$label_value" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("label_not_exists:$display_name") + fi +}