kubestronaut-sim/cks/solve.py
vumas3r f3d0e536b6 Exam simulator for CKA, CKAD and CKS with per-task hosts and a remote desktop
kubestronaut-sim runs each exam task on a host of its own - a kind (kubeadm)
cluster in rootless podman, or a QEMU/KVM Ubuntu VM for the CKS tasks that
need their own kernel (Falco, AppArmor) - and puts a browser desktop in front
of them, the way killer.sh and sailor.sh do.

- ksim, sim/: the engine (sessions, drills, study mode, grading, coverage,
  audit), host and VM lifecycle, the desktop
- images/: exam host images (common, CKS variant with its tools, VM guest)
- cka/ (131 tasks), ckad/ (105), cks/ (110): task banks, curricula with
  study notes, reference solutions and scripted solutions

Every domain of every track has at least 17 pool tasks. `./ksim audit` proves
a task three ways: it seeds cleanly on its own host, scores nothing before
anyone has worked on it, and reaches full marks when its scripted solution is
run on the host as `candidate`. All 346 tasks pass (CKA audited 2026-10-13,
CKAD and CKS 2026-10-12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPd3GGWyxTgAkL6a4gAJz
2026-10-13 16:29:33 -06:00

1267 lines
54 KiB
Python

"""Scripted solutions: what a candidate would type on the task's host, as a script.
`./ksim audit` runs a task's script as `candidate` in an interactive shell on its host (so the
host's aliases apply) after grading the task untouched, and grades it again: full marks prove the
task can be solved as its text describes, on a cluster of its own. %{param}, {WS} and {CP}.. are
substituted as in the task. A task without a script is only audited for a clean seed.
The audit grades the instant a script ends, so the scripts wait where a person would simply look
again: for a static Pod to come back, for a NetworkPolicy to reach the node.
SOLVE: task id -> bash
"""
# Helpers for the scripts below.
H = r"""
set +H
M=/etc/kubernetes/manifests
# medit FILE 'YQ EXPRESSION': edit a static Pod manifest through a copy outside the manifests directory
medit() { sudo cp $M/$1 /tmp/$1 && sudo yq -i "$2" /tmp/$1 && sudo cp /tmp/$1 $M/$1; }
# upwith PROCESS PATTERN: wait until PROCESS runs with PATTERN on its command line and the API answers
upwith() {
local i
for i in $(seq 1 80); do
pgrep -af "^$1 " | grep -q -- "$2" && kubectl get --raw=/readyz >/dev/null 2>&1 && return 0; sleep 3
done
echo "$1 did not come back with $2" >&2; return 1
}
# restart_api: restart the API server (it reads files such as an audit policy only when it starts) by
# moving its manifest out of the directory and back, and wait until the new one answers
restart_api() {
local old i p; old=$(pgrep -o -x kube-apiserver)
sudo mv $M/kube-apiserver.yaml /tmp/ka.yaml
for i in $(seq 1 40); do pgrep -x kube-apiserver >/dev/null || break; sleep 2; done
sudo mv /tmp/ka.yaml $M/kube-apiserver.yaml
for i in $(seq 1 80); do
p=$(pgrep -o -x kube-apiserver); [ -n "$p" ] && [ "$p" != "$old" ] && kubectl get --raw=/readyz >/dev/null 2>&1 && return 0; sleep 3
done; return 1
}
# setflag FILE NAME VALUE: set --NAME=VALUE in a static Pod manifest, replacing the flag if it is there
setflag() { medit "$1" "(.spec.containers[0].command) |= (map(select(test(\"^--$2=\") | not)) + [\"--$2=$3\"])"; }
"""
SOLVE = {
# ---- 1-9: the first tasks of the track (questions.py)
1: r"""
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny, namespace: %{ns}}
spec: {podSelector: {}, policyTypes: [Ingress, Egress]}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-dns, namespace: %{ns}}
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}}]
ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: web-to-db, namespace: %{ns}}
spec:
podSelector: {matchLabels: {app: web}}
policyTypes: [Egress]
egress:
- to: [{podSelector: {matchLabels: {app: db}}}]
ports: [{port: 5432}]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: db-from-web, namespace: %{ns}}
spec:
podSelector: {matchLabels: {app: db}}
policyTypes: [Ingress]
ingress:
- from: [{podSelector: {matchLabels: {app: web}}}]
ports: [{port: 5432}]
EOF
""",
2: H + r"""
(sudo grep -- '--profiling' $M/kube-apiserver.yaml || echo absent) > {WS}/q2/before.txt
setflag kube-apiserver.yaml profiling false
upwith kube-apiserver '--profiling=false'
echo false > {WS}/q2/after.txt
sleep 20
""",
3: r"""
kubectl delete clusterrolebinding %{sa}-god
kubectl -n %{ns} create role %{sa}-view --verb=get,list,watch --resource=pods,pods/log
kubectl -n %{ns} create rolebinding %{sa}-view --role=%{sa}-view --serviceaccount=%{ns}:%{sa}
""",
4: r"""
kubectl -n %{ns} patch sa default -p '{"automountServiceAccountToken": false}'
kubectl -n %{ns} create sa %{dep}-sa
kubectl -n %{ns} set serviceaccount deploy %{dep} %{dep}-sa
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
sleep 35
""",
5: r"""
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata: {name: %{pod}, namespace: %{ns}}
spec:
securityContext:
seccompProfile: {type: RuntimeDefault}
runAsUser: 101
runAsNonRoot: true
volumes:
- {name: cache, emptyDir: {}}
- {name: run, emptyDir: {}}
containers:
- name: web
image: nginx:1.27
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL], add: [NET_BIND_SERVICE, CHOWN]}
volumeMounts:
- {name: cache, mountPath: /var/cache/nginx}
- {name: run, mountPath: /var/run}
EOF
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=180s
""",
6: r"""
kubectl label ns %{ns} pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=latest pod-security.kubernetes.io/warn=restricted pod-security.kubernetes.io/audit=restricted --overwrite
kubectl -n %{ns} patch deploy %{dep} --type=json -p '[
{"op": "add", "path": "/spec/template/spec/securityContext", "value": {"runAsNonRoot": true, "runAsUser": 101, "seccompProfile": {"type": "RuntimeDefault"}}},
{"op": "replace", "path": "/spec/template/spec/containers/0/securityContext", "value": {"allowPrivilegeEscalation": false, "capabilities": {"drop": ["ALL"]}}}]'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
""",
7: r"""
kubectl -n %{ns} create sa %{pod}-sa
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata: {name: %{pod}, namespace: %{ns}}
spec:
serviceAccountName: %{pod}-sa
automountServiceAccountToken: false
hostNetwork: false
hostPID: false
hostIPC: false
containers:
- name: c
image: busybox:1.36
command: [sleep, "86400"]
resources:
requests: {cpu: 50m, memory: 64Mi}
limits: {cpu: 200m, memory: 128Mi}
EOF
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=180s
""",
8: r"""
cat <<EOF | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: allowed-registry}
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: [CREATE, UPDATE]
resources: ["pods"]
validations:
- expression: "object.spec.containers.all(c, c.image.startsWith('%{reg}/'))"
message: "images must come from %{reg}/"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: allowed-registry}
spec:
policyName: allowed-registry
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels: {registry-policy: enforced}
EOF
sleep 20
kubectl -n %{ns} run vap-probe --image=nginx:1.27 > {WS}/q8/denied.txt 2>&1
""",
9: H + r"""
sudo mkdir -p /var/log/kubernetes
cat <<EOF | sudo tee /etc/kubernetes/audit-policy.yaml >/dev/null
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- RequestReceived
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
- level: Metadata
EOF
medit kube-apiserver.yaml '.spec.containers[0].command += ["--audit-policy-file=/etc/kubernetes/audit-policy.yaml", "--audit-log-path=/var/log/kubernetes/audit.log", "--audit-log-maxage=7", "--audit-log-maxbackup=5"] | .spec.containers[0].volumeMounts += [{"name": "audit-policy", "mountPath": "/etc/kubernetes/audit-policy.yaml", "readOnly": true}, {"name": "audit-log", "mountPath": "/var/log/kubernetes"}] | .spec.volumes += [{"name": "audit-policy", "hostPath": {"path": "/etc/kubernetes/audit-policy.yaml", "type": "File"}}, {"name": "audit-log", "hostPath": {"path": "/var/log/kubernetes", "type": "DirectoryOrCreate"}}]'
upwith kube-apiserver '--audit-policy-file='
kubectl get secrets -A >/dev/null
sleep 3
sudo grep secrets /var/log/kubernetes/audit.log | tail -1 > {WS}/q9/audit-line.json
""",
# ---- 10-24: Cluster Setup (bank_setup.py)
10: r"""
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: db-access, namespace: %{ns}}
spec:
podSelector: {matchLabels: {app: db}}
policyTypes: [Ingress, Egress]
ingress:
- from:
- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: %{ns}-app}}
podSelector: {matchLabels: {role: backend}}
ports: [{protocol: TCP, port: 80}]
egress:
- ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}]
EOF
sleep 5
""",
11: r"""
API=$(kubectl get endpointslice -l kubernetes.io/service-name=kubernetes -o jsonpath='{.items[0].endpoints[0].addresses[0]}')
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: runner-egress, namespace: %{ns}}
spec:
podSelector: {matchLabels: {app: runner}}
policyTypes: [Egress]
egress:
- ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}]
- to: [{ipBlock: {cidr: $API/32}}]
ports: [{protocol: TCP, port: 6443}]
EOF
sleep 5
""",
12: r"""
IP=$(cat {WS}/q12/suspect-ip.txt)
POD=$(kubectl -n %{ns} get pod -o jsonpath="{.items[?(@.status.podIP=='$IP')].metadata.name}")
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: quarantine, namespace: %{ns}}
spec:
podSelector: {matchLabels: {quarantine: "true"}}
policyTypes: [Ingress, Egress]
EOF
kubectl -n %{ns} label pod $POD quarantine=true app-
kubectl -n %{ns} rollout status deploy shop --timeout=180s
sleep 5
""",
13: r"""
sudo yq -i '.authentication.anonymous.enabled = false | .authorization.mode = "Webhook"' /var/lib/kubelet/config.yaml
sudo systemctl restart kubelet
sleep 10
sudo kube-bench run --targets node --check 4.2.1,4.2.2 | grep '^\[' > {WS}/q13/after.txt
""",
14: H + r"""
medit etcd.yaml '(.spec.containers[0].command[] | select(. == "--client-cert-auth=false")) = "--client-cert-auth=true" | (.spec.containers[0].command[] | select(. == "--peer-client-cert-auth=false")) = "--peer-client-cert-auth=true"'
sudo useradd -r -s /usr/sbin/nologin etcd
sudo chmod 700 /var/lib/etcd && sudo chown -R etcd:etcd /var/lib/etcd
upwith etcd '--peer-client-cert-auth=true'
""",
15: H + r"""
sudo kube-bench run --targets master | grep -E '^\[FAIL\] 1\.[34]\.' | awk '{print $2}' > {WS}/q15/failed.txt
for c in kube-controller-manager kube-scheduler; do setflag $c.yaml bind-address 127.0.0.1; setflag $c.yaml profiling false; done
upwith kube-controller-manager '--profiling=false'; upwith kube-scheduler '--profiling=false'
sleep 20
""",
16: r"""
kubectl -n %{ns} create secret tls shop-tls --cert={WS}/q16/tls.crt --key={WS}/q16/tls.key
kubectl -n %{ns} create ingress shop --class=nginx --rule="shop.%{dom}.test/*=shop:80,tls=shop-tls"
""",
17: r"""
cd {WS}/q17
openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout tls.key -out tls.crt -subj "/CN=portal.%{dom}.test" -addext "subjectAltName=DNS:portal.%{dom}.test" 2>/dev/null
kubectl -n %{ns} create secret tls portal-tls --cert=tls.crt --key=tls.key --dry-run=client -o yaml | kubectl apply -f -
kubectl -n %{ns} patch ingress portal --type=json -p '[{"op": "replace", "path": "/spec/tls/0/hosts", "value": ["portal.%{dom}.test"]}]'
""",
18: r"""
cd {WS}/q18
for c in cert-1.pem cert-2.pem; do
host=$(openssl x509 -in $c -noout -subject | sed 's/.*CN *= *//')
for k in key-1.pem key-2.pem; do
if [ "$(openssl x509 -in $c -noout -pubkey | sha256sum)" = "$(openssl pkey -in $k -pubout | sha256sum)" ]; then
kubectl -n %{ns} create secret tls ${host%%.*}-tls --cert=$c --key=$k
fi
done
done
kubectl -n %{ns} annotate ingress edge nginx.ingress.kubernetes.io/force-ssl-redirect=true
kubectl -n %{ns} patch ingress edge --type=merge -p '{"spec": {"tls": [{"hosts": ["api.%{dom}.test"], "secretName": "api-tls"}, {"hosts": ["www.%{dom}.test"], "secretName": "www-tls"}]}}'
""",
19: r"""
kubectl -n %{ns} exec web -- wget -qO- http://169.254.169.254/latest/meta-data/iam/security-credentials/node-role > {WS}/q19/leak.txt
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: deny-metadata, namespace: %{ns}}
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- ipBlock: {cidr: 0.0.0.0/0, except: [169.254.169.254/32]}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-metadata, namespace: %{ns}}
spec:
podSelector: {matchLabels: {app: backup}}
policyTypes: [Egress]
egress:
- to:
- ipBlock: {cidr: 169.254.169.254/32}
EOF
sleep 5
""",
20: r"""
ssh -n {W1} 'sudo yq -i ".readOnlyPort = 0 | .authentication.anonymous.enabled = false | .authorization.mode = \"Webhook\"" /var/lib/kubelet/config.yaml && sudo systemctl restart kubelet'
sleep 10
curl -sk -o /dev/null -w '%{http_code}\n' https://{W1}:10250/pods > {WS}/q20/status.txt
""",
21: H + r"""
kubectl -n kube-system get cm kube-proxy -o yaml | sed 's/metricsBindAddress: 0.0.0.0:10249/metricsBindAddress: 127.0.0.1:10249/' | kubectl apply -f -
kubectl -n kube-system rollout restart ds kube-proxy
kubectl -n kube-system rollout status ds kube-proxy --timeout=180s
setflag etcd.yaml listen-metrics-urls http://127.0.0.1:2381
upwith etcd '--listen-metrics-urls=http://127.0.0.1:2381'
sleep 5
""",
22: r"""
cd {WS}/q22/release
sha512sum -c ../SHA512SUMS 2>/dev/null | grep FAILED | cut -d: -f1 > ../tampered.txt
xargs rm -f < ../tampered.txt
""",
23: r"""
cd {WS}/q23
ssh {W1} sudo sha512sum -c - < reference.sha512 2>/dev/null | grep FAILED | cut -d: -f1 > tampered.txt
for f in $(cat tampered.txt); do
scp -q $f {W1}:/tmp/genuine && ssh -n {W1} "sudo install -o root -g root -m 755 /tmp/genuine $f && rm -f /tmp/genuine"
done
""",
24: r"""
sudo sha512sum /proc/$(pgrep -o -x kube-apiserver)/exe | cut -d' ' -f1 > {WS}/q24/apiserver.sha512
if diff -q {WS}/q24/apiserver.sha512 {WS}/q24/reference.sha512 >/dev/null; then echo match; else echo mismatch; fi > {WS}/q24/verdict.txt
""",
# ---- 25-39: Cluster Hardening (bank_harden.py)
25: r"""
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: app-reader, namespace: %{ns}}
rules:
- apiGroups: [""]
resources: [configmaps]
verbs: [get, list, watch]
- apiGroups: [""]
resources: [secrets]
resourceNames: [api-key]
verbs: [get]
EOF
""",
26: r"""
kubectl delete clusterrolebinding dev-team
kubectl -n %{ns}-dev create rolebinding dev-edit --clusterrole=edit --group=dev
kubectl -n %{ns}-prod create rolebinding dev-view --clusterrole=view --group=dev
""",
27: r"""
kubectl -n %{ns} patch role release-manager --type=json -p '[{"op": "replace", "path": "/rules", "value": [
{"apiGroups": ["apps"], "resources": ["deployments"], "verbs": ["get", "list", "watch", "create", "update", "patch"]},
{"apiGroups": [""], "resources": ["pods"], "verbs": ["get", "list", "watch"]}]}]'
""",
28: r"""
for sa in builder deployer viewer auditor ingest reporter; do
[ "$(kubectl -n %{ns} auth can-i get secret/payroll --as=system:serviceaccount:%{ns}:$sa)" = yes ] && echo $sa
done > {WS}/q28/can-read.txt
kubectl delete clusterrolebinding %{ns}-secret-audit
""",
29: r"""
kubectl -n %{ns} create sa vault-client
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata: {name: %{pod}, namespace: %{ns}}
spec:
serviceAccountName: vault-client
automountServiceAccountToken: false
containers:
- name: c
image: busybox:1.36
command: [sleep, "86400"]
volumeMounts:
- {name: vault-token, mountPath: /var/run/secrets/vault, readOnly: true}
volumes:
- name: vault-token
projected:
sources:
- serviceAccountToken: {path: token, audience: vault, expirationSeconds: 600}
EOF
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=120s
""",
30: r"""
kubectl -n %{ns} get secret --field-selector type=kubernetes.io/service-account-token -o name | cut -d/ -f2 > {WS}/q30/legacy.txt
kubectl -n %{ns} delete secret $(cat {WS}/q30/legacy.txt)
kubectl -n %{ns} create token ci --duration=30m > {WS}/q30/ci.token
""",
31: r"""
kubectl -n %{ns} create sa reporter
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: reporter, namespace: %{ns}}
rules:
- {apiGroups: [""], resources: [configmaps], verbs: [get, list, watch]}
- {apiGroups: [""], resources: [events], verbs: [create]}
EOF
kubectl -n %{ns} create rolebinding reporter --role=reporter --serviceaccount=%{ns}:reporter
kubectl -n %{ns} set serviceaccount deploy reporter reporter
kubectl -n %{ns} delete rolebinding default-edit
kubectl -n %{ns} patch sa default -p '{"automountServiceAccountToken": false}'
kubectl -n %{ns} rollout status deploy reporter --timeout=180s
""",
32: r"""
curl -sk https://localhost:6443/api/v1/namespaces/%{ns}/secrets > {WS}/q32/before.json
printf 'monitoring-anon\nlegacy-discovery\nguest-access\n' > {WS}/q32/bindings.txt
kubectl delete clusterrolebinding monitoring-anon legacy-discovery
kubectl -n %{ns} delete rolebinding guest-access
""",
33: H + r"""
sudo mkdir -p /etc/kubernetes/auth
cat <<EOF | sudo tee /etc/kubernetes/auth/authn.yaml >/dev/null
apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
anonymous:
enabled: true
conditions:
- path: /livez
- path: /readyz
- path: /healthz
EOF
medit kube-apiserver.yaml '.spec.containers[0].command += ["--authentication-config=/etc/kubernetes/auth/authn.yaml"] | .spec.containers[0].volumeMounts += [{"name": "authn", "mountPath": "/etc/kubernetes/auth", "readOnly": true}] | .spec.volumes += [{"name": "authn", "hostPath": {"path": "/etc/kubernetes/auth", "type": "DirectoryOrCreate"}}]'
upwith kube-apiserver '--authentication-config='
curl -sk -o /dev/null -w '%{http_code}\n' https://localhost:6443/version > {WS}/q33/version.txt
""",
34: H + r"""
medit kube-apiserver.yaml '(.spec.containers[0].command[] | select(. == "--authorization-mode=AlwaysAllow")) = "--authorization-mode=Node,RBAC" | .spec.containers[0].command += ["--enable-admission-plugins=NodeRestriction"]'
upwith kube-apiserver '--authorization-mode=Node,RBAC'
ssh -n {W1} sudo kubectl --kubeconfig /etc/kubernetes/kubelet.conf label node {CP} audit=failed > {WS}/q34/denied.txt 2>&1
""",
35: H + r"""
medit kube-apiserver.yaml '.spec.containers[0].command |= map(select(test("^--kubernetes-service-node-port=") | not))'
for i in $(seq 1 80); do
pgrep -af '^kube-apiserver ' | grep -q . && ! pgrep -af '^kube-apiserver ' | grep -q -- '--kubernetes-service-node-port' && kubectl get --raw=/readyz >/dev/null 2>&1 && break; sleep 3
done
kubectl delete svc kubernetes
sleep 10
""",
36: r"""
cd {WS}/q36
openssl genrsa -out %{user}.key 2048 2>/dev/null
openssl req -new -key %{user}.key -subj "/CN=%{user}/O=auditors" -out %{user}.csr
cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata: {name: %{user}}
spec:
request: $(base64 -w0 < %{user}.csr)
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 86400
usages: [client auth]
EOF
kubectl certificate approve %{user}
sleep 3
kubectl get csr %{user} -o jsonpath='{.status.certificate}' | base64 -d > %{user}.crt
kubectl -n %{ns} create rolebinding auditors-view --clusterrole=view --group=auditors
""",
37: r"""
V=$(kubectl get node {CP} -o jsonpath='{.status.nodeInfo.kubeletVersion}' | tr -d v)
kubectl drain {W1} --ignore-daemonsets --delete-emptydir-data --force
ssh -n {W1} "sudo apt-get update -qq; sudo apt-mark unhold kubeadm kubelet kubectl >/dev/null; sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq kubeadm=$V-1.1 >/dev/null && sudo kubeadm upgrade node && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq kubelet=$V-1.1 kubectl=$V-1.1 >/dev/null; sudo apt-mark hold kubeadm kubelet kubectl; sudo systemctl daemon-reload; sudo systemctl restart kubelet"
kubectl uncordon {W1}
kubectl -n %{ns} rollout status deploy web --timeout=180s
sleep 15
""",
38: r"""
sudo apt-get update -qq
V=$(apt-cache madison kubeadm | head -1 | awk '{print $3}')
sudo apt-mark unhold kubeadm kubelet kubectl >/dev/null
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq kubeadm=$V >/dev/null
sudo kubeadm upgrade plan | tail -5
sudo kubeadm upgrade apply -y v${V%%-*} | tail -3
for i in $(seq 1 60); do kubectl get --raw=/readyz >/dev/null 2>&1 && break; sleep 3; done
kubectl drain {CP} --ignore-daemonsets --delete-emptydir-data --force
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq kubelet=$V kubectl=$V >/dev/null
sudo apt-mark hold kubeadm kubelet kubectl
sudo systemctl daemon-reload; sudo systemctl restart kubelet
for i in $(seq 1 60); do kubectl get --raw=/readyz >/dev/null 2>&1 && break; sleep 3; done
kubectl uncordon {CP}
sleep 40
""",
39: r"""
T=$(kubectl get node {CP} -o jsonpath='{.status.nodeInfo.kubeletVersion}')
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name} {.status.nodeInfo.kubeletVersion}{"\n"}{end}' | grep -v " $T$" | cut -d' ' -f1 > {WS}/q39/outdated.txt
kubectl cordon $(cat {WS}/q39/outdated.txt)
sudo apt-get update -qq
apt-cache madison kubeadm | head -1 | awk '{print $3}' > {WS}/q39/target.txt
sudo kubeadm upgrade plan > {WS}/q39/plan.txt 2>&1
""",
# ---- 40-55: System Hardening (bank_sys.py)
40: r"""
PID=$(sudo ss -tlnpH | grep ':%{port} ' | sed 's/.*pid=\([0-9]*\).*/\1/')
sudo readlink /proc/$PID/exe > {WS}/q40/binary.txt
UNIT=$(ps -o unit= -p $PID | tr -d ' ')
echo $UNIT > {WS}/q40/unit.txt
sudo systemctl disable --now $UNIT
sudo rm -f /etc/systemd/system/$UNIT $(cat {WS}/q40/binary.txt)
sudo systemctl daemon-reload
""",
41: r"""
PID=$(sudo ss -tlnpH | grep ':21 ' | sed 's/.*pid=\([0-9]*\).*/\1/')
dpkg -S "$(sudo readlink /proc/$PID/exe)" | cut -d: -f1 > {WS}/q41/package.txt
sudo systemctl disable --now vsftpd
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq vsftpd >/dev/null
""",
42: r"""
sudo find /usr/local /opt -xdev -type f -perm -4000 > {WS}/q42/setuid.txt
sudo chmod u-s $(cat {WS}/q42/setuid.txt)
sudo userdel -r tmpadmin 2>/dev/null
""",
43: r"""
printf 'blacklist sctp\nblacklist dccp\ninstall sctp /bin/false\ninstall dccp /bin/false\n' | sudo tee /etc/modprobe.d/disable-protocols.conf >/dev/null
sudo modprobe -n -v sctp > {WS}/q43/sctp.txt 2>&1
""",
44: r"""
ssh -n {W1} "sudo ss -tlnpH | grep ':1338 '" | sed 's/.*(("\([^"]*\)".*/\1/' > {WS}/q44/unit.txt
ssh -n {W1} "sudo sed -i '/^\[metrics\]/,+1d' /etc/containerd/config.toml && sudo systemctl restart containerd"
sleep 10
""",
45: r"""
echo 'deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart kubelet, /usr/local/bin/crictl ps' | sudo tee /etc/sudoers.d/deploy >/dev/null
sudo visudo -c >/dev/null
sudo gpasswd -d intern sudo
""",
46: r"""
ssh -n {W1} "printf 'PermitRootLogin no\nPasswordAuthentication no\nAllowUsers candidate\n' | sudo tee /etc/ssh/sshd_config.d/00-legacy.conf >/dev/null && sudo sshd -t && sudo systemctl reload ssh"
sleep 2
""",
47: r"""
sudo grep -rIl 'client-key-data' /home /tmp /var/tmp /opt /srv 2>/dev/null | grep -v '^/home/candidate/' > {WS}/q47/copies.txt
sudo rm -f $(cat {WS}/q47/copies.txt)
sudo chmod 600 /etc/kubernetes/admin.conf /etc/kubernetes/pki/ca.key
sudo usermod -L -s /usr/sbin/nologin svc-backup
""",
48: r"""
printf 'admin-ui\nmetrics\ncache\n' > {WS}/q48/exposed.txt
kubectl -n %{ns} patch svc admin-ui -p '{"spec":{"type":"ClusterIP"}}'
kubectl -n %{ns} patch svc metrics -p '{"spec":{"type":"ClusterIP"}}'
kubectl -n %{ns} patch svc cache --type=json -p '[{"op":"remove","path":"/spec/externalIPs"}]'
sleep 5
""",
49: r"""
CPIP=$(kubectl get node {CP} -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}')
ssh -n {W1} "sudo iptables -A INPUT -i lo -p tcp --dport 9400 -j ACCEPT && sudo iptables -A INPUT -p tcp --dport 9400 -s $CPIP -j ACCEPT && sudo iptables -A INPUT -p tcp --dport 9400 -j DROP"
""",
50: r"""
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: cluster-only, namespace: %{ns}}
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector: {}
EOF
sleep 5
""",
51: r"""
printf 'gateway\ndebug-agent\n' > {WS}/q51/exposed.txt
kubectl -n %{ns} patch deploy gateway --type=json -p '[{"op":"remove","path":"/spec/template/spec/containers/0/ports/0/hostPort"}]'
kubectl -n %{ns} patch deploy debug-agent --type=json -p '[{"op":"remove","path":"/spec/template/spec/hostNetwork"}]'
kubectl -n %{ns} rollout status deploy gateway --timeout=180s
kubectl -n %{ns} rollout status deploy debug-agent --timeout=180s
sleep 5
""",
52: r"""
scp -q {WS}/q52/deny-chmod.json {W1}:/tmp/
ssh -n {W1} 'sudo mkdir -p /var/lib/kubelet/seccomp/profiles && sudo cp /tmp/deny-chmod.json /var/lib/kubelet/seccomp/profiles/'
kubectl -n %{ns} patch deploy %{dep} -p '{"spec":{"template":{"spec":{"securityContext":{"seccompProfile":{"type":"Localhost","localhostProfile":"profiles/deny-chmod.json"}}}}}}'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
sleep 3
kubectl -n %{ns} exec deploy/%{dep} -- chmod 600 /etc/hostname > {WS}/q52/chmod.txt 2>&1
""",
53: r"""
sudo cat /var/lib/kubelet/seccomp/profiles/no-mkdir.json | ssh {W1} 'sudo mkdir -p /var/lib/kubelet/seccomp/profiles && sudo tee /var/lib/kubelet/seccomp/profiles/no-mkdir.json >/dev/null'
kubectl -n %{ns} get pod %{pod} -o yaml | sed 's#localhostProfile: no-mkdir.json#localhostProfile: profiles/no-mkdir.json#' > /tmp/pod.yaml
kubectl replace --force -f /tmp/pod.yaml
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=120s
""",
54: r"""
cat > /tmp/no-links.json <<EOF
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{"names": ["link", "linkat", "symlink", "symlinkat"], "action": "SCMP_ACT_ERRNO"}
]
}
EOF
scp -q /tmp/no-links.json {W1}:/tmp/
ssh -n {W1} 'sudo mkdir -p /var/lib/kubelet/seccomp/profiles && sudo cp /tmp/no-links.json /var/lib/kubelet/seccomp/profiles/'
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata: {name: %{pod}, namespace: %{ns}}
spec:
containers:
- name: app
image: busybox:1.36
command: [sleep, "86400"]
securityContext:
seccompProfile: {type: Localhost, localhostProfile: profiles/no-links.json}
- name: logger
image: busybox:1.36
command: [sleep, "86400"]
securityContext:
seccompProfile: {type: RuntimeDefault}
EOF
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=120s
""",
55: r"""
ssh -n {W1} 'sudo yq -i ".seccompDefault = true" /var/lib/kubelet/config.yaml && sudo systemctl restart kubelet'
sleep 12
kubectl -n %{ns} run plain --image=busybox:1.36 --command -- sleep 86400
kubectl -n %{ns} wait --for=condition=Ready pod/plain --timeout=120s
kubectl -n %{ns} exec plain -- grep Seccomp: /proc/1/status | tr -dc '0-9' > {WS}/q55/seccomp.txt
""",
# ---- 56-70: Minimize Microservice Vulnerabilities (bank_micro.py)
56: r"""
printf 'agent\ncollector\ntools\n' > {WS}/q56/violators.txt
kubectl label ns %{ns} pod-security.kubernetes.io/enforce=baseline pod-security.kubernetes.io/enforce-version=latest pod-security.kubernetes.io/warn=restricted pod-security.kubernetes.io/audit=restricted
kubectl -n %{ns} delete pod agent collector tools --grace-period=1
""",
57: H + r"""
sudo mkdir -p /etc/kubernetes/psa
cat <<EOF | sudo tee /etc/kubernetes/psa/admission.yaml >/dev/null
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: baseline
enforce-version: latest
audit: restricted
audit-version: latest
warn: restricted
warn-version: latest
exemptions:
usernames: []
runtimeClasses: []
namespaces: [kube-system]
EOF
medit kube-apiserver.yaml '.spec.containers[0].command += ["--admission-control-config-file=/etc/kubernetes/psa/admission.yaml"] | .spec.containers[0].volumeMounts += [{"name": "psa", "mountPath": "/etc/kubernetes/psa", "readOnly": true}] | .spec.volumes += [{"name": "psa", "hostPath": {"path": "/etc/kubernetes/psa", "type": "DirectoryOrCreate"}}]'
upwith kube-apiserver '--admission-control-config-file='
""",
58: r"""
kubectl -n %{ns} patch deploy %{dep} --type=json -p '[
{"op": "add", "path": "/spec/template/spec/securityContext", "value": {"runAsNonRoot": true, "runAsUser": 1000, "seccompProfile": {"type": "RuntimeDefault"}}},
{"op": "replace", "path": "/spec/template/spec/volumes", "value": [{"name": "data", "emptyDir": {}}]},
{"op": "replace", "path": "/spec/template/spec/containers/0/securityContext", "value": {"allowPrivilegeEscalation": false, "capabilities": {"drop": ["ALL"]}}}]'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
sleep 8
""",
59: r"""
for s in a b c; do
n=%{ns}-$s; level=privileged
for l in baseline restricted; do
kubectl label --dry-run=server --overwrite ns $n pod-security.kubernetes.io/enforce=$l 2>&1 | grep -qi warning || level=$l
[ $level = $l ] || break
done
kubectl label ns $n pod-security.kubernetes.io/enforce=$level >/dev/null
echo "$n $level"
done > {WS}/q59/levels.txt
""",
60: H + r"""
sudo mkdir -p /etc/kubernetes/enc
cat <<EOF | sudo tee /etc/kubernetes/enc/enc.yaml >/dev/null
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: [secrets]
providers:
- aescbc:
keys:
- name: key1
secret: $(head -c 32 /dev/urandom | base64)
- identity: {}
EOF
medit kube-apiserver.yaml '.spec.containers[0].command += ["--encryption-provider-config=/etc/kubernetes/enc/enc.yaml"] | .spec.containers[0].volumeMounts += [{"name": "enc", "mountPath": "/etc/kubernetes/enc", "readOnly": true}] | .spec.volumes += [{"name": "enc", "hostPath": {"path": "/etc/kubernetes/enc", "type": "DirectoryOrCreate"}}]'
upwith kube-apiserver '--encryption-provider-config='
kubectl -n %{ns} get secrets -o json | kubectl replace -f -
sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key get /registry/secrets/%{ns}/db-credentials | hexdump -C > {WS}/q60/etcd.txt
""",
61: r"""
E="sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key"
$E get /registry/secrets/%{ns} --prefix --keys-only | grep '/%{name}$' > {WS}/q61/key.txt
$E get "$(cat {WS}/q61/key.txt)" --print-value-only | strings | grep -o 'tok-[A-Z0-9]*' | head -1 > {WS}/q61/token.txt
""",
62: r"""
kubectl -n %{ns} patch deploy %{dep} --type=json -p '[
{"op": "replace", "path": "/spec/template/spec/containers/0/env", "value": [{"name": "SYNC_INTERVAL", "value": "30"}]},
{"op": "add", "path": "/spec/template/spec/volumes", "value": [{"name": "creds", "secret": {"secretName": "api-creds", "defaultMode": 256}}]},
{"op": "add", "path": "/spec/template/spec/containers/0/volumeMounts", "value": [{"name": "creds", "mountPath": "/etc/creds", "readOnly": true}]}]'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
sleep 35
""",
63: r"""
kubectl -n %{ns} create secret generic app-secrets --from-literal=DB_PASSWORD="$(kubectl -n %{ns} get cm app-config -o jsonpath='{.data.DB_PASSWORD}')" --from-literal=API_TOKEN="$(kubectl -n %{ns} get cm app-config -o jsonpath='{.data.API_TOKEN}')"
kubectl -n %{ns} patch cm app-config --type=json -p '[{"op": "remove", "path": "/data/DB_PASSWORD"}, {"op": "remove", "path": "/data/API_TOKEN"}]'
kubectl -n %{ns} patch deploy %{dep} --type=json -p '[{"op": "add", "path": "/spec/template/spec/containers/0/envFrom/-", "value": {"secretRef": {"name": "app-secrets"}}}]'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
sleep 35
""",
64: H + r"""
F=/etc/kubernetes/enc/enc.yaml
K2=$(head -c 32 /dev/urandom | base64)
sudo yq -i "(.resources[0].providers[] | select(has(\"aescbc\")) | .aescbc.keys) |= [{\"name\": \"key2\", \"secret\": \"$K2\"}] + ." $F
restart_api
kubectl get secrets -A -o json | kubectl replace -f - >/dev/null
sudo yq -i '(.resources[0].providers[] | select(has("aescbc")) | .aescbc.keys) |= map(select(.name != "key1"))' $F
restart_api
sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key get /registry/secrets/%{ns}/vault-pass | strings > {WS}/q64/etcd.txt
""",
65: r"""
cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata: {name: gvisor}
handler: runsc
EOF
kubectl -n %{ns} patch deploy %{dep} -p '{"spec":{"template":{"spec":{"runtimeClassName":"gvisor"}}}}'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
sleep 35
kubectl -n %{ns} exec deploy/%{dep} -- dmesg > {WS}/q65/dmesg.txt
""",
66: r"""
kubectl -n tenant-%{t} create quota tenant-quota --hard=pods=5,requests.cpu=1,requests.memory=1Gi,limits.cpu=2,limits.memory=2Gi
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: LimitRange
metadata: {name: tenant-defaults, namespace: tenant-%{t}}
spec:
limits:
- type: Container
defaultRequest: {cpu: 100m, memory: 64Mi}
default: {cpu: 200m, memory: 128Mi}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: tenant-isolation, namespace: tenant-%{t}}
spec:
podSelector: {}
policyTypes: [Ingress]
ingress:
- from: [{podSelector: {}}]
EOF
kubectl -n tenant-%{t} create rolebinding tenant-admins --clusterrole=admin --group=%{t}-admins
sleep 5
""",
67: r"""
kubectl -n %{ns} get deploy -o json | jq -r '.items[] | select(.spec.template.spec as $s | $s.hostPID or $s.hostIPC or $s.hostNetwork or any($s.volumes[]?; .hostPath) or any($s.containers[]; .securityContext.privileged)) | .metadata.name' > {WS}/q67/breakers.txt
kubectl -n %{ns} patch deploy log-shipper --type=json -p '[
{"op": "replace", "path": "/spec/template/spec/volumes/0/hostPath/path", "value": "/var/log"},
{"op": "add", "path": "/spec/template/spec/containers/0/volumeMounts/0/readOnly", "value": true}]'
kubectl -n %{ns} scale deploy metrics-agent ci-runner net-tool --replicas=0
kubectl -n %{ns} rollout status deploy log-shipper --timeout=180s
""",
68: r"""
kubectl label ns %{ns} istio-injection=enabled
kubectl -n %{ns} rollout restart deploy
kubectl -n %{ns} rollout status deploy orders --timeout=240s
kubectl -n %{ns} rollout status deploy payments --timeout=240s
cat <<EOF | kubectl apply -f -
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: {name: default, namespace: %{ns}}
spec:
mtls: {mode: STRICT}
EOF
sleep 40
""",
69: r"""
cat <<EOF | kubectl apply -f -
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: {name: default, namespace: istio-system}
spec:
mtls: {mode: STRICT}
---
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: {name: legacy-metrics, namespace: %{ns}}
spec:
selector:
matchLabels: {app: legacy-metrics}
mtls: {mode: STRICT}
portLevelMtls:
"9090": {mode: PERMISSIVE}
EOF
sleep 10
""",
70: r"""
cat <<EOF | kubectl apply -f -
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata: {name: db-mutual-auth, namespace: %{ns}}
spec:
endpointSelector:
matchLabels: {app: db}
ingress:
- fromEndpoints:
- matchLabels: {app: api}
authentication:
mode: required
toPorts:
- ports:
- {port: "5432", protocol: TCP}
EOF
""",
# ---- 71-86: Supply Chain Security (bank_supply.py)
71: r"""
cd {WS}/q71/app
printf 'FROM alpine:3.20\nCOPY app.sh /app/app.sh\nRUN chmod 755 /app/app.sh\nUSER 10001\nCMD ["/app/app.sh"]\n' > Dockerfile
podman build -q -t localhost/%{img}:hardened . >/dev/null
""",
72: r"""
cd {WS}/q72/src
printf 'FROM golang:1.23-alpine AS build\nWORKDIR /src\nCOPY main.go .\nRUN CGO_ENABLED=0 go build -o /render-worker main.go\n\nFROM scratch\nCOPY --from=build /render-worker /render-worker\nUSER 65532\nENTRYPOINT ["/render-worker"]\n' > Dockerfile
podman build -q -t localhost/%{img}:slim . >/dev/null
""",
73: r"""
cd {WS}/q73
sed -i 's/^FROM nginx$/FROM nginx:1.27/; /^ENV DB_PASSWORD/d; /^USER root$/d' Dockerfile
yq -i '.spec.template.spec.containers[0].securityContext = {"runAsNonRoot": true, "runAsUser": 101} | (.spec.template.spec.containers[0].env[] | select(.name == "DB_PASSWORD")) = {"name": "DB_PASSWORD", "valueFrom": {"secretKeyRef": {"name": "storefront-db", "key": "password"}}}' deployment.yaml
""",
74: r"""
cd {WS}/q74
trivy image -q --format spdx-json --output nginx.spdx.json nginx:1.27-alpine
trivy image -q --format cyclonedx --output redis.cdx.json redis:7.2-alpine
jq -r '.packages[] | select(.name == "busybox") | .versionInfo' nginx.spdx.json > busybox.txt
""",
75: r"""
cd {WS}/q75
for d in $(kubectl -n %{ns} get deploy -o name | cut -d/ -f2); do
i=$(kubectl -n %{ns} get deploy $d -o jsonpath='{.spec.template.spec.containers[0].image}')
if trivy image -q --format cyclonedx $i 2>/dev/null | jq -e '.components[] | select(.name == "libssl1.1" and .version == "1.1.1d-0+deb10u3")' >/dev/null; then
echo $d > affected.txt
trivy image -q --format cyclonedx --output sbom.json $i
kubectl -n %{ns} scale deploy $d --replicas=0
fi
done
""",
76: r"""
cd {WS}/q76
trivy sbom -q --severity CRITICAL --format json --output report.json app.cdx.json
jq '[.Results[].Vulnerabilities[]?] | length' report.json > critical-count.txt
jq -r '.Results[].Vulnerabilities[]? | select(.PkgName == "libssl1.1") | .VulnerabilityID' report.json | sort -u > libssl.txt
""",
77: r"""
cd {WS}/q77
for d in frontend sessions docs; do
echo "$d $(kubectl -n %{ns} get pod -l app=$d -o jsonpath='{.items[0].status.containerStatuses[0].imageID}' | sed 's/.*@//')"
done > running.txt
while read -r d dg; do
if grep -qx "$dg" approved-digests.txt; then
repo=$(kubectl -n %{ns} get deploy $d -o jsonpath='{.spec.template.spec.containers[0].image}' < /dev/null | cut -d: -f1)
kubectl -n %{ns} set image deploy/$d "*=$repo@$dg" < /dev/null
else
kubectl -n %{ns} scale deploy $d --replicas=0 < /dev/null
fi
done < running.txt
for d in frontend sessions docs; do kubectl -n %{ns} rollout status deploy $d --timeout=180s; done
sleep 5
""",
78: r"""
cd {WS}/q78
bom generate --image-archive app.tar --output image.spdx 2>/dev/null
bom generate --dirs src --output src.spdx 2>/dev/null
bom document outline image.spdx > outline.txt 2>/dev/null
""",
79: H + r"""
sudo sed -i 's/defaultAllow: true/defaultAllow: false/' /etc/kubernetes/policywebhook/admission_config.yaml
sudo sed -i 's#https://CHANGE-ME/image_policy#https://localhost:1323/image_policy#' /etc/kubernetes/policywebhook/kubeconf
medit kube-apiserver.yaml '.spec.containers[0].command |= map(select(test("^--enable-admission-plugins=") | not)) | .spec.containers[0].command += ["--enable-admission-plugins=NodeRestriction,ImagePolicyWebhook", "--admission-control-config-file=/etc/kubernetes/policywebhook/admission_config.yaml"] | .spec.containers[0].volumeMounts += [{"name": "policywebhook", "mountPath": "/etc/kubernetes/policywebhook", "readOnly": true}] | .spec.volumes += [{"name": "policywebhook", "hostPath": {"path": "/etc/kubernetes/policywebhook", "type": "DirectoryOrCreate"}}]'
upwith kube-apiserver 'ImagePolicyWebhook'
kubectl run test --image=nginx:latest > {WS}/q79/denied.txt 2>&1
""",
80: r"""
cat <<'EOF' | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: no-latest}
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- {apiGroups: [""], apiVersions: ["v1"], operations: [CREATE, UPDATE], resources: [pods]}
validations:
- expression: >-
(object.spec.containers + (has(object.spec.initContainers) ? object.spec.initContainers : [])).all(c,
c.image.contains('@sha256:') || (c.image.matches('^.*:[^/:]+$') && !c.image.endsWith(':latest')))
message: every image needs a tag other than latest, or a digest
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: no-latest}
spec:
policyName: no-latest
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels: {images: pinned}
EOF
sleep 20
""",
81: r"""
kubectl -n %{ns} create secret docker-registry regcred --docker-server=registry.%{dom}.internal:5000 --docker-username=puller --docker-password="$(cat {WS}/q81/password.txt)"
kubectl -n %{ns} patch sa default -p '{"imagePullSecrets": [{"name": "regcred"}]}'
kubectl -n %{ns} delete pod -l app=app --wait=false
sleep 8
""",
82: r"""
cd {WS}/q82/release
: > ../rejected.txt
for f in payments.yaml catalog.yaml search.yaml; do
if cosign verify-blob --key cosign.pub --bundle $f.bundle --insecure-ignore-tlog=true $f >/dev/null 2>&1; then
kubectl apply -f $f
else
echo $f >> ../rejected.txt
fi
done
""",
83: r"""
cd {WS}/q83
cat > pod.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: %{pod}
namespace: %{ns}
spec:
serviceAccountName: default
securityContext: {runAsNonRoot: true, runAsUser: 10001, seccompProfile: {type: RuntimeDefault}}
containers:
- name: exporter
image: busybox:1.36
command: ["sleep", "86400"]
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
resources:
requests: {cpu: 50m, memory: 32Mi}
limits: {cpu: 100m, memory: 64Mi}
EOF
kubesec scan pod.yaml > report.json
kubectl apply -f pod.yaml
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=120s
""",
84: r"""
cd {WS}/q84
kube-linter lint manifests/deployment.yaml > before.txt 2>&1
yq -i '.spec.template.spec.containers[0].image = "busybox:1.36" | .spec.template.spec.containers[0].securityContext = {"privileged": false, "allowPrivilegeEscalation": false, "readOnlyRootFilesystem": true, "runAsNonRoot": true, "runAsUser": 10001} | .spec.template.spec.containers[0].resources = {"requests": {"cpu": "50m", "memory": "32Mi"}, "limits": {"cpu": "100m", "memory": "64Mi"}}' manifests/deployment.yaml
kubectl apply -f manifests/deployment.yaml
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
""",
85: r"""
cd {WS}/q85
: > critical.txt
for d in $(kubectl -n %{ns} get deploy -o name | cut -d/ -f2); do
i=$(kubectl -n %{ns} get deploy $d -o jsonpath='{.spec.template.spec.containers[0].image}')
n=$(trivy image -q --severity CRITICAL --format json $i 2>/dev/null | jq '[.Results[].Vulnerabilities[]?] | length')
if [ "$n" -gt 0 ]; then echo $d >> critical.txt; kubectl -n %{ns} scale deploy $d --replicas=0; fi
done
""",
86: r"""
cd {WS}/q86
trivy config -q --severity HIGH,CRITICAL --format json deployment.yaml 2>/dev/null | jq '[.Results[].Misconfigurations[]?] | length' > count.txt
yq -i 'del(.spec.template.spec.hostNetwork) | del(.spec.template.spec.hostPID) | .spec.template.spec.securityContext = {"runAsNonRoot": true, "runAsUser": 10001, "seccompProfile": {"type": "RuntimeDefault"}} | .spec.template.spec.containers[0].securityContext = {"privileged": false, "readOnlyRootFilesystem": true, "allowPrivilegeEscalation": false, "runAsNonRoot": true, "runAsUser": 10001, "capabilities": {"drop": ["ALL"]}}' deployment.yaml
kubectl apply -f deployment.yaml
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
""",
# ---- 87-102: Monitoring, Logging and Runtime Security (bank_runtime.py)
87: r"""
PID=$(ssh -n {W1} "pgrep -x %{proc}" | head -1)
echo $PID > {WS}/q87/pid.txt
CID=$(ssh -n {W1} "cat /proc/$PID/cgroup" | sed 's/.*cri-containerd-\([0-9a-f]*\)\.scope.*/\1/')
ssh -n {W1} "sudo crictl inspect $CID" | jq -r '.status.labels["io.kubernetes.pod.namespace"] + "/" + .status.labels["io.kubernetes.pod.name"]' > {WS}/q87/pod.txt
POD=$(cut -d/ -f2 {WS}/q87/pod.txt)
kubectl -n %{ns} scale deploy "$(kubectl -n %{ns} get pod $POD -o jsonpath='{.metadata.labels.app}')" --replicas=0
""",
88: r"""
for d in api billing search; do
f=$(kubectl -n %{ns} exec deploy/$d -- sh -c 'find / -xdev -type f -perm -4000 2>/dev/null' | head -1)
if [ -n "$f" ]; then
echo $d > {WS}/q88/deployment.txt; echo $f > {WS}/q88/added.txt
kubectl -n %{ns} patch deploy $d --type=json -p '[{"op": "add", "path": "/spec/template/spec/containers/0/securityContext", "value": {"readOnlyRootFilesystem": true}}]'
kubectl -n %{ns} rollout status deploy $d --timeout=180s
fi
done
sleep 35
""",
89: r"""
IP=$(ssh -n {W1} "sudo conntrack -L -d 198.51.100.77 2>/dev/null" | sed -n 's/.* src=\([0-9.]*\) dst=198\.51\.100\.77 .*/\1/p' | head -1)
kubectl get pod -A -o jsonpath="{range .items[?(@.status.podIP=='$IP')]}{.metadata.namespace}/{.metadata.name}{end}" > {WS}/q89/pod.txt
NS=$(cut -d/ -f1 {WS}/q89/pod.txt); POD=$(cut -d/ -f2 {WS}/q89/pod.txt)
APP=$(kubectl -n $NS get pod $POD -o jsonpath='{.metadata.labels.app}')
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: isolate-c2, namespace: $NS}
spec:
podSelector: {matchLabels: {app: $APP}}
policyTypes: [Egress]
EOF
sleep 6
""",
90: r"""
ssh -n {W1} "sudo grep -ls 'name: %{name}' /etc/kubernetes/manifests/*.yaml" > {WS}/q90/source.txt
ssh -n {W1} "sudo rm -f $(cat {WS}/q90/source.txt)"
echo staticPodPath > {WS}/q90/setting.txt
sleep 20
""",
91: r"""
cd {WS}/q91
jq -r 'select(.objectRef.resource == "secrets" and .objectRef.name == "%{secret}" and .responseStatus.code == 200) | .user.username' audit.log | sort -u > actor.txt
A=$(cat actor.txt)
jq -r --arg a "$A" 'select(.user.username == $a and .objectRef.name == "%{secret}") | .sourceIPs[0]' audit.log | sort -u > ip.txt
jq -r --arg a "$A" 'select(.user.username == $a and .objectRef.resource == "clusterrolebindings" and .responseStatus.code == 201) | .objectRef.name' audit.log > binding.txt
kubectl delete clusterrolebinding "$(cat binding.txt)"
""",
92: r"""
kubectl get deploy,ds,sts,cronjob,job,cm -A -o json | jq -r '.items[] | select(tostring | test("198\\.51\\.100\\.66([^0-9]|$)")) | (.kind | ascii_downcase) + "/" + .metadata.namespace + "/" + .metadata.name' > {WS}/q92/found.txt
while IFS=/ read -r k n name; do kubectl -n $n delete $k $name < /dev/null; done < {WS}/q92/found.txt
""",
93: r"""
F=/var/log/kubernetes/audit/audit.log
sudo jq -c 'select(.objectRef.subresource == "exec" and .objectRef.name == "%{pod}")' $F | tail -1 > {WS}/q93/event.json
jq -r .user.username {WS}/q93/event.json > {WS}/q93/user.txt
kubectl -n %{ns} patch role %{sa}-debug --type=json -p '[{"op": "remove", "path": "/rules/2"}]'
""",
94: r"""
kubectl -n %{ns} auth can-i get secrets --as=system:serviceaccount:%{ns}:ci-bot > {WS}/q94/secrets.txt
kubectl -n %{ns} delete sa ci-bot
kubectl -n %{ns} create sa ci-bot
sleep 6
""",
95: r"""
kubectl -n %{ns} patch deploy %{dep} --type=json -p '[
{"op": "add", "path": "/spec/template/spec/containers/0/securityContext", "value": {"readOnlyRootFilesystem": true, "allowPrivilegeEscalation": false}},
{"op": "add", "path": "/spec/template/spec/volumes", "value": [{"name": "tmp", "emptyDir": {}}, {"name": "log", "emptyDir": {}}]},
{"op": "add", "path": "/spec/template/spec/containers/0/volumeMounts", "value": [{"name": "tmp", "mountPath": "/tmp"}, {"name": "log", "mountPath": "/var/log"}]}]'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
sleep 35
""",
96: r"""
kubectl -n %{ns} get pod -o json | jq -r '.items[] | select(any(.spec.containers[]; .securityContext.readOnlyRootFilesystem != true) or any(.spec.containers[]; .securityContext.privileged == true) or any(.spec.volumes[]?; .hostPath)) | .metadata.name' > {WS}/q96/mutable.txt
kubectl -n %{ns} delete pod $(cat {WS}/q96/mutable.txt) --wait=false
sleep 3
""",
97: r"""
cat <<'EOF' | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: immutable-pods}
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- {apiGroups: [""], apiVersions: ["v1"], operations: [CREATE, UPDATE], resources: [pods]}
validations:
- expression: >-
(object.spec.containers + (has(object.spec.initContainers) ? object.spec.initContainers : [])).all(c,
has(c.securityContext) && has(c.securityContext.readOnlyRootFilesystem) && c.securityContext.readOnlyRootFilesystem
&& !(has(c.securityContext.privileged) && c.securityContext.privileged))
message: every container needs readOnlyRootFilesystem true and must not be privileged
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: immutable-pods}
spec:
policyName: immutable-pods
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels: {immutable: "true"}
EOF
sleep 20
""",
98: r"""
kubectl -n %{ns} patch cm app-settings -p '{"immutable": true}'
kubectl -n %{ns} patch secret app-keys -p '{"immutable": true}'
kubectl -n %{ns} patch role developer --type=json -p '[{"op": "remove", "path": "/rules/2"}]'
kubectl -n %{ns} patch cm app-settings -p '{"data": {"mode": "test"}}' > {WS}/q98/immutable.txt 2>&1
""",
99: H + r"""
cat <<EOF | sudo tee /etc/kubernetes/audit/policy.yaml >/dev/null
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- RequestReceived
rules:
- level: Metadata
resources:
- group: ""
resources: [secrets]
- level: None
verbs: [get, list, watch]
- level: RequestResponse
resources:
- group: ""
resources: [configmaps]
namespaces: [%{ns}]
- level: Metadata
EOF
restart_api
""",
100: r"""
F=/var/log/kubernetes/audit/audit.log
sudo jq -r 'select(.objectRef.resource == "secrets" and .objectRef.name == "payroll") | .user.username' $F | sort | uniq -c | sort -rn | head -1 | awk '{print $2}' > {WS}/q100/user.txt
SA=$(cut -d: -f4 {WS}/q100/user.txt)
kubectl -n %{ns} get pod -o jsonpath="{.items[?(@.spec.serviceAccountName=='$SA')].metadata.name}" > {WS}/q100/pod.txt
kubectl -n %{ns} delete rolebinding $SA-secrets
sleep 12
""",
101: H + r"""
sudo mkdir -p /etc/kubernetes/audit /var/log/kubernetes/audit
cat <<EOF | sudo tee /etc/kubernetes/audit/policy.yaml >/dev/null
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["pods/exec", "pods/attach"]
- level: RequestResponse
verbs: [delete, deletecollection]
namespaces: [%{ns}]
- level: None
EOF
medit kube-apiserver.yaml '.spec.containers[0].command += ["--audit-policy-file=/etc/kubernetes/audit/policy.yaml", "--audit-log-path=/var/log/kubernetes/audit/audit.log", "--audit-log-maxage=14", "--audit-log-maxbackup=5", "--audit-log-maxsize=10"] | .spec.containers[0].volumeMounts += [{"name": "audit-policy", "mountPath": "/etc/kubernetes/audit", "readOnly": true}, {"name": "audit-log", "mountPath": "/var/log/kubernetes/audit"}] | .spec.volumes += [{"name": "audit-policy", "hostPath": {"path": "/etc/kubernetes/audit", "type": "DirectoryOrCreate"}}, {"name": "audit-log", "hostPath": {"path": "/var/log/kubernetes/audit", "type": "DirectoryOrCreate"}}]'
upwith kube-apiserver '--audit-policy-file='
""",
102: H + r"""
sudo sed -i 's/level: Metdata/level: Metadata/' /etc/kubernetes/audit/policy.yaml
sudo mkdir -p /var/log/kubernetes/audit
medit kube-apiserver.yaml '.spec.containers[0].volumeMounts += [{"name": "audit-policy", "mountPath": "/etc/kubernetes/audit", "readOnly": true}, {"name": "audit-log", "mountPath": "/var/log/kubernetes/audit"}] | .spec.volumes += [{"name": "audit-policy", "hostPath": {"path": "/etc/kubernetes/audit", "type": "DirectoryOrCreate"}}, {"name": "audit-log", "hostPath": {"path": "/var/log/kubernetes/audit", "type": "DirectoryOrCreate"}}]'
upwith kube-apiserver '--audit-policy-file='
""",
# ---- 103-110: Falco and AppArmor, on a VM host (bank_vm.py)
103: r"""
L=$(sudo journalctl -u falco-modern-bpf --no-pager -o cat --since -5min | grep 'Sensitive file opened' | grep 'k8s_ns_name=%{ns}' | tail -1)
echo "$L" > {WS}/q103/alert.txt
POD=$(echo "$L" | sed 's/.*k8s_pod_name=\([^ ]*\).*/\1/')
echo "%{ns}/$POD" > {WS}/q103/pod.txt
sudo grep -B60 'Sensitive file opened for reading by non-trusted program' /etc/falco/falco_rules.yaml | grep -- '- rule:' | tail -1 | sed 's/.*- rule: //' > {WS}/q103/rule.txt
kubectl -n %{ns} scale deploy "$(kubectl -n %{ns} get pod $POD -o jsonpath='{.metadata.labels.app}')" --replicas=0
""",
104: r"""
cat <<'EOF' | sudo tee -a /etc/falco/falco_rules.local.yaml >/dev/null
- rule: Shell in container
desc: A shell was started inside a container
condition: spawned_process and container and proc.name in (sh, bash)
output: "Shell in container: user=%user.name container=%container.name pod=%k8s.pod.name cmdline=%proc.cmdline"
priority: WARNING
EOF
sudo systemctl restart falco-modern-bpf
sleep 12
kubectl -n %{ns} exec %{pod} -- sh -c 'echo hello'
sleep 5
sudo journalctl -u falco-modern-bpf --no-pager -o cat --since -1min | grep 'Shell in container' | tail -1 > {WS}/q104/alert.txt
""",
105: r"""
cat <<'EOF' | sudo tee -a /etc/falco/falco_rules.local.yaml >/dev/null
- rule: Read sensitive file untrusted
output: "%evt.time,%container.id,%container.name,%k8s.ns.name,%k8s.pod.name,%user.name,%fd.name"
override:
output: replace
EOF
sudo systemctl restart falco-modern-bpf
sleep 25
sudo journalctl -u falco-modern-bpf --no-pager -o cat --since -20s | grep ',%{pod},' > {WS}/q105/alerts.txt
""",
106: r"""
sudo mkdir -p /var/log/falco
printf 'json_output: true\nfile_output:\n enabled: true\n keep_alive: false\n filename: /var/log/falco/events.json\n' | sudo tee /etc/falco/config.d/90-json-file.yaml >/dev/null
sudo systemctl restart falco-modern-bpf
sleep 20
""",
107: r"""
cat <<'EOF' | sudo tee -a /etc/falco/falco_rules.local.yaml >/dev/null
- list: network_tools
items: [nc, ncat, wget, curl, nslookup]
- rule: Network tool in restricted namespace
desc: A network tool was started in a container of a Namespace that should not need one
condition: spawned_process and container and proc.name in (network_tools) and k8s.ns.name = "%{ns}"
output: "Network tool: ns=%k8s.ns.name pod=%k8s.pod.name tool=%proc.name cmdline=%proc.cmdline"
priority: ERROR
EOF
sudo systemctl restart falco-modern-bpf
sleep 15
""",
108: r"""
sudo apparmor_parser -q {WS}/q108/k8s-deny-write
kubectl -n %{ns} patch deploy %{dep} -p '{"spec":{"template":{"spec":{"containers":[{"name":"busybox","securityContext":{"appArmorProfile":{"type":"Localhost","localhostProfile":"k8s-deny-write"}}}]}}}}'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=180s
sleep 35
kubectl -n %{ns} exec deploy/%{dep} -- touch /tmp/test > {WS}/q108/denied.txt 2>&1
kubectl -n %{ns} exec deploy/%{dep} -- cat /proc/1/attr/current > {WS}/q108/current.txt
""",
109: r"""
sudo apparmor_parser -q /etc/apparmor.d/k8s-audit-agent
kubectl -n %{ns} get pod %{pod} -o yaml | sed 's/localhostProfile: k8s-audit-agent/localhostProfile: audit-agent-restricted/' > /tmp/pod.yaml
kubectl replace --force -f /tmp/pod.yaml
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=120s
""",
110: r"""
cat <<'EOF' | sudo tee /etc/apparmor.d/%{name} >/dev/null
#include <tunables/global>
profile %{name} flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
file,
network,
capability,
deny /etc/shadow r,
deny /usr/** w,
}
EOF
sudo apparmor_parser -q /etc/apparmor.d/%{name}
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata: {name: %{pod}, namespace: %{ns}}
spec:
containers:
- name: app
image: busybox:1.36
command: [sleep, "86400"]
securityContext:
appArmorProfile: {type: Localhost, localhostProfile: %{name}}
EOF
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=120s
""",
}