"""CKS track. Starter bank. Caveat worth knowing before you rely on this: several CKS objectives need infrastructure the default kind cluster does not have - kindnet does not enforce NetworkPolicy, and there is no Falco, AppArmor profile loader, gVisor runtime or image scanner installed. Tasks here either stick to what a stock kubeadm/kind cluster can really do (PSA, RBAC, admission policies, audit logging, seccomp, token hygiene) or are graded on the object rather than on enforcement, and say so. """ from tracks.cka.common import C, NS, ns_pick, name_pick TRACK = { "id": "cks", "name": "Certified Kubernetes Security Specialist", "minutes": 120, "pass_pct": 67, "domains": { "SETUP": "Cluster Setup (15%)", "HARDEN": "Cluster Hardening (15%)", "SYS": "System Hardening (10%)", "MICRO": "Minimize Microservice Vulnerabilities (20%)", "SUPPLY": "Supply Chain Security (20%)", "RUNTIME": "Monitoring, Logging and Runtime Security (20%)", }, "quota": {"SETUP": 2, "HARDEN": 2, "SYS": 1, "MICRO": 2, "SUPPLY": 1, "RUNTIME": 1}, "quota_hard": {"SETUP": 2, "HARDEN": 2, "SYS": 1, "MICRO": 2, "SUPPLY": 1, "RUNTIME": 1}, } QUESTIONS = [ # ----------------------------------------------------------------- SETUP ---- { "id": 1, "domain": "SETUP", "title": "Default-deny, then allow what's needed", "params": lambda r: {"ns": ns_pick(r, "zone")}, "task": """ Namespace `%{ns}` runs `app=web` (port 8080) and `app=db` (port 5432). Create three NetworkPolicies: 1. `default-deny` — denies all ingress **and** egress for every Pod in the Namespace. 2. `allow-dns` — lets every Pod reach DNS (UDP and TCP 53) in `kube-system`. 3. `web-to-db` — lets `app=web` reach `app=db` on 5432, and lets `app=db` accept that ingress. (kindnet does not enforce policies; you are graded on the objects.) """, "setup": NS + r""" mkns %{ns} for a in web db; do $K -n %{ns} create deploy $a --image=nginx:1.27 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n %{ns} patch deploy $a -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"app\":\"$a\"}}}}}" >/dev/null done $K -n %{ns} delete netpol --all >/dev/null 2>&1 """, "checks": [ C("default-deny covers all pods, both directions, with no allow rules", 3, r""" n="$K -n %{ns} get netpol default-deny" eq "$($n -o jsonpath='{.spec.podSelector}' 2>/dev/null)" '{}' && $n -o jsonpath='{.spec.policyTypes}' | grep -q Ingress \ && $n -o jsonpath='{.spec.policyTypes}' | grep -q Egress && [ -z "$($n -o jsonpath='{.spec.ingress}')" ] && [ -z "$($n -o jsonpath='{.spec.egress}')" ]"""), C("allow-dns permits UDP and TCP 53 to kube-system", 3, r""" $K -n %{ns} get netpol allow-dns -o json 2>/dev/null | python3 -c ' import sys,json s=json.load(sys.stdin)["spec"] ok=any({p.get("protocol") for p in r.get("ports",[]) if str(p.get("port"))=="53"}>={"UDP","TCP"} and r.get("to") and all("kube-system" in json.dumps(t.get("namespaceSelector",{})) for t in r["to"]) for r in s.get("egress",[])) sys.exit(0 if ok else 1)'"""), C("web-to-db pairs web egress and db ingress on 5432", 4, r""" $K -n %{ns} get netpol -o json 2>/dev/null | python3 -c ' import sys,json eg=ing=False for n in json.load(sys.stdin)["items"]: s=n["spec"]; app=s["podSelector"].get("matchLabels",{}).get("app") for r in s.get("egress",[]): if app=="web" and any(t.get("podSelector",{}).get("matchLabels",{}).get("app")=="db" for t in r.get("to",[])) \ and any(str(p.get("port"))=="5432" for p in r.get("ports",[])): eg=True for r in s.get("ingress",[]): if app=="db" and any(t.get("podSelector",{}).get("matchLabels",{}).get("app")=="web" for t in r.get("from",[])) \ and any(str(p.get("port"))=="5432" for p in r.get("ports",[])): ing=True sys.exit(0 if eg and ing else 1)'"""), ], "solution": """ cat </dev/null"""), C("the manifest now sets --profiling=false", 4, r"""nx "$CP" 'grep -q -- "--profiling=false" /etc/kubernetes/manifests/kube-apiserver.yaml'"""), C("apiserver is healthy again", 4, r""" for i in $(seq 1 20); do $K get --raw=/healthz >/dev/null 2>&1 && [ "$(j -n kube-system pod -l component=kube-apiserver '{.items[0].status.phase}')" = Running ] && exit 0; sleep 6; done; exit 1"""), C("after.txt records false", 1, r"""grep -q 'false' "$WS/q2/after.txt" 2>/dev/null"""), ], "solution": """ {SHELL_CP} grep profiling /etc/kubernetes/manifests/kube-apiserver.yaml || echo absent # -> {WS}/q2/before.txt vi /etc/kubernetes/manifests/kube-apiserver.yaml # under command:, add - --profiling=false # the kubelet notices the manifest change and restarts the static pod - watch with: crictl ps | grep apiserver exit k get --raw=/healthz echo false > {WS}/q2/after.txt # Editing the manifest is what makes it persistent; `kubectl edit` on the mirror pod would be lost. # Mind the indentation: the flag goes in the container's command list, not under spec. """, }, # ---------------------------------------------------------------- HARDEN ---- { "id": 3, "domain": "HARDEN", "title": "Replace a wildcard ClusterRole", "params": lambda r: {"ns": ns_pick(r, "team"), "sa": name_pick(r, "runner")}, "task": """ ServiceAccount `%{sa}` in `%{ns}` is bound to a ClusterRole granting `*` on `*` cluster-wide. Reduce it to exactly what it needs: - read-only (`get`, `list`, `watch`) on `pods` and `pods/log` **in `%{ns}` only**. Use Role `%{sa}-view` and RoleBinding `%{sa}-view`. Remove the over-privileged binding. The ServiceAccount must have no other permissions anywhere. """, "setup": NS + r""" mkns %{ns} $K -n %{ns} create sa %{sa} --dry-run=client -o yaml | $K apply -f - >/dev/null $K delete clusterrole %{sa}-god --ignore-not-found >/dev/null; $K delete clusterrolebinding %{sa}-god --ignore-not-found >/dev/null $K -n %{ns} delete role,rolebinding --all >/dev/null 2>&1 $K create clusterrole %{sa}-god --verb='*' --resource='*.*' >/dev/null $K create clusterrolebinding %{sa}-god --clusterrole=%{sa}-god --serviceaccount=%{ns}:%{sa} >/dev/null """, "checks": [ C("the wildcard binding is gone", 3, r"""! $K get clusterrolebinding %{sa}-god >/dev/null 2>&1"""), C("can get/list/watch pods and pods/log in %{ns}", 3, r""" for res in pods pods/log; do for v in get list watch; do eq "$($K auth can-i $v $res -n %{ns} --as=system:serviceaccount:%{ns}:%{sa} 2>/dev/null)" yes || exit 1 done; done"""), C("nothing else: no delete pods, no secrets, nothing in other namespaces", 4, r""" for t in "delete pods -n %{ns}" "get secrets -n %{ns}" "create pods -n %{ns}" "list pods -n default" "list nodes"; do eq "$($K auth can-i $t --as=system:serviceaccount:%{ns}:%{sa} 2>/dev/null || true)" no || exit 1 done"""), ], "solution": """ k get clusterrolebinding -o wide | grep %{sa} k delete clusterrolebinding %{sa}-god k -n %{ns} create role %{sa}-view --verb=get,list,watch --resource=pods,pods/log k -n %{ns} create rolebinding %{sa}-view --role=%{sa}-view --serviceaccount=%{ns}:%{sa} k auth can-i --list -n %{ns} --as=system:serviceaccount:%{ns}:%{sa} # Deleting the ClusterRole itself is optional - the binding is what grants the access. """, }, { "id": 4, "domain": "HARDEN", "title": "Stop handing out API tokens", "params": lambda r: {"ns": ns_pick(r, "apps"), "dep": name_pick(r, "worker")}, "task": """ In Namespace `%{ns}`: 1. Make the **default** ServiceAccount stop mounting its API token into Pods automatically. 2. Deployment `%{dep}` genuinely needs API access: give it a dedicated ServiceAccount `%{dep}-sa` that does mount its token, and make the Deployment use it. 3. Confirm: a new Pod using the default ServiceAccount has no token mounted, while `%{dep}`'s Pods do. """, "setup": NS + r""" mkns %{ns} $K -n %{ns} patch sa default -p '{"automountServiceAccountToken":null}' >/dev/null 2>&1 || true $K -n %{ns} delete sa %{dep}-sa --ignore-not-found >/dev/null $K -n %{ns} delete deploy %{dep} --ignore-not-found >/dev/null $K -n %{ns} delete pod plain --ignore-not-found --wait=false >/dev/null $K -n %{ns} create deploy %{dep} --image=nginx:1.27 >/dev/null """, "checks": [ C("default ServiceAccount has automountServiceAccountToken: false", 3, r"""eq "$(j -n %{ns} sa default '{.automountServiceAccountToken}')" false"""), C("%{dep} uses %{dep}-sa, which does mount a token", 3, r""" eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.serviceAccountName}')" %{dep}-sa \ && [ "$(j -n %{ns} sa %{dep}-sa '{.automountServiceAccountToken}')" != false ]"""), C("%{dep} pods have a projected token mounted", 3, r""" p=$(j -n %{ns} pod -l app=%{dep} '{.items[0].metadata.name}'); [ -n "$p" ] || exit 1 $K -n %{ns} exec "$p" -- test -f /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null"""), C("a pod on the default ServiceAccount has no token", 3, r""" $K -n %{ns} delete pod plain --ignore-not-found --wait=true >/dev/null 2>&1 $K -n %{ns} run plain --image=busybox:1.36 --restart=Never -- sleep 120 >/dev/null 2>&1 for i in $(seq 1 15); do [ "$(j -n %{ns} pod plain '{.status.phase}')" = Running ] && break; sleep 3; done r=1; $K -n %{ns} exec plain -- test -f /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null || r=0 $K -n %{ns} delete pod plain --wait=false >/dev/null 2>&1; exit $r"""), ], "solution": """ k -n %{ns} patch sa default -p '{"automountServiceAccountToken": false}' k -n %{ns} create sa %{dep}-sa k -n %{ns} set serviceaccount deploy %{dep} %{dep}-sa k -n %{ns} rollout status deploy %{dep} # The setting can also be put on the Pod spec (automountServiceAccountToken: false), which # overrides the ServiceAccount - useful when only one workload should differ. """, }, # ------------------------------------------------------------------- SYS ---- { "id": 5, "domain": "SYS", "title": "seccomp and a minimal container", "params": lambda r: {"ns": ns_pick(r, "sys"), "pod": name_pick(r, "confined")}, "task": """ Create Pod `%{pod}` in `%{ns}` (image `nginx:1.27`) that: - uses the **RuntimeDefault** seccomp profile at Pod level, - runs as a non-root user (UID 101, nginx's own user) with `allowPrivilegeEscalation: false`, - drops all capabilities and adds back only `NET_BIND_SERVICE` and `CHOWN`, - mounts an `emptyDir` at `/var/cache/nginx` and one at `/var/run` so nginx can still write while the root filesystem is read-only. The Pod must reach Running. """, "setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null""", "checks": [ C("seccompProfile RuntimeDefault at pod level", 3, r"""eq "$(j -n %{ns} pod %{pod} '{.spec.securityContext.seccompProfile.type}')" RuntimeDefault"""), C("non-root UID 101, no privilege escalation, read-only root fs", 3, r""" p="$K -n %{ns} get pod %{pod}" u=$($p -o jsonpath='{.spec.securityContext.runAsUser}' 2>/dev/null); [ -z "$u" ] && u=$($p -o jsonpath='{.spec.containers[0].securityContext.runAsUser}') eq "$u" 101 && eq "$($p -o jsonpath='{.spec.containers[0].securityContext.allowPrivilegeEscalation}')" false \ && eq "$($p -o jsonpath='{.spec.containers[0].securityContext.readOnlyRootFilesystem}')" true"""), C("capabilities drop ALL, add NET_BIND_SERVICE and CHOWN", 3, r""" c='{.spec.containers[0].securityContext.capabilities' j -n %{ns} pod %{pod} "$c.drop[*]}" | grep -q ALL && j -n %{ns} pod %{pod} "$c.add[*]}" | grep -q NET_BIND_SERVICE && j -n %{ns} pod %{pod} "$c.add[*]}" | grep -q CHOWN"""), C("pod Running with both writable mounts", 3, r""" eq "$(j -n %{ns} pod %{pod} '{.status.phase}')" Running \ && j -n %{ns} pod %{pod} '{.spec.containers[0].volumeMounts[*].mountPath}' | grep -q '/var/cache/nginx' \ && j -n %{ns} pod %{pod} '{.spec.containers[0].volumeMounts[*].mountPath}' | grep -q '/var/run'"""), ], "solution": """ cat </dev/null 2>&1 || true $K -n %{ns} delete deploy %{dep} --ignore-not-found --wait=true >/dev/null $K apply -f - >/dev/null </dev/null 2>&1 """, "checks": [ C("namespace enforces, warns and audits restricted", 3, r""" l=$(j ns %{ns} '{.metadata.labels}') echo "$l" | grep -q '"pod-security.kubernetes.io/enforce":"restricted"' && echo "$l" | grep -q '"pod-security.kubernetes.io/warn":"restricted"' && echo "$l" | grep -q '"pod-security.kubernetes.io/audit":"restricted"'"""), C("deployment no longer asks for privilege", 3, r""" t=$(j -n %{ns} deploy %{dep} '{.spec.template.spec}') ! echo "$t" | grep -q '"privileged":true' && echo "$t" | grep -q '"allowPrivilegeEscalation":false' && echo "$t" | grep -q 'RuntimeDefault'"""), C("it satisfies restricted: non-root, drop ALL", 3, r""" t=$(j -n %{ns} deploy %{dep} '{.spec.template.spec}') echo "$t" | grep -q '"runAsNonRoot":true' && echo "$t" | grep -q '"drop":\["ALL"\]'"""), C("2 replicas Available under enforcement, image unchanged", 4, r""" eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].image}')" nginx:1.27 for i in $(seq 1 12); do [ "$(j -n %{ns} deploy %{dep} '{.status.availableReplicas}')" = 2 ] && exit 0; sleep 5; done; exit 1"""), ], "solution": """ k 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 k -n %{ns} rollout restart deploy %{dep} # now blocked, see the events for the exact reasons k -n %{ns} get events --field-selector reason=FailedCreate k -n %{ns} edit deploy %{dep} # pod template needs, at minimum: # securityContext: {runAsNonRoot: true, runAsUser: 101, seccompProfile: {type: RuntimeDefault}} # container securityContext: {allowPrivilegeEscalation: false, capabilities: {drop: [ALL]}} # and remove privileged: true k -n %{ns} rollout status deploy %{dep} # restricted requires all four: runAsNonRoot, allowPrivilegeEscalation=false, drop ALL, # and a RuntimeDefault (or Localhost) seccomp profile. """, }, { "id": 7, "domain": "MICRO", "title": "Isolate a workload's ServiceAccount and namespace", "params": lambda r: {"ns": ns_pick(r, "tenant"), "pod": name_pick(r, "isolated")}, "task": """ Create Pod `%{pod}` in Namespace `%{ns}` (image `busybox:1.36`, `sleep 86400`) that: - uses ServiceAccount `%{pod}-sa` (create it), - does **not** get an API token mounted, set on the **Pod**, not the ServiceAccount, - runs with `hostNetwork`, `hostPID` and `hostIPC` all explicitly disabled, - has both CPU and memory requests and limits set (any sane values). """, "setup": NS + r""" mkns %{ns} $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null $K -n %{ns} delete sa %{pod}-sa --ignore-not-found >/dev/null """, "checks": [ C("uses its own ServiceAccount", 2, r"""$K -n %{ns} get sa %{pod}-sa >/dev/null 2>&1 && eq "$(j -n %{ns} pod %{pod} '{.spec.serviceAccountName}')" %{pod}-sa"""), C("token not mounted, set on the pod spec", 3, r""" eq "$(j -n %{ns} pod %{pod} '{.spec.automountServiceAccountToken}')" false \ && ! $K -n %{ns} exec %{pod} -- test -f /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null"""), C("host namespaces explicitly disabled", 3, r""" p="$K -n %{ns} get pod %{pod}" eq "$($p -o jsonpath='{.spec.hostNetwork}' 2>/dev/null)" false && eq "$($p -o jsonpath='{.spec.hostPID}')" false && eq "$($p -o jsonpath='{.spec.hostIPC}')" false"""), C("requests and limits set for cpu and memory", 2, r""" c='{.spec.containers[0].resources' for f in requests.cpu requests.memory limits.cpu limits.memory; do [ -n "$(j -n %{ns} pod %{pod} "$c.$f}")" ] || exit 1; done"""), ], "solution": """ k -n %{ns} create sa %{pod}-sa cat <&1 || echo "no token, as intended" """, }, # ---------------------------------------------------------------- SUPPLY ---- { "id": 8, "domain": "SUPPLY", "title": "Admission policy for allowed registries", "difficulty": "hard", "params": lambda r: {"ns": ns_pick(r, "trusted"), "reg": r.choice(["registry.internal.io", "harbor.corp.io"])}, "task": """ Only images from `%{reg}/` may run in Namespace `%{ns}`. Using **ValidatingAdmissionPolicy** (no third-party admission controller): 1. Create a `ValidatingAdmissionPolicy` named `allowed-registry` that denies any Pod whose containers are not all from `%{reg}/`. 2. Bind it with a `ValidatingAdmissionPolicyBinding` named `allowed-registry` that enforces (`validationActions: [Deny]`) on Namespace `%{ns}` only. 3. Prove it: creating a Pod from `nginx:1.27` in `%{ns}` must be rejected. Save the rejection message to `{WS}/q8/denied.txt`. """, "setup": NS + r""" mkns %{ns} rm -rf "$WS/q8"; mkdir -p "$WS/q8" $K label ns %{ns} registry-policy=enforced --overwrite >/dev/null $K delete validatingadmissionpolicybinding allowed-registry --ignore-not-found >/dev/null 2>&1 $K delete validatingadmissionpolicy allowed-registry --ignore-not-found >/dev/null 2>&1 """, "checks": [ C("ValidatingAdmissionPolicy allowed-registry exists and checks image prefixes", 3, r""" v=$(j validatingadmissionpolicy allowed-registry '{.spec.validations[*].expression}' 2>/dev/null) [ -n "$v" ] && echo "$v" | grep -q '%{reg}' && j validatingadmissionpolicy allowed-registry '{.spec.matchConstraints.resourceRules[*].resources[*]}' | grep -q pods"""), C("binding enforces Deny and is scoped to %{ns}", 3, r""" b="$K get validatingadmissionpolicybinding allowed-registry" eq "$($b -o jsonpath='{.spec.policyName}' 2>/dev/null)" allowed-registry && $b -o jsonpath='{.spec.validationActions[*]}' | grep -q Deny \ && $b -o jsonpath='{.spec.matchResources}' | grep -Eq '%{ns}|registry-policy'"""), C("a disallowed image is actually rejected", 4, r""" out=$($K -n %{ns} run vap-probe --image=nginx:1.27 --restart=Never 2>&1); rc=$? $K -n %{ns} delete pod vap-probe --ignore-not-found --wait=false >/dev/null 2>&1 [ $rc -ne 0 ] && echo "$out" | grep -qi 'denied\|not allowed\|failed'"""), C("denied.txt captures the rejection", 2, r"""grep -Eqi 'denied|not allowed|admission' "$WS/q8/denied.txt" 2>/dev/null"""), ], "solution": """ cat <&1 | tee {WS}/q8/denied.txt # rejected # CEL runs in-process in the apiserver - no webhook to deploy, no certificate to manage. # Add initContainers to the expression too if the exam asks for every container. """, }, # --------------------------------------------------------------- RUNTIME ---- { "id": 9, "domain": "RUNTIME", "title": "Turn on API audit logging", "breaks": "apiserver-flag", "difficulty": "hard", "task": """ The cluster has no audit trail. On the control-plane node: 1. Write an audit policy to `/etc/kubernetes/audit-policy.yaml` that logs Secret access at `Metadata` level and everything else at `Metadata` too, but drops the noisy `RequestReceived` stage. 2. Configure the kube-apiserver to use it, writing to `/var/log/kubernetes/audit.log`, keeping 7 days of logs and at most 5 rotated files. 3. Bring the API server back healthy, then generate an event (`kubectl get secrets -A`) and copy the last audit line mentioning `secrets` to `{WS}/q9/audit-line.json`. """, "setup": r""" rm -rf "$WS/q9"; mkdir -p "$WS/q9" nx "$CP" 'sed -i "/--audit-/d" /etc/kubernetes/manifests/kube-apiserver.yaml; rm -f /etc/kubernetes/audit-policy.yaml' || true """, "checks": [ C("audit policy file exists and omits RequestReceived", 3, r""" nx "$CP" 'test -f /etc/kubernetes/audit-policy.yaml' && nx "$CP" 'grep -q "omitStages" /etc/kubernetes/audit-policy.yaml' \ && nx "$CP" 'grep -q "RequestReceived" /etc/kubernetes/audit-policy.yaml'"""), C("apiserver flags: policy file, log path, maxage 7, maxbackup 5", 4, r""" m=$(nx "$CP" 'cat /etc/kubernetes/manifests/kube-apiserver.yaml') echo "$m" | grep -q -- '--audit-policy-file=/etc/kubernetes/audit-policy.yaml' \ && echo "$m" | grep -q -- '--audit-log-path=/var/log/kubernetes/audit.log' \ && echo "$m" | grep -q -- '--audit-log-maxage=7' && echo "$m" | grep -q -- '--audit-log-maxbackup=5'"""), C("apiserver healthy and writing the log", 4, r""" for i in $(seq 1 20); do $K get --raw=/healthz >/dev/null 2>&1 && break; sleep 6; done $K get --raw=/healthz >/dev/null 2>&1 || exit 1 $K get secrets -A >/dev/null 2>&1 for i in $(seq 1 6); do nx "$CP" 'test -s /var/log/kubernetes/audit.log' && exit 0; sleep 5; done; exit 1"""), C("audit-line.json is a real audit event about secrets", 3, r""" python3 -c ' import json,sys d=json.load(open("'"$WS"'/q9/audit-line.json")) sys.exit(0 if d.get("kind")=="Event" and "secrets" in json.dumps(d) else 1)' 2>/dev/null"""), ], "solution": """ {SHELL_CP} mkdir -p /var/log/kubernetes cat > /etc/kubernetes/audit-policy.yaml <<'EOF' apiVersion: audit.k8s.io/v1 kind: Policy omitStages: - RequestReceived rules: - level: Metadata resources: - group: "" resources: ["secrets"] - level: Metadata EOF vi /etc/kubernetes/manifests/kube-apiserver.yaml # 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 # volumeMounts: # - {name: audit-policy, mountPath: /etc/kubernetes/audit-policy.yaml, readOnly: true} # - {name: audit-log, mountPath: /var/log/kubernetes} # volumes: # - {name: audit-policy, hostPath: {path: /etc/kubernetes/audit-policy.yaml, type: File}} # - {name: audit-log, hostPath: {path: /var/log/kubernetes, type: DirectoryOrCreate}} exit k get secrets -A # grab the last secrets line: # grep secrets /var/log/kubernetes/audit.log | tail -1 -> {WS}/q9/audit-line.json # Forgetting the two hostPath mounts is the usual reason the apiserver crashloops here: # the flags point at paths that don't exist inside the container. """, }, ]