Add lessons: untimed guided labs, one per curriculum objective, on a host of their own, in the browser and in the terminal. Each step has a check, a hint and its commands; a lesson writes no history or scores, only a done mark on its objective. CKAD 2.1-2.4 (24 steps) and all 27 CKA objectives (135 steps) have one, and `./ksim lesson audit` proves that every step fails, is done by its own commands, then passes. Add CKA tasks 132-147 (cka/bank_course.py) on what a full CKA course teaches beyond the bank: a real upgrade from v1.36, a drain under a PodDisruptionBudget, QoS and eviction, in-place resize, kube-proxy's chains, encryption at rest, an operator, and CSI expansion, snapshots and clones (ensure_csi_hostpath). Move CKA and CKS to Kubernetes v1.37 and rebuild the hosts on Ubuntu 24.04, with kubelet, kubeadm and kubectl as held packages from pkgs.k8s.io; the desktop runs Ubuntu too. Hosts gain man pages, hexdump and xxd. Fix CKS Q4, which gave 3 of 12 points untouched, and CKA Q4 and Q49, whose solutions raced their new CRD. Audited on v1.37: CKA 147/147, CKAD 105/105, CKS 109/110 (Q103, a Falco task, failed to seed once and awaits a re-audit); lessons: 135 of 135 CKA steps and 24 of 24 CKAD steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1267 lines
54 KiB
Python
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 sed -i 's|/v[0-9.]*/deb/|/v${V%.*}/deb/|' /etc/apt/sources.list.d/kubernetes.list; 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
|
|
""",
|
|
}
|