Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .devcontainer/05-lex-imperfecta_02-intermediate/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "βš–οΈ Adventure 05 | 🟑 Intermediate (Governing the Provinces)",
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/adventures/05-lex-imperfecta/intermediate",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"postCreateCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/05-lex-imperfecta_02-intermediate/post-create.sh",
"postStartCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/05-lex-imperfecta_02-intermediate/post-start.sh",
"customizations": {
"codespaces": {
"openFiles": [
"manifests/policies/no-privileged-containers.yaml",
"manifests/policies/require-census.yaml",
"manifests/policies/aegyptus-require-scribe-role.yaml",
"manifests/exceptions/aegyptus-legacy-workload.yaml"
],
"permissions": {
"codespaces": "write"
}
}
},
"forwardPorts": [30110],
"portsAttributes": {
"30110": {
"label": "Policy Reporter",
"onAutoForward": "notify"
}
},
"otherPortsAttributes": {
"onAutoForward": "ignore"
}
}
19 changes: 19 additions & 0 deletions .devcontainer/05-lex-imperfecta_02-intermediate/post-create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

# shellcheck disable=SC1091
source "$REPO_ROOT/lib/scripts/tracker.sh"
set_tracking_context "lex-imperfecta" "intermediate" "05" "06" "2026"
track_container_created

"$REPO_ROOT/lib/shared/init.sh" --version v0.17.0
"$REPO_ROOT/lib/kubernetes/init.sh" \
--kind-version v0.32.0 \
--kubectl-version v1.36.1 \
--kubens-version v0.11.0 \
--k9s-version v0.50.18 \
--helm-version v4.2.0
"$REPO_ROOT/lib/kyverno/init.sh" --version 3.8.1 --cli-version v1.18.1
"$REPO_ROOT/lib/policy-reporter/init.sh" --version 3.7.4
61 changes: 61 additions & 0 deletions .devcontainer/05-lex-imperfecta_02-intermediate/post-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -e

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CHALLENGE_DIR="$REPO_ROOT/adventures/05-lex-imperfecta/intermediate"

echo "✨ Starting Lex Imperfecta - Intermediate Level"

echo "πŸ›οΈ Creating provinces..."
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: gallia
labels:
republic.rome/realm: province
---
apiVersion: v1
kind: Namespace
metadata:
name: hispania
labels:
republic.rome/realm: province
---
apiVersion: v1
kind: Namespace
metadata:
name: aegyptus
labels:
republic.rome/realm: province
---
apiVersion: v1
kind: Namespace
metadata:
name: britannia
labels:
republic.rome/realm: province
---
apiVersion: v1
kind: Namespace
metadata:
name: castra
labels:
republic.rome/realm: infra
EOF

echo "βš–οΈ Applying policies..."
kubectl apply -f "$CHALLENGE_DIR/manifests/policies/"

echo "πŸ“œ Applying exceptions..."
kubectl apply -f "$CHALLENGE_DIR/manifests/exceptions/"

echo "🏟️ Deploying workloads..."
# Some workloads may be blocked by misconfigured policies β€” this is intentional.
# Open Policy Reporter at http://localhost:30110 to start investigating.
kubectl apply -f "$CHALLENGE_DIR/manifests/workloads/" 2>&1 || true

# shellcheck disable=SC1091
source "$REPO_ROOT/lib/scripts/tracker.sh"
set_tracking_context "lex-imperfecta" "intermediate" "05" "06" "2026"
track_container_initialized
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ infra/tracker/tracker
.offon-session-id

# Custom ignores/includes
.prompts
.prompts
2 changes: 1 addition & 1 deletion adventures/05-lex-imperfecta/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ in haste, and the exceptions were written too generously. Policies go unenforced
something has slipped through the gates unnoticed. As a newly appointed Praetor, your mission is to restore order before
chaos takes hold.

**Technologies:** Kyverno, Falco, Policy Reporter, Argo CD, Kubernetes
**Technologies:** Kyverno, Falco, Policy Reporter, Kubernetes

The entire **infrastructure is pre-provisioned in your Codespace**
**You don't need to set up anything locally. Just focus on solving the problem.**
Expand Down
1 change: 1 addition & 0 deletions adventures/05-lex-imperfecta/docs/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ emoji: "βš–οΈ"

tags:
- Kyverno
- Policy Reporter
- Kubernetes

backstory:
Expand Down
184 changes: 184 additions & 0 deletions adventures/05-lex-imperfecta/docs/intermediate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
level: intermediate
emoji: "🟑"
title: "Governing the Provinces"
devcontainer: lex-imperfecta_intermediate
community_url: "" # TODO: add community thread URL once the adventure is live

summary: "Fix a misconfigured Kyverno policy estate and use Policy Reporter to restore proper governance across the Republic's provinces."

audience: >-
Platform engineers and SREs who have some familiarity with Kyverno β€” ideally after completing the Beginner level.
You should be comfortable reading Kubernetes YAML and basic kubectl commands.

