diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..d8dc2823 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,111 @@ +# Agent Guidelines + +This guide is for AI agents working on the Open Ecosystem Challenges repository. +For structural scaffolding, use `scripts/new-adventure.sh`. This document covers +everything that is easy to miss. + +## Ground Rules + +- **Never assume — always ask.** If intent is unclear, ask before implementing. +- **Propose before acting.** For anything structural (new files, renames, moving things), + outline the approach and get confirmation first. +- **Be critical.** If something seems inconsistent or wrong, flag it. Don't silently comply. +- **Read the docs in place.** Before working on an adventure, read the contributing guide + and relevant documentation in the repo. Don't rely on assumptions or memory — read the + actual files. + +--- + +## Creating a New Adventure + +Never create adventure files manually. The correct flow is: + +1. An approved idea file must exist in `ideas/` before any implementation begins. + Never start building an adventure without one. +2. Run `scripts/new-adventure.sh` to scaffold the adventure structure from the idea file. + This ensures consistent structure across all adventures. + +--- + +## Building Challenges + +> Read [docs/contributing/adventures.md](./docs/contributing/adventures.md) first — it covers the full step-by-step process. +> The sections below capture things that are easy to miss on top of that guide. + + +### Theme consistency + +Every technical element should have a thematic counterpart — label keys, error messages, +pod names, policy descriptions. Thematic flavor enhances the experience but must never +obscure what the player needs to understand technically. + +When in doubt: combine both. A Roman-themed error message can still explain *why* the +policy exists. + +### Intentional bugs + +- Always work backwards from the solution. Implement the fully working state first, + verify it, then introduce bugs from there. Never design the buggy state first. +- One concept per bug — keep bugs focused and isolated. +- Keep challenge artifacts minimal. Only include what is directly relevant to the concept + being tested — anything extra distracts beginners. For example, a test workload should + not include security context fields or resource limits that aren't part of the challenge. + +### verify.sh + +- Always define an `OBJECTIVE` variable at the top that matches the wording in the docs exactly. +- Use inline YAML heredocs for test manifests — never reference files from `manifests/workloads/` + since those are player-editable. The verify script must be self-contained. +- Extend `lib/scripts/` with reusable helper functions rather than writing logic inline + in verify.sh. Keep verify.sh a clean list of check calls. +- Hints must be **directional, not prescriptive**: + - Bad: `"is the validationFailureAction set to Enforce?"` + - Good: `"what happens when a violation is detected?"` + - The hint should point players to *where* to look, not *what* to change. + +### Devcontainer / post-start + +- `post-create.sh`: install tools, set up the cluster, install dependencies. +- `post-start.sh`: deploy the initial challenge state (policies + workloads). The goal is + that players open their Codespace and the problem is immediately visible — not a blank + slate. The broken state should be the first thing they see. + +### Makefile + +Keep it minimal. For a typical challenge level: +- `make apply` — reset everything (delete known pods, re-apply policies, re-apply workloads) +- `make verify` — run verify.sh + +Don't add targets that duplicate verify.sh logic or that give players shortcuts that +bypass learning. + +### Shared scripts + +`lib/scripts/loader.sh` sources all shared helper functions automatically. Always source +it at the top of verify.sh. Before writing any verification logic, check whether a helper +already exists in `lib/scripts/` — prefer extending the shared library over writing +inline logic. + +### Pre-release checklist + +When explicitly asked to review an adventure for release readiness, check: + +- Deadline is filled in (not empty) +- Community thread link is not a `TODO` +- `Status: Coming Soon` is updated to reflect actual state +- Adventure has a real number assigned (not `00`, which is the placeholder for planned adventures) +- No `` comments remain in any docs +- verify.sh hints have been reviewed — none give away the exact fix +- `OBJECTIVE` in verify.sh matches the `## 🎯 Objective` section in the docs + +Only make changes to these when explicitly prompted to do so. + +### Docs + +- The `## 🎯 Objective` section in the docs and the `OBJECTIVE` variable in verify.sh + must match. +- `## 🧠 What You'll Learn` bullet points should link to the relevant docs pages. +- Hints in `verify.sh` must not appear in the docs — the docs describe *what* to achieve, + not *how* to fix it. +- The `## 🏗️ Architecture` section should explicitly tell players which files to edit + and which to leave alone. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/adventures/planned/00-lex-imperfecta/README.md b/adventures/planned/00-lex-imperfecta/README.md new file mode 100644 index 00000000..f27ea873 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/README.md @@ -0,0 +1,16 @@ +# ⚖️ Adventure 00: Lex Imperfecta + +The Roman Republic has built a sophisticated legal system to protect its citizens — but the laws were written +in haste, and the exceptions were written too generously. Policies go unenforced, the wrong citizens are exempt, and +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 + +The entire **infrastructure is pre-provisioned in your Codespace** +**You don't need to set up anything locally. Just focus on solving the problem.** + +## 🚀 Ready to Start? + +[Choose your level](https://dynatrace-oss.github.io/open-ecosystem-challenges/00-lex-imperfecta/) and begin +learning! diff --git a/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/devcontainer.json b/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/devcontainer.json new file mode 100644 index 00000000..4d4b95cb --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/devcontainer.json @@ -0,0 +1,25 @@ +{ + "name": "⚖️ Adventure 00 | 🟢 Beginner (The Twelve Tables)", + "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/adventures/planned/00-lex-imperfecta/beginner", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "postCreateCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/00-lex-imperfecta_01-beginner/post-create.sh", + "postStartCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/00-lex-imperfecta_01-beginner/post-start.sh", + "customizations": { + "codespaces": { + "openFiles": [ + "adventures/planned/00-lex-imperfecta/README.md" + ], + "permissions": { + "codespaces": "write" + } + } + }, + "forwardPorts": [], + "portsAttributes": {}, + "otherPortsAttributes": { + "onAutoForward": "ignore" + } +} 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 new file mode 100755 index 00000000..4b81324a --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/post-create.sh @@ -0,0 +1,18 @@ +#!/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" "beginner" +track_codespace_created + +"$REPO_ROOT/lib/shared/init.sh" --version v0.17.0 +"$REPO_ROOT/lib/kubernetes/init.sh" \ + --kind-version v0.31.0 \ + --kubectl-version v1.35.0 \ + --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 diff --git a/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/post-start.sh b/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/post-start.sh new file mode 100755 index 00000000..27fc1a8d --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/.devcontainer/00-lex-imperfecta_01-beginner/post-start.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CHALLENGE_DIR="$REPO_ROOT/adventures/planned/00-lex-imperfecta/beginner" + +echo "✨ Starting Lex Imperfecta - Beginner Level" + +echo "⚖️ Applying policies..." +kubectl apply -f "$CHALLENGE_DIR/manifests/policies/" + +echo "🏛️ Deploying pods..." +# Some pods may be blocked by the misconfigured policies — this is intentional. +# Run 'kubectl get pods' to see the current state and start investigating. +kubectl apply -f "$CHALLENGE_DIR/manifests/pods/" 2>&1 || true + +# shellcheck disable=SC1091 +source "$REPO_ROOT/lib/scripts/tracker.sh" +set_tracking_context "lex-imperfecta" "beginner" +track_codespace_initialized diff --git a/adventures/planned/00-lex-imperfecta/beginner/Makefile b/adventures/planned/00-lex-imperfecta/beginner/Makefile new file mode 100644 index 00000000..b40317a1 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/Makefile @@ -0,0 +1,31 @@ +.PHONY: help apply verify + +help: + @echo "Lex Imperfecta - Beginner: Available Commands:" + @echo " make apply Apply policies and re-deploy all workloads" + @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 + @echo "Applied Policies:"; \ + for f in manifests/policies/*.yaml; do \ + name=$$(grep '^ name:' "$$f" | awk '{print $$2}'); \ + kubectl apply -f "$$f" > /dev/null 2>&1 && echo " $$name"; \ + done; \ + echo "" + @echo "Applied Pods:"; \ + blocked=""; \ + for f in manifests/pods/*.yaml; do \ + name=$$(grep '^ name:' "$$f" | awk '{print $$2}'); \ + if kubectl apply -f "$$f" > /dev/null 2>&1; then \ + echo " $$name"; \ + else \ + blocked="$$blocked $$name"; \ + fi; \ + done; \ + echo ""; \ + echo "Blocked Pods:"; \ + for name in $$blocked; do echo " $$name"; done + +verify: + @./verify.sh diff --git a/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/compliant.yaml b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/compliant.yaml new file mode 100644 index 00000000..2f18a458 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/compliant.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: compliant + labels: + republic.rome/gens: forum-romanum +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/pods/missing-labels.yaml b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/missing-labels.yaml new file mode 100644 index 00000000..b7b437d3 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/missing-labels.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: missing-labels +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/pods/privileged-init-container.yaml b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/privileged-init-container.yaml new file mode 100644 index 00000000..d22d8a75 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/privileged-init-container.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Pod +metadata: + name: privileged-init-container + labels: + republic.rome/gens: castra-praetoria +spec: + restartPolicy: Never + initContainers: + - name: init + image: busybox:stable + command: ["sh", "-c", "exit 0"] + securityContext: + privileged: true + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false diff --git a/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/privileged.yaml b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/privileged.yaml new file mode 100644 index 00000000..83c5cac7 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/pods/privileged.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: privileged + labels: + republic.rome/gens: castra-praetoria +spec: + restartPolicy: Never + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: true 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 new file mode 100644 index 00000000..a0264b71 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/no-privileged-containers.yaml @@ -0,0 +1,34 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: no-privileged-containers + annotations: + policies.kyverno.io/title: No Privileged Containers + policies.kyverno.io/category: Pod Security + policies.kyverno.io/severity: high + policies.kyverno.io/subject: Pod + policies.kyverno.io/description: >- + The Senate forbids unchecked power. Running containers with elevated privileges + 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 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 new file mode 100644 index 00000000..a69443c9 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/manifests/policies/require-labels.yaml @@ -0,0 +1,28 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-labels + annotations: + policies.kyverno.io/title: Require Labels + policies.kyverno.io/category: Best Practices + policies.kyverno.io/severity: medium + policies.kyverno.io/subject: Pod, Label + policies.kyverno.io/description: >- + 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: "?*" diff --git a/adventures/planned/00-lex-imperfecta/beginner/verify.sh b/adventures/planned/00-lex-imperfecta/beginner/verify.sh new file mode 100755 index 00000000..1a8a5f61 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/beginner/verify.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Load shared libraries +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/../../../../lib/scripts/loader.sh" + +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 +- 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" + +print_header \ + 'Challenge 00: Lex Imperfecta' \ + 'Level 01: The Twelve Tables' \ + 'Verification' + +# Init test counters +TESTS_PASSED=0 +TESTS_FAILED=0 +FAILED_CHECKS=() + +check_prerequisites kubectl + +print_sub_header "Running verification checks..." + +# ============================================================================= +# Objective 1: Workloads missing republic.rome/gens label are blocked +# ============================================================================= +print_new_line +print_sub_header "1. Checking that unlabelled workloads are blocked..." + +check_admission_blocked \ + "Pod without republic.rome/gens label" \ + "Check the 'require-labels' policy — what happens when a violation is detected?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-no-label +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_allowed \ + "Pod with republic.rome/gens label" \ + "Check the 'require-labels' policy — is the label pattern correct?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-with-label + labels: + republic.rome/gens: forum-romanum +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + + + +# ============================================================================= +# Objective 2: Workloads running as privileged containers are blocked +# ============================================================================= +print_new_line +print_sub_header "2. Checking that privileged workloads are blocked..." + +check_admission_blocked \ + "Pod with privileged container" \ + "Check the 'no-privileged-containers' policy — does the deny condition cover regular containers?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-privileged + labels: + republic.rome/gens: castra-praetoria +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: true +EOF + +check_admission_blocked \ + "Pod with privileged init container" \ + "Check the 'no-privileged-containers' policy — does the deny condition cover init containers?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-privileged-init + labels: + republic.rome/gens: castra-praetoria +spec: + initContainers: + - name: init + image: busybox:stable + command: ["sh", "-c", "exit 0"] + securityContext: + privileged: true + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_allowed \ + "Pod with non-privileged container" \ + "Check the 'no-privileged-containers' policy — is the deny condition correct?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-non-privileged + labels: + republic.rome/gens: forum-romanum +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + + + +# ============================================================================= +# Objective 3: Compliant workloads deploy and run successfully +# ============================================================================= +print_new_line +print_sub_header "3. Checking that compliant workloads are admitted..." + +check_admission_allowed \ + "Fully compliant pod (label + non-privileged)" \ + "Check both policies — does the pod carry the required label and run without elevated privileges?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-compliant + labels: + republic.rome/gens: forum-romanum +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +# ============================================================================= +# Summary & Next Steps +# ============================================================================= +failed_checks_json="[]" +if [[ -n "${FAILED_CHECKS[*]:-}" ]]; then + failed_checks_json=$(printf '%s\n' "${FAILED_CHECKS[@]}" | jq -R . | jq -s .) +fi + +if [[ $TESTS_FAILED -gt 0 ]]; then + # Track failure + track_verification_completed "failed" "$failed_checks_json" + + print_verification_summary "lex-imperfecta" "$DOCS_URL" "$OBJECTIVE" + exit 1 +fi + +# Track success +track_verification_completed "success" "$failed_checks_json" + +# Success! +print_header "Test Results Summary" +print_success "✅ PASSED: All $TESTS_PASSED verification checks passed!" +print_new_line + +# Run submission readiness checks +check_submission_readiness "00-lex-imperfecta" "beginner" diff --git a/adventures/planned/00-lex-imperfecta/docs/beginner.md b/adventures/planned/00-lex-imperfecta/docs/beginner.md new file mode 100644 index 00000000..70351030 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/docs/beginner.md @@ -0,0 +1,154 @@ +# 🟢 Beginner: The Twelve Tables + +> **Best suited for:** Platform engineers, SREs, and developers curious about Kubernetes security — no prior Kyverno experience needed, but familiarity with basic `kubectl` and YAML will help. + +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." + +Your mission: investigate the Kyverno policies and restore proper admission control before chaos reaches the city. + +## 🏗️ Architecture + +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`. + +The pods in `manifests/pods/` are there for reference only — **you don't need to edit them**. + +No GitOps, no dashboards — just you, the policies, and the cluster. + +## 🎯 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 +- 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 to use [custom label keys](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to enforce workload identity standards + +## 🧰 Toolbox + +Your Codespace comes pre-configured with the following tools: + +| Tool | What it's for | +|------|---------------| +| `kubectl` | Apply and inspect cluster resources | +| `kyverno` CLI | Test and lint policies locally before applying | +| `k9s` | Explore cluster resources in a terminal UI | + +## ⏰ Deadline + +> ℹ️ You can still complete the challenge after this date, but points will only be awarded for submissions before the +> deadline. + +## 💬 Join the discussion + +Share your solutions and questions in +the [challenge thread](TODO) +in the Open Ecosystem Community. + +## ✅ How to Play + +### 1. Start Your Challenge + +> 📖 **First time?** Check out the [Getting Started Guide](../../start-a-challenge) for detailed instructions on +> forking, starting a Codespace, and waiting for infrastructure setup. + +Quick start: + +- Fork the [repo](https://github.com/dynatrace-oss/open-ecosystem-challenges/) +- Create a Codespace +- Select "⚖️ Adventure 00 | 🟢 Beginner (The Twelve Tables)" +- Wait a couple of minutes for the environment to initialize (`Cmd/Ctrl + Shift + P` → `View Creation Log` to view progress) + +### 2. Explore the Cluster + +When your Codespace is ready, four pods are already running — or trying to. Open a terminal and check what's going on: + +```bash +kubectl get pods +``` + +Inspect why a pod was blocked or admitted: + +```bash +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 +``` + +You can also launch **k9s** for a terminal UI view of all cluster resources: + +```bash +k9s +``` + +Navigate to `ClusterPolicy` resources with `:clusterpolicies` to inspect both 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. + +#### Test Locally with the Kyverno CLI + +Before applying to the cluster, you can use the `kyverno` CLI to test your policy changes locally against the workload manifests: + +```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 +``` + +This gives you fast feedback without touching the cluster. + +#### Apply to the Cluster + +Once you're happy with your changes, re-apply everything: + +```bash +make apply +``` + +This re-applies the policies and re-deploys all workloads so you immediately see the effect of your changes. + +#### 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) + +### 4. Verify Your Solution + +Once you think you've solved the challenge, run the verification script: + +```bash +./verify.sh +# or: make verify +``` + +**If the verification fails:** + +The script will tell you which checks failed and give you a hint. Fix the issues and run it again. + +**If the verification passes:** + +1. The script will check if your changes are committed and pushed. +2. Follow the on-screen instructions to commit your changes if needed. +3. Once everything is ready, the script will generate a **Certificate of Completion**. +4. **Copy this certificate** and paste it into + the [challenge thread](TODO) + to claim your victory! 🏆 diff --git a/adventures/planned/00-lex-imperfecta/docs/index.md b/adventures/planned/00-lex-imperfecta/docs/index.md new file mode 100644 index 00000000..eac337b9 --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/docs/index.md @@ -0,0 +1,28 @@ +# ⚖️ Adventure 00: Lex Imperfecta + +The Republic's legal system is in disarray — workloads run unchecked, required labels go missing, and privileged containers slip through the gates. As a newly appointed Praetor, your mission is to restore order by fixing broken Kyverno policies and enforcing proper admission control. + +**Technologies:** Kyverno, 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. + +## 🪐 The Backstory + +The Roman Republic has built a sophisticated legal system to protect its citizens — but the laws were written +in haste, and the exceptions were written too generously. Policies go unenforced, the wrong citizens are exempt, and +something has slipped through the gates unnoticed. As a newly appointed Praetor, your mission is to restore order before +chaos takes hold. + +## 🎮 Choose Your Level + +Each level is a standalone challenge with its own Codespace that builds on the story while being technically +independent — pick your level and start wherever you feel comfortable. + +### 🟢 Beginner: The Twelve Tables + +- **Status:** 🚧 Coming Soon +- **Topics:** Kyverno, Falco, Policy Reporter, Argo CD, Kubernetes + +Fix broken Kyverno policies to restore proper admission control. + +[**Start the Beginner Challenge**](./beginner.md){ .md-button .md-button--primary } diff --git a/adventures/planned/00-lex-imperfecta/mkdocs.yaml b/adventures/planned/00-lex-imperfecta/mkdocs.yaml new file mode 100644 index 00000000..275892ff --- /dev/null +++ b/adventures/planned/00-lex-imperfecta/mkdocs.yaml @@ -0,0 +1,5 @@ +site_name: '⚖️ 00: Lex Imperfecta' + +nav: + - Introduction: index.md + - '🟢 Beginner': beginner.md diff --git a/ideas/lex-imperfecta.md b/ideas/.implemented/lex-imperfecta.md similarity index 100% rename from ideas/lex-imperfecta.md rename to ideas/.implemented/lex-imperfecta.md diff --git a/ideas/may-the-fourth-be-with-you.md b/ideas/may-the-fourth-be-with-you.md deleted file mode 100644 index 33aac133..00000000 --- a/ideas/may-the-fourth-be-with-you.md +++ /dev/null @@ -1,129 +0,0 @@ -# Adventure Idea: ⚔️ May the 4th Be With You - -## Overview - -**Theme:** The Alliance's most critical secret - the Death Star schematics, stored deep within the cluster at Massassi Station. ISB (Imperial Security Bureau) agents have infiltrated the network, and the defenses meant to protect the plans are broken, misconfigured, or simply not enough. As a Rebel Engineer in General Draven's intelligence cell, your mission: detect the breach, contain the threat, and prove the Rebellion's defenses will hold. - -**Skills:** - -- Detect unauthorized file access using eBPF-based runtime monitoring -- Contain threats by enforcing least-privilege access and kernel-level syscall interception -- Prove security defenses work by adversarially testing them - -**Technologies:** [Tetragon](https://tetragon.io/), [Kubernetes](https://kubernetes.io/), [Chainsaw](https://kyverno.github.io/chainsaw/) - ---- - -## Levels - -### 🟢 Beginner: Disturbance in the Force - -#### Description - -Fix a broken Tetragon TracingPolicy to detect unauthorized access to the Death Star schematics. - -#### Story - -The Death Star schematics are stored deep within the cluster at Massassi Station, the Rebellion's most closely guarded secret. Reports have confirmed what the Force already whispered: ISB agents have infiltrated the network. A fellow engineer deployed Tetragon and wrote a TracingPolicy to stand guard, a silent tripwire on the schematics file. But something is wrong. The policy is applied, Tetragon is running, and nothing fires. - -The agent could be reading the plans right now. Your mission: find what's broken in the TracingPolicy and restore the watch before the Empire learns what the Rebellion knows. - -#### The Problem - -A TracingPolicy is deployed in the cluster but has two deliberate mistakes: the wrong syscall (`sys_read` instead of `sys_openat`) and a wrong path prefix (e.g. `/etc/plans/` instead of the actual schematics path). The ISB agent's workload is already running and periodically reads the schematics file, but the broken policy never fires. The participant must identify and fix both mistakes until `tetra getevents` shows the agent's file access. - -#### Objective - -By the end of this level, the learner should: - -- Have a TracingPolicy active that fires an event when the schematics file is read by an unauthorized entity -- See the ISB agent's file access appear in `tetra getevents` output -- Confirm the policy only fires on access to the schematics file, not on unrelated file access - -#### What You'll Learn - -- How Tetragon TracingPolicies work: syscall hooks and path filters -- How to inspect Tetragon events using the `tetra` CLI -- The difference between "Tetragon is running" and "Tetragon is watching the right thing" - -#### Tools & Infrastructure - -- **Tools:** `kubectl`, `tetra` CLI, `k9s` -- **Infrastructure:** Kubernetes cluster, Tetragon - ---- - -### 🟡 Intermediate: The Phantom Plans - -#### Description - -Fix misconfigured RBAC and a broken Tetragon enforcement policy to contain an ISB agent that already slipped past detection. - -#### Story - -The agent was caught - briefly. Tetragon fired, the event was logged, and the Rebellion breathed a sigh of relief. But relief was premature. Before the TracingPolicy ever fired, the agent's pod had quietly pulled classified intel through the Kubernetes API using a ServiceAccount with sweeping permissions across the cluster. And your TracingPolicy? It watched. It did not act. - -Another member of Draven's cell started hardening the defences, tightening the ServiceAccount and configuring Tetragon to respond rather than just observe. The work was left unfinished. The RBAC is misconfigured and the enforcement action is broken. The agent is still in the cluster, and the schematics are still readable. Your mission: lock down what the agent can reach, and make the Death Star schematics vanish the moment an unauthorised process touches them. - -#### The Problem - -Two independent issues exist in the cluster. First, a legitimate workload's ServiceAccount is bound to a ClusterRole that grants read access to all Secrets and ConfigMaps across every namespace - far broader than needed. The ISB agent exploited this to pull classified intel through the Kubernetes API. The binding must be scoped down to only the permissions the workload legitimately requires. Second, a Tetragon TracingPolicy with an `override` action exists but is misconfigured. The action is defined but not correctly wired to the selector, so the syscall is never intercepted and the file remains readable. - -#### Objective - -By the end of this level, the learner should: - -- Identify and fix the over-permissive ServiceAccount that the ISB agent exploited, so no workload has access beyond what it legitimately needs -- Have a Tetragon TracingPolicy active that overrides the syscall when the schematics file is accessed, making the file appear to not exist to the agent process -- Confirm the agent process can no longer read the schematics - -#### What You'll Learn - -- How Kubernetes RBAC controls what identities can access via the API and how over-permissive bindings create silent vulnerabilities -- The difference between Tetragon detecting an event and Tetragon responding to one -- How syscall override works as a deception technique that stops an attack without revealing detection - -#### Tools & Infrastructure - -- **Tools:** `kubectl`, `tetra` CLI, `k9s` -- **Infrastructure:** Kubernetes cluster, Tetragon - ---- - -### 🔴 Expert: Trust in the Force, Verify the Policy - -#### Description - -Complete a broken Chainsaw test suite that simulates the full attack chain and proves the cluster's TracingPolicies and RBAC defenses actually work. - -#### Story - -General Draven is satisfied. General Dodonna is not. Detection is in place, the override is active, the RBAC is locked down. Dodonna has one more question: "How do we know these defenses will hold next time?" The Rebellion trusts its engineers, but trust without proof is just hope. You've never formally proven your policies work. You assumed they did because you configured them. - -Another engineer started writing Chainsaw tests to simulate the attack: access the schematics file, attempt to escalate via the over-permissive ServiceAccount. The tests exist but are incomplete - missing assertions, broken attack simulations, and steps that don't actually trigger the policies. Your mission: complete the suite so it reliably passes on a correctly defended cluster and fails the moment a defense is removed. - -> 💡 **Beyond the challenge:** *"Secured the door, you have. Who forged the key, you never asked."* Runtime security catches what happens inside the cluster - but what about what was baked into the image before it ever arrived? Explore [Sigstore/cosign](https://docs.sigstore.dev/) to see what image provenance verification looks like, and imagine what a Chapter IV might look like. - -#### The Problem - -A Chainsaw test suite exists but is incomplete. Some tests are missing assertion steps — they simulate the attack but never check whether Tetragon fired or the override blocked the read. Others have broken attack simulation steps that don't actually trigger the TracingPolicies. The RBAC escalation scenario is missing entirely. The participant must fix and complete the suite so all tests pass on a correctly configured cluster, and at least one test fails when a TracingPolicy is removed and one fails when the RBAC fix is reverted. - -#### Objective - -By the end of this level, the learner should: - -- Have a Chainsaw test suite that simulates the full attack chain: unauthorized file access and ServiceAccount escalation -- All tests pass against a correctly configured cluster -- At least one test fails when a TracingPolicy is removed, proving the tests have real signal -- At least one test fails when the RBAC fix is reverted - -#### What You'll Learn - -- How Chainsaw works for Kubernetes-native testing of security scenarios -- How to think adversarially about your own defenses ("would this actually catch anything?") -- Why runtime defenses need to be tested, not assumed - -#### Tools & Infrastructure - -- **Tools:** `kubectl`, `tetra` CLI, `chainsaw` CLI, `k9s` -- **Infrastructure:** Kubernetes cluster, Tetragon diff --git a/lib/argo-rollouts/init.sh b/lib/argo-rollouts/init.sh index 1d391844..cb8e6475 100755 --- a/lib/argo-rollouts/init.sh +++ b/lib/argo-rollouts/init.sh @@ -45,8 +45,10 @@ echo "✨ Waiting for Argo Rollouts controller to be ready" kubectl rollout status deployment/argo-rollouts -n argo-rollouts --timeout=300s echo "✨ Installing Argo Rollouts Kubectl plugin" -curl -LO "https://github.com/argoproj/argo-rollouts/releases/download/${version}/kubectl-argo-rollouts-linux-amd64" -chmod +x ./kubectl-argo-rollouts-linux-amd64 -sudo mv ./kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts +# shellcheck disable=SC1091 +source "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../scripts/arch.sh" +curl -LO "https://github.com/argoproj/argo-rollouts/releases/download/${version}/kubectl-argo-rollouts-linux-${ARCH}" +chmod +x "./kubectl-argo-rollouts-linux-${ARCH}" +sudo mv "./kubectl-argo-rollouts-linux-${ARCH}" /usr/local/bin/kubectl-argo-rollouts echo "✅ Argo Rollouts is ready" \ No newline at end of file diff --git a/lib/argocd/init.sh b/lib/argocd/init.sh index 117c3725..ed3f1c49 100755 --- a/lib/argocd/init.sh +++ b/lib/argocd/init.sh @@ -56,9 +56,11 @@ sed -i "s|argoproj/argo-cd/[^/]*/manifests/install.yaml|argoproj/argo-cd/${versi kubectl apply -k "${manifests_tmp}" echo "✨ Installing Argo CD CLI" -curl -sSL -o argocd-linux-amd64 "https://github.com/argoproj/argo-cd/releases/download/${version}/argocd-linux-amd64" -sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd -rm argocd-linux-amd64 +# shellcheck disable=SC1091 +source "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../scripts/arch.sh" +curl -sSL -o "argocd-linux-${ARCH}" "https://github.com/argoproj/argo-cd/releases/download/${version}/argocd-linux-${ARCH}" +sudo install -m 555 "argocd-linux-${ARCH}" /usr/local/bin/argocd +rm "argocd-linux-${ARCH}" echo "✨ Waiting for Argo CD server to be ready" kubectl rollout status deployment/argocd-server -n argocd --timeout=300s diff --git a/lib/kubernetes/init.sh b/lib/kubernetes/init.sh index 102558cc..f19560e9 100755 --- a/lib/kubernetes/init.sh +++ b/lib/kubernetes/init.sh @@ -95,11 +95,14 @@ if [[ -z "$helm_version" ]]; then exit 1 fi +# shellcheck disable=SC1091 +source "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../scripts/arch.sh" + echo "✨ Installing Kind" curl -sS "https://webi.sh/kind@${kind_version}" | sh echo "✨ Installing kubectl" -curl -LO "https://dl.k8s.io/release/${kubectl_version}/bin/linux/amd64/kubectl" +curl -LO "https://dl.k8s.io/release/${kubectl_version}/bin/linux/${ARCH}/kubectl" chmod +x kubectl sudo mv kubectl /usr/local/bin/ @@ -110,11 +113,11 @@ echo "✨ Installing k9s" curl -sS "https://webinstall.dev/k9s@${k9s_version}" | bash echo "✨ Installing Helm" -curl -LO "https://get.helm.sh/helm-${helm_version}-linux-amd64.tar.gz" -tar -zxvf "helm-${helm_version}-linux-amd64.tar.gz" -chmod +x linux-amd64/helm -sudo mv linux-amd64/helm /usr/local/bin/helm -rm -rf linux-amd64 "helm-${helm_version}-linux-amd64.tar.gz" +curl -LO "https://get.helm.sh/helm-${helm_version}-linux-${ARCH}.tar.gz" +tar -zxvf "helm-${helm_version}-linux-${ARCH}.tar.gz" +chmod +x "linux-${ARCH}/helm" +sudo mv "linux-${ARCH}/helm" /usr/local/bin/helm +rm -rf "linux-${ARCH}" "helm-${helm_version}-linux-${ARCH}.tar.gz" echo "✨ Starting Kind cluster" kind create cluster --config "$SCRIPT_DIR/config.yaml" --wait 300s diff --git a/lib/kyverno/init.sh b/lib/kyverno/init.sh new file mode 100755 index 00000000..10d1ac0c --- /dev/null +++ b/lib/kyverno/init.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -e + +help() { + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " --help Display this help message" + echo " --version Kyverno Helm chart version to install (required)" + echo " --cli-version Kyverno CLI version to install (required)" +} + +# Parse flags +version="" +cli_version="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --help) + help + exit 0 + ;; + --version) + if [[ -z "${2-}" ]]; then + echo "Error: --version requires a value" >&2 + exit 1 + fi + version="$2" + shift 2 + ;; + --cli-version) + if [[ -z "${2-}" ]]; then + echo "Error: --cli-version requires a value" >&2 + exit 1 + fi + cli_version="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$version" ]]; then + echo "Error: --version is required" >&2 + exit 1 +fi + +if [[ -z "$cli_version" ]]; then + echo "Error: --cli-version is required" >&2 + exit 1 +fi + +echo "✨ Installing Kyverno" +helm repo add kyverno https://kyverno.github.io/kyverno/ +helm repo update +helm install kyverno kyverno/kyverno \ + --namespace kyverno \ + --create-namespace \ + --version "$version" \ + --set admissionController.replicas=1 \ + --set features.policyExceptions.enabled=true \ + --wait + +echo "✨ Waiting for Kyverno admission controller to be ready" +kubectl rollout status deployment/kyverno-admission-controller -n kyverno --timeout=300s + +echo "✨ Installing Kyverno CLI" +# shellcheck disable=SC1091 +source "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../scripts/arch.sh" +# Kyverno CLI release assets use x86_64 instead of amd64 +if [[ "$ARCH" == "amd64" ]]; then + CLI_ARCH="x86_64" +else + CLI_ARCH="$ARCH" +fi +TARBALL="kyverno-cli_${cli_version}_linux_${CLI_ARCH}.tar.gz" +curl -LO "https://github.com/kyverno/kyverno/releases/download/${cli_version}/${TARBALL}" +tar -xf "${TARBALL}" kyverno +chmod +x kyverno +sudo mv kyverno /usr/local/bin/kyverno +rm "${TARBALL}" + +echo "✅ Kyverno is ready" \ No newline at end of file diff --git a/lib/open-tofu/init.sh b/lib/open-tofu/init.sh index 87aa0e03..977690c9 100755 --- a/lib/open-tofu/init.sh +++ b/lib/open-tofu/init.sh @@ -38,12 +38,14 @@ if [[ -z "$version" ]]; then fi echo "✨ Installing Open Tofu" +# shellcheck disable=SC1091 +source "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../scripts/arch.sh" -curl -LO "https://github.com/opentofu/opentofu/releases/download/${version}/tofu_${version#v}_linux_amd64.zip" -unzip "tofu_${version#v}_linux_amd64.zip" tofu +curl -LO "https://github.com/opentofu/opentofu/releases/download/${version}/tofu_${version#v}_linux_${ARCH}.zip" +unzip "tofu_${version#v}_linux_${ARCH}.zip" tofu chmod +x tofu sudo mv tofu /usr/local/bin/tofu -rm -f "tofu_${version#v}_linux_amd64.zip" +rm -f "tofu_${version#v}_linux_${ARCH}.zip" tofu version echo "✅ Open Tofu is ready" \ No newline at end of file diff --git a/lib/scripts/arch.sh b/lib/scripts/arch.sh new file mode 100644 index 00000000..c7bf9eb5 --- /dev/null +++ b/lib/scripts/arch.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Source this file to get ARCH set to "amd64" or "arm64". +ARCH=$(uname -m) +case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; +esac +export ARCH diff --git a/lib/scripts/kubernetes.sh b/lib/scripts/kubernetes.sh index cce766a6..79e46a32 100644 --- a/lib/scripts/kubernetes.sh +++ b/lib/scripts/kubernetes.sh @@ -244,5 +244,42 @@ check_resource_version() { fi } +# Check that a manifest is blocked at admission (dry-run=server expects failure) +# Reads manifest YAML from stdin. +# Usage: check_admission_blocked "display name" "hint" </dev/null; then + print_error_indent "$display_name was admitted — policy not enforcing" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("admission_blocked:$display_name") + else + print_success_indent "$display_name was correctly blocked at admission" + TESTS_PASSED=$((TESTS_PASSED + 1)) + fi +} + +# Check that a manifest is admitted (dry-run=server expects success) +# Reads manifest YAML from stdin. +# Usage: check_admission_allowed "display name" "hint" </dev/null; then + print_success_indent "$display_name was correctly admitted" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + print_error_indent "$display_name was incorrectly blocked at admission" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("admission_allowed:$display_name") + fi +} diff --git a/lib/shared/init.sh b/lib/shared/init.sh index 03726a32..61f13570 100755 --- a/lib/shared/init.sh +++ b/lib/shared/init.sh @@ -38,10 +38,8 @@ if [[ -z "$version" ]]; then fi echo "✨ Installing gum" -case "$(uname -m)" in - aarch64|arm64) ARCH="arm64" ;; - *) ARCH="amd64" ;; -esac +# shellcheck disable=SC1091 +source "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../scripts/arch.sh" curl -LO "https://github.com/charmbracelet/gum/releases/download/${version}/gum_${version#v}_${ARCH}.deb" sudo apt install "./gum_${version#v}_${ARCH}.deb"