diff --git a/.devcontainer/05-lex-imperfecta_03-expert/devcontainer.json b/.devcontainer/05-lex-imperfecta_03-expert/devcontainer.json new file mode 100644 index 0000000..86996b3 --- /dev/null +++ b/.devcontainer/05-lex-imperfecta_03-expert/devcontainer.json @@ -0,0 +1,35 @@ +{ + "name": "โš–๏ธ Adventure 05 | ๐Ÿ”ด Expert (Quis Custodiet)", + "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/adventures/05-lex-imperfecta/expert", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "postCreateCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/05-lex-imperfecta_03-expert/post-create.sh", + "postStartCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/05-lex-imperfecta_03-expert/post-start.sh", + "customizations": { + "codespaces": { + "openFiles": [ + "falco-rules.yaml", + "manifests/policies/no-privileged-containers.yaml" + ], + "permissions": { + "codespaces": "write" + } + } + }, + "forwardPorts": [30110, 30111], + "portsAttributes": { + "30110": { + "label": "Policy Reporter", + "onAutoForward": "notify" + }, + "30111": { + "label": "Falcosidekick UI", + "onAutoForward": "notify" + } + }, + "otherPortsAttributes": { + "onAutoForward": "ignore" + } +} diff --git a/.devcontainer/05-lex-imperfecta_03-expert/post-create.sh b/.devcontainer/05-lex-imperfecta_03-expert/post-create.sh new file mode 100755 index 0000000..ffe16b5 --- /dev/null +++ b/.devcontainer/05-lex-imperfecta_03-expert/post-create.sh @@ -0,0 +1,22 @@ +#!/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" "expert" "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 +"$REPO_ROOT/lib/falco/init.sh" \ + --falco-version 9.0.0 \ + --falcosidekick-version 0.13.1 diff --git a/.devcontainer/05-lex-imperfecta_03-expert/post-start.sh b/.devcontainer/05-lex-imperfecta_03-expert/post-start.sh new file mode 100755 index 0000000..ea54bf1 --- /dev/null +++ b/.devcontainer/05-lex-imperfecta_03-expert/post-start.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CHALLENGE_DIR="$REPO_ROOT/adventures/05-lex-imperfecta/expert" + +echo "โœจ Starting Lex Imperfecta - Expert 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 "๐Ÿ“œ Deploying census archive..." +kubectl apply -f "$CHALLENGE_DIR/manifests/secrets/" + +echo "โš–๏ธ Applying policies..." +kubectl apply -f "$CHALLENGE_DIR/manifests/policies/" + +echo "๐Ÿ“‹ Applying exceptions..." +kubectl apply -f "$CHALLENGE_DIR/manifests/exceptions/" + +echo "๐Ÿฆ… Loading Falco rules..." +helm upgrade falco falcosecurity/falco \ + --namespace falco \ + --reuse-values \ + --set-file 'customRules.praetorian-guard\.yaml='"$CHALLENGE_DIR/falco-rules.yaml" \ + --wait > /dev/null + +echo "๐Ÿงน Clearing Falcosidekick event history..." +kubectl exec -n falco falcosidekick-ui-redis-0 -- redis-cli FLUSHALL > /dev/null +kubectl rollout restart deployment/falcosidekick-ui -n falco > /dev/null +kubectl rollout status deployment/falcosidekick-ui -n falco --timeout=60s > /dev/null + +echo "๐ŸŸ๏ธ Deploying workloads..." +# The intruder (speculator) is among these. Some workloads may be blocked +# by a misconfigured exception โ€” open Falcosidekick UI at http://localhost:30111 +# and Policy Reporter at http://localhost:30110 to begin your investigation. +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" "expert" "05" "06" "2026" +track_container_initialized + +echo "" +echo "๐Ÿ›๏ธ The estate is deployed." +echo "" +echo " Policy Reporter: http://localhost:30110" +echo " Falcosidekick UI: http://localhost:30111" +echo "" +echo " The intruder is already in the estate. The Guard sees nothing." +echo " Run 'make verify' to check your progress." +echo "" diff --git a/adventures/05-lex-imperfecta/docs/expert.yaml b/adventures/05-lex-imperfecta/docs/expert.yaml new file mode 100644 index 0000000..0533625 --- /dev/null +++ b/adventures/05-lex-imperfecta/docs/expert.yaml @@ -0,0 +1,168 @@ +level: expert +emoji: "๐Ÿ”ด" +title: "Quis Custodiet" +devcontainer: lex-imperfecta_03-expert +community_url: "" # TODO: add community thread URL once the adventure is live + +summary: "An intruder is already inside the Republic, and the watchmen cannot see it. Fix the Praetorian Guard's broken detection rule, close the admission gap that let the intruder slip through, and seal the census archive against unauthorized access." + +audience: >- + Security engineers and platform engineers who want to explore the boundary between admission control + and runtime security. Completing the Intermediate level first is helpful but not required. You should + be comfortable reading Kyverno ValidatingPolicies and CEL expressions. No prior Falco experience + required; the level introduces it. + +backstory: + - >- + The Republic's defences have always rested on the law: block the wrong workloads at the gate, and + nothing bad gets in. But the Senate's Praetorian Guard was built for a different threat: not just the + workload that *should* have been stopped, but the one that slips through and acts badly at runtime. + The Guard watches the provinces through Falco, but tonight, the watchtower is dark. Someone broke + the rule that should fire when the census archive is touched. The Guard sees nothing. + - >- + And while the Guard slept, an intruder crept in. It declared valid labels, passed the census, and + presented itself as a loyal citizen of the Republic: *speculator*, a quiet auxiliary in Gallia. Its + papers were in order. Its power was not. Once inside, it reached straight for the census archive: + the imperial rolls of every citizen, sealed records it had no right to touch. It reads them on a + loop and tries to send them out of the Republic. + - >- + By the time the auditors noticed, three cracks had opened. The Guard's rule does not fire on the + actual breach. The gate has a gap the law overlooked. And the archive's seal is not quite what it + appears. All three must be closed before the intruder escapes with the census rolls. + +objective: + - >- + **The Praetorian Guard awake**: Falco fires an alert every time an unauthorized process reads the + census archive, with live alerts streaming into the Falcosidekick UI + - >- + **The gate closed**: the intruder is denied re-admission โ€” the policy that kept privileged + containers out now covers every path to unchecked host power + - >- + **The archive sealed**: the census-archive secret is inaccessible to any workload that does not + bear the Archivist role + - >- + **The empire-wide laws holding**: all intermediate-level checks still green across every province + +what_you_learn: + - >- + How [Falco rules](https://falco.org/docs/reference/rules/) are structured: conditions, syscall + events, and kernel-level fields; and how to write a detection rule that captures a specific runtime + behaviour + - >- + Why `privileged: false` is not the same as "safe": how + [Linux capabilities](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container) + grant containers host-level access without the privileged flag + - >- + How to use [`spec.variables`](https://kyverno.io/docs/policy-types/validating-policy/) in a + `ValidatingPolicy` to define reusable expressions and keep validation logic readable + - >- + How pod volumes reference secrets โ€” and why a volume's name and the secret it actually mounts are + two different fields in the pod spec + - >- + How [Falcosidekick](https://github.com/falcosecurity/falcosidekick) aggregates live Falco alerts + and how to use its UI to watch a runtime incident unfold in real time + +architecture: + - >- + The estate inherits the full intermediate topology: four province namespaces (`gallia`, `hispania`, + `britannia`, `aegyptus`) and one infra namespace (`castra`), each labelled as before. Alongside the + Kyverno stack and Policy Reporter, the cluster now runs **Falco** (eBPF-based, as a DaemonSet) and + **Falcosidekick** with its UI at port + **30111**. At startup, the intruder pod `speculator` is already running in `gallia`, quietly reading + the census archive: imperial rolls that only workloads bearing the `republic.rome/role: archivist` + label are permitted to access. + - >- + Your working directory is the challenge root. `manifests/secrets/` and `manifests/workloads/` are + already in place โ€” they define the estate and the intruder, and need no changes. Everything else is + yours to investigate and fix. + +toolbox: + - name: kubectl + url: "https://kubernetes.io/docs/reference/kubectl/" + description: Apply and inspect cluster resources, check pod status and security contexts + - name: k9s + url: "https://k9scli.io/" + description: Explore cluster resources, pod logs, and policy reports in a terminal UI + - name: kyverno + url: "https://kyverno.io/docs/kyverno-cli/" + description: Test a policy against a resource locally before applying it to the cluster + +services: + - name: Falcosidekick UI + port: 30111 + description: Live stream of Falco alerts, your primary instrument for watching the intruder get caught in real time + - name: Policy Reporter + port: 30110 + description: Audit the policy estate. Check admission violations and confirm the intermediate-level checks are still green + +how_to_play: + - id: survey + title: "Survey the Scene" + content: | + When your Codespace opens, the intruder is already running. Open the **Falcosidekick UI** at + the forwarded port **30111**. It should be streaming alerts about census archive reads, + but it is silent. That silence is your first clue that something is wrong with the Guard. + + Start by getting oriented: + + ```bash + # Can you find the intruder? + kubectl get pods -A + + # Read the Falco rule that should be firing + cat falco-rules.yaml + + # Which policies are in force? + kubectl get validatingpolicies + kubectl get namespacedvalidatingpolicies -A + ``` + + Open **Policy Reporter** at port **30110** as well. The intermediate estate should look clean โ€” + the intruder left no trace at admission. That is part of the problem. + + - id: act1 + title: "Act 1: Wake the Praetorian Guard" + content: | + The Falco rule in `falco-rules.yaml` has a defect โ€” find the break, fix it, and run `make apply`; + alerts streaming into the **Falcosidekick UI at port 30111** are your signal. The + [Falco condition fields reference](https://falco.org/docs/reference/rules/supported-fields/) + documents every available field. + + - id: act2 + title: "Act 2: Close the Gate" + content: | + The intruder passed admission โ€” find the policy gap that let it through, close it, and run + `make apply`; re-admission denied and the **Falcosidekick UI** going quiet confirm Act 2 is done. + The existing policy already uses [`spec.variables`](https://kyverno.io/docs/policy-types/validating-policy/) + to share expressions across validations โ€” a pattern worth exploring. + + - id: act3 + title: "Act 3: Seal the Archive" + content: | + Open `manifests/policies/` โ€” something is missing; the other policies show the structure, write + what's needed, then run `make apply`. + + **Going further:** even with the archive sealed at admission, any workload admitted with the + Archivist role can read the secret. Kubernetes RBAC can restrict which service accounts may + `get` a secret at the API level, a complementary layer that admission control alone cannot + provide. + +helpful_links: + - title: Falco Rules Reference + url: "https://falco.org/docs/reference/rules/" + description: "The anatomy of a Falco rule: condition, output, priority, tags, and how rules are evaluated" + - title: Falco Condition Fields + url: "https://falco.org/docs/reference/rules/supported-fields/" + description: "Every field available in Falco rule conditions: syscall events, file descriptors, process info, and Kubernetes metadata" + - title: Linux Capabilities in Kubernetes + url: "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container" + description: How Linux capabilities work and how to configure or restrict them in a pod's security context + - title: Kyverno ValidatingPolicy + url: "https://kyverno.io/docs/policy-types/validating-policy/" + description: Reference docs for ValidatingPolicy, including spec.variables for composing reusable CEL expressions + - title: CEL Validation Expressions + url: "https://kubernetes.io/docs/reference/using-api/cel/" + description: "How CEL expressions work in Kubernetes admission: operators, optional chaining, and collection functions" + - title: Falcosidekick + url: "https://github.com/falcosecurity/falcosidekick" + description: The Falco alert aggregator that routes Falco events to sinks including the Falcosidekick UI diff --git a/adventures/05-lex-imperfecta/expert/Makefile b/adventures/05-lex-imperfecta/expert/Makefile new file mode 100644 index 0000000..0ea6e00 --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/Makefile @@ -0,0 +1,64 @@ +.PHONY: help apply verify + +help: + @echo "Lex Imperfecta - Expert: Available Commands:" + @echo " make apply Reload Falco rules, reset workloads, re-apply policies and exceptions" + @echo " make verify Run the verification script" + +apply: + @echo "Reloading Falco rules..." + @helm upgrade falco falcosecurity/falco \ + --namespace falco \ + --reuse-values \ + --set-file 'customRules.praetorian-guard\.yaml=./falco-rules.yaml' \ + --wait > /dev/null + @echo " praetorian-guard.yaml" + @echo "" + @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 "Applying Secrets:"; \ + for f in manifests/secrets/*.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 diff --git a/adventures/05-lex-imperfecta/expert/falco-rules.yaml b/adventures/05-lex-imperfecta/expert/falco-rules.yaml new file mode 100644 index 0000000..07b74df --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/falco-rules.yaml @@ -0,0 +1,25 @@ +# โ”€โ”€โ”€ Write your Praetorian Guard rule here โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +- rule: Praetorian Guard - Census Archive Read + desc: > + A process has opened the census archive secret mount. The imperial census + rolls are under constant surveillance. Any read is reported to the + Praetorian Guard immediately. + condition: 'proc.name = "GUARD_NOT_YET_ACTIVE"' + output: > + Census archive accessed by unauthorized hand + (proc=%proc.name pid=%proc.pid user=%user.name + ns=%k8s.ns.name pod=%k8s.pod.name file=%fd.name) + priority: CRITICAL + tags: [census, intrusion, praetorian-guard] + +# โ”€โ”€โ”€ Infrastructure noise suppression โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# "Drop and execute new binary in container" is a default Falco rule that fires +# when a binary is executed from the OverlayFS upper layer. In this environment +# (kind running inside Docker-in-Docker), Falco observes all container processes +# through nested overlay layers and cannot distinguish image-layer binaries from +# runtime-written ones. The rule fires on every pod start for every container โ€” +# including the busybox workloads in this challenge โ€” and would flood the UI. +# This has nothing to do with the challenge; leave this override in place. +- rule: Drop and execute new binary in container + enabled: false +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/adventures/05-lex-imperfecta/expert/manifests/exceptions/aegyptus-legacy-workload.yaml b/adventures/05-lex-imperfecta/expert/manifests/exceptions/aegyptus-legacy-workload.yaml new file mode 100644 index 0000000..cacedb1 --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/exceptions/aegyptus-legacy-workload.yaml @@ -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' diff --git a/adventures/05-lex-imperfecta/expert/manifests/policies/aegyptus-require-scribe-role.yaml b/adventures/05-lex-imperfecta/expert/manifests/policies/aegyptus-require-scribe-role.yaml new file mode 100644 index 0000000..1344f94 --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/policies/aegyptus-require-scribe-role.yaml @@ -0,0 +1,31 @@ +apiVersion: policies.kyverno.io/v1 +kind: NamespacedValidatingPolicy +metadata: + name: aegyptus-require-scribe-role + namespace: aegyptus + annotations: + policies.kyverno.io/title: Aegyptus โ€” Require Scribe Role + policies.kyverno.io/category: Best Practices + policies.kyverno.io/severity: medium + policies.kyverno.io/subject: Pod, Label + policies.kyverno.io/description: >- + Aegyptus is the Republic's centre of scholarship โ€” only scribe workloads + are permitted to operate here. All pods must carry the + 'republic.rome/role: scribe' label. This is a provincial law, enacted by + the magistrate of Aegyptus, and its jurisdiction ends at Aegyptus's borders. +spec: + validationActions: + - Deny + - Audit + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["pods"] + validations: + - expression: >- + has(object.metadata.labels) && + 'republic.rome/role' in object.metadata.labels && + object.metadata.labels['republic.rome/role'] == 'scribe' + message: "Aegyptus permits only scribe workloads. Pods must carry the 'republic.rome/role: scribe' label to be admitted." diff --git a/adventures/05-lex-imperfecta/expert/manifests/policies/no-privileged-containers.yaml b/adventures/05-lex-imperfecta/expert/manifests/policies/no-privileged-containers.yaml new file mode 100644 index 0000000..f1b050e --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/policies/no-privileged-containers.yaml @@ -0,0 +1,39 @@ +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy +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 across all provinces. A container need + not run as privileged to wield host-level power โ€” Linux capabilities offer + a second path to elevated access that the Senate's law must also cover. + This law applies empire-wide to all provinces; the infra realm is exempt. +spec: + validationActions: + - Deny + - Audit + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["pods"] + namespaceSelector: + matchLabels: + republic.rome/realm: province + variables: + - name: allContainers + expression: >- + object.spec.?containers.orValue([]) + + object.spec.?initContainers.orValue([]) + + object.spec.?ephemeralContainers.orValue([]) + validations: + - expression: >- + variables.allContainers.all(c, + !c.?securityContext.?privileged.orValue(false) + ) + message: "The Senate forbids unchecked power: privileged containers are not permitted in the provinces." diff --git a/adventures/05-lex-imperfecta/expert/manifests/policies/require-census.yaml b/adventures/05-lex-imperfecta/expert/manifests/policies/require-census.yaml new file mode 100644 index 0000000..e6e2d62 --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/policies/require-census.yaml @@ -0,0 +1,35 @@ +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy +metadata: + name: require-census + annotations: + policies.kyverno.io/title: Citizen Census + 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 and their province. These are the + foundational registration requirements of the Republic, applied empire-wide + to all provinces. Unregistered or misdeclared workloads cannot be admitted. + Recognised legacy workloads may be granted a formal exception. +spec: + validationActions: + - Deny + - Audit + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["pods"] + namespaceSelector: + matchLabels: + republic.rome/realm: province + validations: + - expression: "has(object.metadata.labels) && 'republic.rome/gens' in object.metadata.labels" + message: "All citizens must declare their gens. Unregistered workloads are not permitted in the Republic." + - expression: >- + has(object.metadata.labels) && + 'republic.rome/province' in object.metadata.labels && + object.metadata.labels['republic.rome/province'] == object.metadata.namespace + message: "A workload's declared province must match the namespace it is deployed in. Citizens cannot claim citizenship in a province they do not inhabit." diff --git a/adventures/05-lex-imperfecta/expert/manifests/secrets/census-archive.yaml b/adventures/05-lex-imperfecta/expert/manifests/secrets/census-archive.yaml new file mode 100644 index 0000000..c71bbbe --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/secrets/census-archive.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Secret +metadata: + name: census-archive + namespace: gallia +type: Opaque +stringData: + registrations: | + CIVITAS ROMANA โ€” GALLIA CENSUS REGISTER + ANNO DCCCLXIX AB URBE CONDITA + + FAMILY IULIA โ€” Marcus Iulius (head), Lucia Iulia, Gaius Iulius (minor) + FAMILY VALERIA โ€” Quintus Valerius (head), Servilia Valeria + FAMILY CLAUDIA โ€” Publius Claudius (head), Valeria Claudia, twins Lucius et Titus + + REGISTERED RESIDENT AUXILIARIES: 17 + TOTAL CIVITAS COUNT: 2,841 + + IMPERIAL USE ONLY โ€” TABULARIUM ROME โ€” DO NOT DISTRIBUTE diff --git a/adventures/05-lex-imperfecta/expert/manifests/workloads/aegyptus-legacy-scribe.yaml b/adventures/05-lex-imperfecta/expert/manifests/workloads/aegyptus-legacy-scribe.yaml new file mode 100644 index 0000000..c64ccff --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/workloads/aegyptus-legacy-scribe.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: legacy-scribe + namespace: aegyptus + labels: + # No republic.rome/gens โ€” predates the requirement. Opts in to the legacy + # exemption (PolicyException aegyptus-legacy-workload) via this explicit marker. + republic.rome/legacy: "true" + republic.rome/province: aegyptus + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false diff --git a/adventures/05-lex-imperfecta/expert/manifests/workloads/castra-praetorian-monitor.yaml b/adventures/05-lex-imperfecta/expert/manifests/workloads/castra-praetorian-monitor.yaml new file mode 100644 index 0000000..937ee85 --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/workloads/castra-praetorian-monitor.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: praetorian-monitor + namespace: castra + labels: + republic.rome/gens: praetoria +spec: + containers: + - name: monitor + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: true diff --git a/adventures/05-lex-imperfecta/expert/manifests/workloads/citizens.yaml b/adventures/05-lex-imperfecta/expert/manifests/workloads/citizens.yaml new file mode 100644 index 0000000..bdf9893 --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/workloads/citizens.yaml @@ -0,0 +1,64 @@ +apiVersion: v1 +kind: Pod +metadata: + name: citizen + namespace: gallia + labels: + republic.rome/gens: iulia + republic.rome/province: gallia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false +--- +apiVersion: v1 +kind: Pod +metadata: + name: citizen + namespace: hispania + labels: + republic.rome/gens: cornelia + republic.rome/province: hispania +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false +--- +apiVersion: v1 +kind: Pod +metadata: + name: citizen + namespace: britannia + labels: + republic.rome/gens: claudia + republic.rome/province: britannia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false +--- +apiVersion: v1 +kind: Pod +metadata: + name: citizen + namespace: aegyptus + labels: + republic.rome/gens: aemilia + republic.rome/province: aegyptus + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "infinity"] + securityContext: + privileged: false diff --git a/adventures/05-lex-imperfecta/expert/manifests/workloads/speculator.yaml b/adventures/05-lex-imperfecta/expert/manifests/workloads/speculator.yaml new file mode 100644 index 0000000..ac9d628 --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/manifests/workloads/speculator.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Pod +metadata: + name: speculator + namespace: gallia + labels: + republic.rome/gens: brutia + republic.rome/province: gallia +spec: + containers: + - name: auxiliary + image: busybox:stable + command: + - sh + - -c + - | + while true; do + cat /run/secrets/census-archive/registrations 2>/dev/null || true + nslookup enemy.invalid 2>/dev/null || true + sleep 30 + done + securityContext: + privileged: false + capabilities: + add: ["SYS_ADMIN"] + volumeMounts: + - name: archive + mountPath: /run/secrets/census-archive + readOnly: true + volumes: + - name: archive + secret: + secretName: census-archive diff --git a/adventures/05-lex-imperfecta/expert/verify.sh b/adventures/05-lex-imperfecta/expert/verify.sh new file mode 100755 index 0000000..f1c9321 --- /dev/null +++ b/adventures/05-lex-imperfecta/expert/verify.sh @@ -0,0 +1,372 @@ +#!/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" + +set_tracking_context "lex-imperfecta" "expert" "05" "06" "2026" + +OBJECTIVE="By the end of this level, you should have: + +- The Praetorian Guard awake: Falco fires an alert every time an unauthorized process reads the census archive, with live alerts streaming into the Falcosidekick UI +- The gate closed: the intruder is denied re-admission โ€” the policy that kept privileged containers out now covers every path to unchecked host power +- The archive sealed: the census-archive secret is inaccessible to any workload that does not bear the Archivist role +- The empire-wide laws holding: all intermediate-level checks still green across every province" + +DOCS_URL="https://offon.dev/adventures/lex-imperfecta/levels/expert" + +print_header \ + 'Challenge 05: Lex Imperfecta' \ + 'Level 03: Quis Custodiet' \ + 'Verification' + +# Init test counters +TESTS_PASSED=0 +TESTS_FAILED=0 +FAILED_CHECKS=() + +check_prerequisites kubectl jq + +print_sub_header "Running verification checks..." + +# ============================================================================= +# Objective 1: Empire-wide laws still hold (regression from intermediate) +# ============================================================================= +print_new_line +print_sub_header "1. Checking the empire-wide laws still hold..." + +check_admission_blocked \ + "Workload with no gens in a province" \ + "Every citizen counted in the census must declare something about themselves โ€” does this one?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-no-gens + namespace: gallia + labels: + republic.rome/province: gallia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_blocked \ + "Workload whose declared province does not match its namespace" \ + "The census cross-checks where a workload claims to belong against where it actually runs." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-mismatch + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: hispania +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_blocked \ + "Privileged workload in a province (no exception)" \ + "What does the Senate forbid in the provinces, no matter who is asking?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-privileged-no-exception + namespace: britannia + labels: + republic.rome/gens: verus + republic.rome/province: britannia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: true +EOF + +check_admission_allowed \ + "Privileged workload in the infra realm" \ + "The infra realm lies outside the provinces' jurisdiction โ€” empire-wide laws should not reach into it." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-privileged-infra + namespace: castra +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: true +EOF + +check_admission_blocked \ + "Non-scribe workload in Aegyptus" \ + "Aegyptus admits only one kind of workload โ€” is its local law still in force there?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-aegyptus-non-scribe + namespace: aegyptus + labels: + republic.rome/gens: verus + republic.rome/province: aegyptus +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +check_admission_allowed \ + "Grandfathered legacy workload in Aegyptus" \ + "Aegyptus's grandfathered scribes hold a Senate exception โ€” is it honoured where it belongs?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-aegyptus-legacy + namespace: aegyptus + labels: + republic.rome/legacy: "true" + republic.rome/province: aegyptus + republic.rome/role: scribe +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +# ============================================================================= +# Objective 2: The gate is closed โ€” the policy covers capabilities, not just privileged +# ============================================================================= +print_new_line +print_sub_header "2. Checking that the policy covers dangerous capabilities..." + +check_admission_blocked \ + "Pod with a dangerous capability in a province" \ + "Look at what the intruder is carrying โ€” and what the policy currently checks." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-cap-sysadmin + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: gallia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false + capabilities: + add: ["SYS_ADMIN"] +EOF + +check_admission_blocked \ + "Pod with another dangerous capability in a province" \ + "Does the policy cover all the ways a container can acquire elevated power?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-cap-netadmin + namespace: britannia + labels: + republic.rome/gens: verus + republic.rome/province: britannia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false + capabilities: + add: ["NET_ADMIN"] +EOF + +check_admission_allowed \ + "Pod with no dangerous capabilities in a province" \ + "A workload with no elevated capabilities should still be admitted โ€” make sure the policy is not over-reaching." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-cap-none + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: gallia +spec: + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false +EOF + +# ============================================================================= +# Objective 3: The archive is sealed โ€” census-archive mount is restricted +# ============================================================================= +print_new_line +print_sub_header "3. Checking that the census archive is sealed..." + +check_admission_blocked \ + "Pod mounting census-archive without the Archivist role" \ + "What distinguishes a workload that should have access from one that should not?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-mount-no-role + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: gallia +spec: + volumes: + - name: archive + secret: + secretName: census-archive + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false + volumeMounts: + - name: archive + mountPath: /run/secrets/census-archive +EOF + +check_admission_allowed \ + "Pod mounting census-archive with the Archivist role" \ + "A workload bearing the Archivist seal should be permitted to access the census rolls." <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-mount-archivist + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: gallia + republic.rome/role: archivist +spec: + volumes: + - name: archive + secret: + secretName: census-archive + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false + volumeMounts: + - name: archive + mountPath: /run/secrets/census-archive +EOF + +check_admission_blocked \ + "Pod mounting census-archive under a different volume name" \ + "A volume has a name, and it has a reference to what it actually contains โ€” which one does your policy inspect?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: verify-census-mount-renamed + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: gallia +spec: + volumes: + - name: definitely-not-census + secret: + secretName: census-archive + containers: + - name: app + image: busybox:stable + command: ["sleep", "1"] + securityContext: + privileged: false + volumeMounts: + - name: definitely-not-census + mountPath: /run/secrets/census-archive +EOF + +# ============================================================================= +# Objective 4: The Praetorian Guard is awake โ€” Falco rule is in place +# (Phase 2: Falco alert verification via Falcosidekick events API) +# ============================================================================= +print_new_line +print_sub_header "4. Checking that the Praetorian Guard is awake (Falco)..." + +check_falco_alert \ + "census archive read" \ + "Praetorian Guard - Census Archive Read" \ + "The guard can only report what it has been told to watch โ€” does the rule cover the right syscall and the right path?" <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: falco-tripwire + namespace: gallia + labels: + republic.rome/gens: verus + republic.rome/province: gallia + republic.rome/role: archivist +spec: + restartPolicy: Never + containers: + - name: tripwire + image: busybox:stable + command: ["cat", "/run/secrets/census-archive/registrations"] + securityContext: + privileged: false + volumeMounts: + - name: archive + mountPath: /run/secrets/census-archive + volumes: + - name: archive + secret: + secretName: census-archive +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_verification_completed "failed" "$failed_checks_json" + print_verification_summary "lex-imperfecta" "$DOCS_URL" "$OBJECTIVE" + exit 1 +fi + +track_verification_completed "success" "$failed_checks_json" + +print_header "Test Results Summary" +print_success "โœ… PASSED: All $TESTS_PASSED verification checks passed!" +print_new_line + +check_submission_readiness "05-lex-imperfecta" "expert" diff --git a/lib/falco/init.sh b/lib/falco/init.sh new file mode 100755 index 0000000..70c70b1 --- /dev/null +++ b/lib/falco/init.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -e + +help() { + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " --help Display this help message" + echo " --falco-version Falco Helm chart version to install (required)" + echo " --falcosidekick-version Falcosidekick Helm chart version to install (required)" +} + +falco_version="" +falcosidekick_version="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --help) + help + exit 0 + ;; + --falco-version) + if [[ -z "${2-}" ]]; then + echo "Error: --falco-version requires a value" >&2 + exit 1 + fi + falco_version="$2" + shift 2 + ;; + --falcosidekick-version) + if [[ -z "${2-}" ]]; then + echo "Error: --falcosidekick-version requires a value" >&2 + exit 1 + fi + falcosidekick_version="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$falco_version" ]]; then + echo "Error: --falco-version is required" >&2 + exit 1 +fi + +if [[ -z "$falcosidekick_version" ]]; then + echo "Error: --falcosidekick-version is required" >&2 + exit 1 +fi + +helm repo add falcosecurity https://falcosecurity.github.io/charts +helm repo update + +# Install Falcosidekick first so the service endpoint exists when Falco starts. +# The webui (with its Redis backend) is exposed as a NodePort and patched to 30111. +echo "โœจ Installing Falcosidekick + UI" +helm install falcosidekick falcosecurity/falcosidekick \ + --namespace falco \ + --create-namespace \ + --version "$falcosidekick_version" \ + --set webui.enabled=true \ + --set webui.service.type=NodePort \ + --set webui.disableauth=true \ + --wait + +echo "โœจ Pinning Falcosidekick UI to NodePort 30111" +kubectl patch svc falcosidekick-ui -n falco \ + --type='json' \ + -p='[{"op":"replace","path":"/spec/ports/0/nodePort","value":30111}]' + +# Install Falco with the modern eBPF driver (no kernel headers required). +# JSON output is routed to Falcosidekick via HTTP so alerts appear in the UI. +echo "โœจ Installing Falco (modern eBPF)" +helm install falco falcosecurity/falco \ + --namespace falco \ + --version "$falco_version" \ + --set driver.kind=modern_ebpf \ + --set falco.json_output=true \ + --set falco.http_output.enabled=true \ + --set falco.http_output.url=http://falcosidekick.falco.svc.cluster.local:2801 \ + --wait + +echo "โœจ Waiting for Falco DaemonSet to be ready" +kubectl rollout status daemonset/falco -n falco --timeout=300s + +echo "โœ… Falco is ready (Falcosidekick UI: http://localhost:30111)" diff --git a/lib/kubernetes/config.yaml b/lib/kubernetes/config.yaml old mode 100644 new mode 100755 index 4878890..66847f2 --- a/lib/kubernetes/config.yaml +++ b/lib/kubernetes/config.yaml @@ -43,3 +43,7 @@ nodes: - hostPort: 30110 containerPort: 30110 protocol: TCP + # Falcosidekick UI + - hostPort: 30111 + containerPort: 30111 + protocol: TCP diff --git a/lib/scripts/falco.sh b/lib/scripts/falco.sh new file mode 100644 index 0000000..4e148ef --- /dev/null +++ b/lib/scripts/falco.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +# Deploy a manifest from stdin, wait for Falco to process the resulting +# syscall events, then assert the named rule fired in Falco's output. +# Cleans up the deployed manifest afterwards. +# +# Usage: check_falco_alert "display name" "Exact Falco Rule Name" "hint" <<'EOF' +# +# EOF +check_falco_alert() { + local display_name=$1 + local rule_name=$2 + local hint=$3 + local wait_seconds=${4:-10} + + local manifest + manifest=$(cat) + + print_test_section "Checking Falco detects: $display_name..." + + echo "$manifest" | kubectl apply -f - >/dev/null 2>&1 + + sleep "$wait_seconds" + + local found=false + if kubectl logs -n falco -l app.kubernetes.io/name=falco -c falco --since=60s 2>/dev/null \ + | grep -qF "\"rule\":\"${rule_name}\""; then + found=true + fi + + echo "$manifest" | kubectl delete -f - --ignore-not-found >/dev/null 2>&1 & + + if [[ "$found" == "true" ]]; then + print_success_indent "Falco rule '${rule_name}' fired correctly" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + print_error_indent "Falco rule '${rule_name}' did not fire" + print_hint "$hint" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("falco_alert:${display_name}") + fi +} diff --git a/lib/scripts/loader.sh b/lib/scripts/loader.sh index 8deb3ea..2e18cf6 100644 --- a/lib/scripts/loader.sh +++ b/lib/scripts/loader.sh @@ -21,6 +21,7 @@ source "$LIB_DIR/github.sh" source "$LIB_DIR/submission.sh" source "$LIB_DIR/tracker.sh" source "$LIB_DIR/jaeger.sh" +source "$LIB_DIR/falco.sh" # Set up cleanup trap for port-forwards trap cleanup_port_forwards EXIT INT TERM