backstory:
- >-
The Republic has grown. What once was a single city is now a sprawling empire of provinces, each governed by
different magistrates with different needs. The legal scholars decided to catalogue every law in a central
archive β€” the Tabularium β€” so that each province's statutes could be tracked and audited in one place.
- >-
But cataloguing the laws introduced new chaos. Policies meant for one province are bleeding into another.
Exceptions that were meant to be narrow have been written too broadly. And somewhere in the estate, a
workload is slipping through that shouldn't be.
- >-
The Tabularium's auditors have handed you a report: Policy Reporter shows violations where there should be
none, and silence where there should be enforcement. Your mission: investigate the policy estate, fix the
scoping issues, and restore order before the provinces descend into chaos.

objective:
- >-
The **empire-wide laws** enforce correctly across every province β€” privileged containers are blocked,
and every workload declares a valid `republic.rome/gens` and a `republic.rome/province` that matches the
namespace it runs in. Policies must scope to provinces using the namespace labels as the source of truth,
not by hardcoding individual namespace names.
- >-
**Aegyptus's provincial scribe law** takes effect only within Aegyptus β€” it admits only workloads carrying
`republic.rome/role: scribe` there, and has no power over the other provinces
- >-
The **legacy exception** is scoped to Aegyptus alone β€” only Aegyptus's grandfathered workload is spared the
census, and no workload in any other province can claim it
- >-
The **Tabularium's ledger** is clean and on file β€” the estate's policy report is exported in the OpenReports
format and saved as `estate-audit.yaml`

what_you_learn:
- >-
How to scope policies using [`ValidatingPolicy`](https://kyverno.io/docs/policy-types/validating-policy/)
(cluster-wide) and [`NamespacedValidatingPolicy`](https://kyverno.io/docs/policy-types/validating-policy/)
(per-namespace), and when to use each
- >-
How [CEL expressions](https://kubernetes.io/docs/reference/using-api/cel/) in `ValidatingPolicy` and
`PolicyException` express fine-grained admission conditions
- >-
How to write and scope a [`PolicyException`](https://kyverno.io/docs/policy-types/policy-exception/)
correctly so only the intended workloads are exempt
- >-
How to use [Policy Reporter](https://kyverno.github.io/policy-reporter/) and the
[OpenReports](https://kyverno.github.io/policy-reporter/guide/openreports/) format to audit and debug a
policy estate across multiple namespaces

architecture:
- >-
The Republic's policy estate spans five namespaces. Four are **provinces** β€” `gallia`, `hispania`,
`britannia`, and `aegyptus` β€” each carrying the label `republic.rome/realm: province`. The fifth,
`castra`, is the **infra** namespace (`republic.rome/realm: infra`) and operates outside the provinces'
legal jurisdiction. Use `kubectl get ns --show-labels` to inspect these labels β€” they are what the
cluster-wide policies use to decide where to enforce.
- >-
The estate is governed by a handful of Kyverno policies. Two are **empire-wide**:
`no-privileged-containers` blocks privileged containers, and `require-census` requires every workload to
declare its `republic.rome/gens` and a `republic.rome/province` that matches its namespace. Both are meant
to apply across the provinces but not the infra realm. Aegyptus also carries a **local law of its own** β€”
`aegyptus-require-scribe-role` β€” meant to admit only scribe workloads *within Aegyptus*. Finally, a
**`PolicyException`** grandfathers Aegyptus's single legacy workload, which predates the census, so it
alone may run without a gens. Some of these are no longer doing exactly what they were meant to.
- >-
**Edit `manifests/policies/` and `manifests/exceptions/`** β€” that is where the scoping has gone wrong.
Leave `manifests/workloads/` and `manifests/namespaces/` alone; the workloads represent the citizens and
their expected state, and the namespace labels are the ground truth the policies are measured against.
Policies act at admission, so after each change run `make apply` to redeploy the workloads and
re-evaluate the estate, then `make verify` to check your progress. When the estate is in order, file the
audit with the Tabularium (see How to Play).

architecture_diagram: "" # TODO: add diagram

toolbox:
- name: kubectl
url: "https://kubernetes.io/docs/reference/kubectl/"
description: Apply and inspect cluster resources, check namespace labels and policy status
- name: kyverno CLI
url: "https://kyverno.io/docs/kyverno-cli/"
description: Test and lint policies locally before applying to the cluster
- name: k9s
url: "https://k9scli.io/"
description: Explore cluster resources and policy reports in a terminal UI

services:
- name: Policy Reporter
port: 30110
description: Audit the full policy estate β€” see which policies apply to which namespaces and where violations exist

how_to_play:
- id: explore
title: "Explore the Estate"
content: |
When your Codespace is ready, the policy estate is already deployed β€” but something is wrong.
Open Policy Reporter at the forwarded port **30110** to get an overview of the estate:

- Which namespaces have violations?
- Which policies are generating results, and which are silent when they shouldn't be?

Then dig into the cluster:

```bash
# Inspect the namespace topology β€” the labels here drive policy scoping
kubectl get ns --show-labels

# List all policies β€” note which are cluster-wide and which are namespaced
kubectl get validatingpolicies
kubectl get namespacedvalidatingpolicies -A

# Inspect any policy or exception in full
kubectl get validatingpolicy <name> -o yaml
kubectl get policyexceptions -A -o yaml

# See the raw OpenReports data behind Policy Reporter
kubectl get policyreports -A
```

You can also launch **k9s** for a terminal UI view:

```bash
k9s
```

- id: fix
title: "Fix the Policies"
content: |
Review the [Objective](#objective) and investigate what is wrong in `manifests/policies/` and
`manifests/exceptions/`.

Think about what each policy is *supposed* to cover, and compare that against what it is *actually*
matching. The namespace labels you saw with `kubectl get ns --show-labels` are a key part of the picture.

**Test locally with the Kyverno CLI before applying:**

```bash
kyverno apply manifests/policies/require-census.yaml --resource manifests/workloads/citizens.yaml
kyverno apply manifests/policies/aegyptus-require-scribe-role.yaml --resource manifests/workloads/aegyptus-legacy-scribe.yaml
```

**Apply your changes to the cluster:**

```bash
make apply
```

Policies only act at admission, so `make apply` redeploys the workloads to re-evaluate the estate against
your changes. Then check Policy Reporter again β€” the picture should improve as you fix each issue.

- id: file
title: "File the Audit"
content: |
Once the estate is in order, the Senate expects the Tabularium's ledger on file. Export the cluster's
policy reports, the OpenReports data behind Policy Reporter, as the audit of record.

```bash
kubectl get policyreports -A -o yaml > estate-audit.yaml
```

helpful_links:
- title: Kyverno ValidatingPolicy
url: "https://kyverno.io/docs/policy-types/validating-policy/"
description: Reference docs for ValidatingPolicy and NamespacedValidatingPolicy β€” the policy types you'll fix
- title: Kyverno PolicyException
url: "https://kyverno.io/docs/policy-types/policy-exception/"
description: How to write and scope a PolicyException to exempt specific workloads
- title: CEL Validation Expressions
url: "https://kubernetes.io/docs/reference/using-api/cel/"
description: How CEL expressions work in Kubernetes admission β€” including accessing namespace context
- title: Policy Reporter
url: "https://kyverno.github.io/policy-reporter/"
description: How to use Policy Reporter to audit and visualise policy results across the cluster
- title: OpenReports Format
url: "https://kyverno.github.io/policy-reporter/guide/openreports/"
description: The OpenReports standard that Kyverno uses to emit PolicyReport resources
47 changes: 47 additions & 0 deletions adventures/05-lex-imperfecta/intermediate/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.PHONY: help apply verify

help:
@echo "Lex Imperfecta - Intermediate: Available Commands:"
@echo " make apply Reset workloads, re-apply policies and exceptions"
@echo " make verify Run the verification script"

apply:
@kubectl delete validatingpolicies --all --ignore-not-found > /dev/null 2>&1; \
kubectl delete namespacedvalidatingpolicies --all -A --ignore-not-found > /dev/null 2>&1; \
true
@echo "Applying Policies:"; \
for f in manifests/policies/*.yaml; do \
name=$$(grep '^ name:' "$$f" | head -1 | awk '{print $$2}'); \
out=$$(kubectl apply -f "$$f" 2>&1); rc=$$?; \
if [ $$rc -ne 0 ]; then \
echo ""; \
echo "$$out"; \
exit 1; \
fi; \
echo " $$name"; \
done; \
echo ""
@echo "Applying Exceptions:"; \
for f in manifests/exceptions/*.yaml; do \
name=$$(grep '^ name:' "$$f" | head -1 | awk '{print $$2}'); \
out=$$(kubectl apply -f "$$f" 2>&1); rc=$$?; \
if [ $$rc -ne 0 ]; then \
echo ""; \
echo "$$out"; \
exit 1; \
fi; \
echo " $$name"; \
done; \
echo ""
@echo "Deploying Workloads..."
@for ns in gallia hispania aegyptus britannia castra; do \
kubectl delete pods --all -n $$ns --ignore-not-found --grace-period=0 --force \
> /dev/null 2>&1 & \
done; \
wait
@kubectl apply -f manifests/workloads/ 2>&1 || true
@echo ""
@echo "Blocked workloads above mean a policy is enforcing. Run 'make verify' to check your progress."

verify:
@./verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata:
name: aegyptus-legacy-workload
namespace: aegyptus
annotations:
policies.kyverno.io/description: >-
The scribes of Aegyptus have served the Republic since before the Twelve
Tables were written. Their ancient tools cannot carry the gens label, so
the Senate has granted them a formal exception β€” workloads that declare
themselves legacy ('republic.rome/legacy: "true"') are spared the census.
spec:
policyRefs:
- name: require-census
kind: ValidatingPolicy
matchConditions:
- name: is-legacy
expression: >-
has(object.metadata.labels) &&
'republic.rome/legacy' in object.metadata.labels &&
object.metadata.labels['republic.rome/legacy'] == 'true'
Loading
Loading