Aller au contenu
English
English
Conteneurs & Orchestration medium

Exercices CKS, Entraînement sécurité chronométré

115 min de lecture

logo kubernetes

Entraînement CKS format examen. Ces exercices reproduisent les tâches de sécurisation Kubernetes demandées à la CKS : isolation réseau, RBAC, hardening, supply chain, runtime. Chronométrez-vous et visez 80% de réussite.

Configurez votre environnement avant de commencer :

Fenêtre de terminal
# Alias essentiels
alias k=kubectl
alias kn='kubectl config set-context --current --namespace'
alias kgp='kubectl get pods'
export do="--dry-run=client -o yaml"
export now="--force --grace-period=0"
# Autocomplétion
source <(kubectl completion bash)
complete -F __start_kubectl k
# Namespace de travail
kubectl create ns cks-exercises
kn cks-exercises

Ces exercices couvrent les tâches universelles de la CKS, réalisables dans n'importe quel cluster Kubernetes.


Temps cible : 4 minutes

ContexteNamespace cks-exercises
TâcheCréer une NetworkPolicy deny-all qui bloque tout trafic entrant et sortant pour tous les Pods du namespace
Validationkubectl describe netpol deny-all montre podSelector: {} et policyTypes: [Ingress, Egress]
Solution
Fenêtre de terminal
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: cks-exercises
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
EOF
# Validation
kubectl get netpol deny-all -n cks-exercises
kubectl describe netpol deny-all -n cks-exercises

Point clé : podSelector: {} sélectionne tous les Pods. Sans règle ingress ni egress, tout est bloqué par défaut.


Exercice 2 : NetworkPolicy Allow Specific (avec Service)

Section intitulée « Exercice 2 : NetworkPolicy Allow Specific (avec Service) »

Temps cible : 6 minutes

ContexteNamespace cks-exercises, policy deny-all en place
TâcheCréer un Deployment api (nginx:1.25, label app=api), un Service api-svc port 80, et une NetworkPolicy allow-frontend autorisant uniquement les Pods app=frontend à joindre le Service
Validationkubectl exec frontend -- wget -qO- --timeout=2 api-svc réussit, kubectl exec other -- wget ... échoue
Solution
Fenêtre de terminal
# Deployment api avec label explicite
kubectl create deploy api --image=nginx:1.25 -n cks-exercises
kubectl label deploy api -n cks-exercises app=api
kubectl patch deploy api -n cks-exercises -p '{"spec":{"template":{"metadata":{"labels":{"app":"api"}}}}}'
# Service devant api
kubectl expose deploy api --port=80 --name=api-svc -n cks-exercises
# NetworkPolicy autorisant frontend vers api
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend
namespace: cks-exercises
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 80
EOF
# Pods de test
kubectl run frontend --image=busybox:1.36 --labels="app=frontend" -n cks-exercises -- sleep 3600
kubectl run other --image=busybox:1.36 -n cks-exercises -- sleep 3600
# Validation (attendre que les pods soient Running)
kubectl exec frontend -n cks-exercises -- wget -qO- --timeout=2 api-svc
# doit réussir (ou timeout si deny-all bloque egress de frontend aussi)
kubectl exec other -n cks-exercises -- wget -qO- --timeout=2 api-svc 2>&1
# doit timeout

Point clé : La NetworkPolicy allow-frontend crée une exception à deny-all pour le trafic ingress vers les Pods app=api.


Temps cible : 5 minutes

ContexteNamespace cks-exercises
TâcheCréer un ServiceAccount readonly-sa, un Role pod-reader (get, list sur pods), et lier les deux
Validationkubectl auth can-i list pods -n cks-exercises --as=system:serviceaccount:cks-exercises:readonly-sa retourne yes, delete retourne no
Solution
Fenêtre de terminal
# ServiceAccount
kubectl create sa readonly-sa -n cks-exercises
# Role
kubectl create role pod-reader \
--verb=get,list \
--resource=pods \
-n cks-exercises
# RoleBinding
kubectl create rolebinding readonly-binding \
--role=pod-reader \
--serviceaccount=cks-exercises:readonly-sa \
-n cks-exercises
# Validation
kubectl auth can-i list pods -n cks-exercises \
--as=system:serviceaccount:cks-exercises:readonly-sa
# yes
kubectl auth can-i delete pods -n cks-exercises \
--as=system:serviceaccount:cks-exercises:readonly-sa
# no
kubectl auth can-i list secrets -n cks-exercises \
--as=system:serviceaccount:cks-exercises:readonly-sa
# no

Temps cible : 4 minutes

ContexteServiceAccount readonly-sa créé
TâcheCréer un Pod no-token-pod (nginx:1.25) utilisant readonly-sa avec automountServiceAccountToken: false
Validationkubectl exec no-token-pod -- ls /var/run/secrets/kubernetes.io/serviceaccount retourne "No such file"
Solution
Fenêtre de terminal
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: no-token-pod
namespace: cks-exercises
spec:
serviceAccountName: readonly-sa
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:1.25@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c
EOF
# Validation
kubectl exec no-token-pod -n cks-exercises -- ls /var/run/secrets/kubernetes.io/serviceaccount 2>&1
# No such file or directory
# Double-check dans le YAML
kubectl get pod no-token-pod -n cks-exercises -o yaml | grep automount

Temps cible : 6 minutes

ContexteNamespace cks-exercises
TâcheCréer un Pod hardened-pod (nginx:1.25) avec : runAsNonRoot, runAsUser 1000, allowPrivilegeEscalation false, readOnlyRootFilesystem true, capabilities drop ALL
Contraintenginx nécessite des emptyDir pour /tmp, /var/cache/nginx, /var/run
Validationkubectl exec hardened-pod -- id retourne uid=1000, le Pod est Running
Solution
Fenêtre de terminal
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: hardened-pod
namespace: cks-exercises
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
containers:
- name: nginx
image: nginx:1.25@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
- name: run
mountPath: /var/run
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- name: run
emptyDir: {}
EOF
# Validation
kubectl get pod hardened-pod -n cks-exercises
kubectl exec hardened-pod -n cks-exercises -- id
# uid=1000 gid=1000

Point clé : Certaines images nécessitent des volumes writable pour fonctionner avec readOnlyRootFilesystem: true.


Temps cible : 3 minutes

ContexteNamespace cks-exercises
TâcheCréer un Pod seccomp-pod (nginx:1.25) avec profil Seccomp RuntimeDefault
Validationkubectl get pod seccomp-pod -o jsonpath='{.spec.securityContext.seccompProfile.type}' retourne RuntimeDefault
Solution
Fenêtre de terminal
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: seccomp-pod
namespace: cks-exercises
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginx:1.25@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c
EOF
# Validation
kubectl get pod seccomp-pod -n cks-exercises -o jsonpath='{.spec.securityContext.seccompProfile.type}'
# RuntimeDefault

Temps cible : 5 minutes

ContexteNouveau namespace psa-test
TâcheCréer un namespace psa-test avec PSA enforce=restricted (v1.34), puis tester avec un Pod non-conforme et un Pod conforme
ValidationLe Pod non-conforme est rejeté, le Pod conforme démarre
Solution
Fenêtre de terminal
# Créer namespace avec PSA
kubectl create ns psa-test
kubectl label namespace psa-test \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.34
# Vérifier les labels
kubectl get ns psa-test --show-labels
# Test Pod non-conforme
kubectl run non-compliant --image=nginx:1.25 -n psa-test 2>&1
# Error: violates PodSecurity "restricted:v1.34"
# Pod conforme (tous les critères restricted)
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: compliant-pod
namespace: psa-test
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.36@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
command: ['sh', '-c', 'sleep 3600']
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
EOF
# Validation
kubectl get pod compliant-pod -n psa-test

Point clé : Le niveau restricted exige runAsNonRoot, seccompProfile, capabilities drop ALL, et allowPrivilegeEscalation false.


Temps cible : 4 minutes

ContexteNamespace cks-exercises
TâcheCréer un Secret app-secret (API_KEY=secret123), puis un Pod secret-pod (busybox:1.36) qui monte ce Secret dans /etc/secrets en lecture seule
Validationkubectl exec secret-pod -- cat /etc/secrets/API_KEY retourne secret123, écriture impossible
Solution
Fenêtre de terminal
# Créer le Secret
kubectl create secret generic app-secret \
--from-literal=API_KEY=secret123 \
-n cks-exercises
# Pod avec montage readonly
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
namespace: cks-exercises
spec:
containers:
- name: app
image: busybox:1.36@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
command: ['sh', '-c', 'cat /etc/secrets/API_KEY && sleep 3600']
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secrets
secret:
secretName: app-secret
EOF
# Validation
kubectl exec secret-pod -n cks-exercises -- cat /etc/secrets/API_KEY
# secret123
kubectl exec secret-pod -n cks-exercises -- touch /etc/secrets/test 2>&1
# Read-only file system

Temps cible : 5 minutes

ContexteCluster complet
TâcheLister tous les Pods du cluster qui ont privileged: true ou allowPrivilegeEscalation: true
ValidationCommande fonctionnelle retournant namespace/pod
Solution
Fenêtre de terminal
# Pods avec privileged: true
kubectl get pods -A -o json | jq -r '
.items[] |
select(
.spec.containers[]?.securityContext?.privileged == true
) |
"\(.metadata.namespace)/\(.metadata.name)"
'
# Pods avec allowPrivilegeEscalation: true (ou non défini = true par défaut)
kubectl get pods -A -o json | jq -r '
.items[] |
select(
.spec.containers[]?.securityContext?.allowPrivilegeEscalation == true or
(.spec.containers[]?.securityContext?.allowPrivilegeEscalation == null and .spec.containers[]?.securityContext?.privileged != false)
) |
"\(.metadata.namespace)/\(.metadata.name)"
' | sort -u
# Version simplifiée : pods explicitement privileged
echo "=== Pods privileged ==="
kubectl get pods -A -o json | jq -r '
.items[] |
select(.spec.containers[].securityContext.privileged == true) |
"\(.metadata.namespace)/\(.metadata.name)"
'

Point clé : Les expressions jq peuvent échouer si securityContext est null. Utiliser ? pour gérer les champs optionnels.


Temps cible : 5 minutes

ContexteNamespace cks-exercises
TâcheObtenir le digest SHA256 de nginx:1.25, puis créer un Pod pinned-pod utilisant l'image avec ce digest
Validationkubectl get pod pinned-pod -o jsonpath='{.spec.containers[0].image}' contient @sha256:
Solution
Fenêtre de terminal
# Obtenir le digest (plusieurs méthodes selon les outils disponibles)
# Option 1 : kubectl debug image (si disponible)
kubectl run digest-check --image=nginx:1.25 --rm -it --restart=Never -- cat /dev/null 2>/dev/null
kubectl get pod digest-check -o jsonpath='{.status.containerStatuses[0].imageID}' 2>/dev/null
# Option 2 : crane (si installé)
crane digest nginx:1.25
# Option 3 : skopeo (si installé)
skopeo inspect docker://nginx:1.25 | jq -r '.Digest'
# Option 4 : docker pull + inspect
docker pull nginx:1.25
docker inspect nginx:1.25 --format='{{index .RepoDigests 0}}'
# Digest de nginx:1.25 relevé au 2026-09-14, utilisable tel quel.
# Le vôtre différera si le tag a été republié depuis : c'est tout l'intérêt.
# nginx@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pinned-pod
namespace: cks-exercises
spec:
containers:
- name: nginx
# Remplacez par le digest que vous venez d'obtenir
image: nginx@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c
EOF
# Validation
kubectl get pod pinned-pod -n cks-exercises -o jsonpath='{.spec.containers[0].image}'
# Doit contenir @sha256:

Point clé : Un digest garantit l'immutabilité de l'image, contrairement aux tags qui peuvent être écrasés.


Temps cible : 5 minutes

ContexteCluster complet
TâcheLister toutes les images du cluster utilisant le tag :latest ou sans tag (donc implicitement latest)
ValidationCommande fonctionnelle retournant namespace/pod: image
Solution
Fenêtre de terminal
# Images avec :latest ou sans tag
kubectl get pods -A -o json | jq -r '
.items[] |
. as $pod |
.spec.containers[] |
select(.image | (test(":latest$") or (test(":") | not))) |
"\($pod.metadata.namespace)/\($pod.metadata.name): \(.image)"
'
# Alternative avec initContainers aussi
kubectl get pods -A -o json | jq -r '
.items[] |
. as $pod |
(.spec.containers + (.spec.initContainers // []))[] |
select(.image | (test(":latest$") or (test(":") | not))) |
"\($pod.metadata.namespace)/\($pod.metadata.name): \(.image)"
' | sort -u

Recommandation : Ces images présentent un risque de dérive. Préférer des tags versionnés ou des digests.


Temps cible : 8 minutes

ContextePod insecure-app avec problèmes de sécurité
TâcheIdentifier les problèmes et créer une version sécurisée secure-app
Validationkubectl get pod secure-app Running, tous les SecurityContext restrictifs appliqués

Créez d'abord le Pod problématique :

Fenêtre de terminal
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: insecure-app
namespace: cks-exercises
spec:
containers:
- name: app
image: nginx:1.31.3@sha256:5a88c9c45479443d7be2eadc894b4ed0a9801bae03d97a5760ae13b5c2005942
securityContext:
privileged: true
EOF
Solution

Problèmes identifiés :

  1. image: nginx:latest, tag non-immutable
  2. privileged: true, accès root complet au host
  3. Pas de runAsNonRoot
  4. Pas de readOnlyRootFilesystem
  5. Capabilities non supprimées
  6. Pas de profil Seccomp

Version sécurisée :

Fenêtre de terminal
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: cks-exercises
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.25@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c
securityContext:
privileged: false
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
- name: run
mountPath: /var/run
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- name: run
emptyDir: {}
EOF
# Validation
kubectl get pod secure-app -n cks-exercises
kubectl get pod secure-app -n cks-exercises -o jsonpath='{.spec.securityContext}'

Temps cible : 10 minutes

ContexteNouveau namespace production
TâcheCréer production avec : (1) PSA restricted enforced, (2) NetworkPolicy deny-all, (3) SA app-sa sans automount, (4) Role lecture ConfigMaps, (5) Pod conforme utilisant ce SA
Validationkubectl auth can-i list configmaps -n production --as=system:serviceaccount:production:app-sa = yes, secrets = no
Solution
Fenêtre de terminal
# 1. Namespace avec PSA
kubectl create ns production
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.34
# 2. NetworkPolicy deny-all
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
EOF
# 3. ServiceAccount sans automount
kubectl create sa app-sa -n production
kubectl patch sa app-sa -n production -p '{"automountServiceAccountToken": false}'
# 4. Role ConfigMap reader + binding
kubectl create role configmap-reader \
--verb=get,list \
--resource=configmaps \
-n production
kubectl create rolebinding app-configmap-binding \
--role=configmap-reader \
--serviceaccount=production:app-sa \
-n production
# 5. Pod conforme PSA restricted
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: prod-app
namespace: production
spec:
serviceAccountName: app-sa
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.36@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
command: ['sh', '-c', 'sleep 3600']
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
EOF
# Validation
kubectl get ns production --show-labels
kubectl get netpol -n production
kubectl auth can-i list configmaps -n production --as=system:serviceaccount:production:app-sa
# yes
kubectl auth can-i list secrets -n production --as=system:serviceaccount:production:app-sa
# no
kubectl get pod prod-app -n production

Temps cible : 6 minutes

ContexteNamespace cks-exercises
TâcheCréer un Deployment web (nginx:1.25), un Service web-svc, un Secret TLS auto-signé pour secure.example.com, et un Ingress utilisant ce Secret
Validationkubectl describe ingress secure-ingress montre le TLS configuré
Solution
Fenêtre de terminal
# Backend
kubectl create deploy web --image=nginx:1.25 -n cks-exercises
kubectl expose deploy web --port=80 --name=web-svc -n cks-exercises
# Certificat auto-signé
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/tls.key -out /tmp/tls.crt \
-subj "/CN=secure.example.com"
# Secret TLS
kubectl create secret tls secure-tls \
--cert=/tmp/tls.crt \
--key=/tmp/tls.key \
-n cks-exercises
# Ingress
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-ingress
namespace: cks-exercises
spec:
ingressClassName: nginx
tls:
- hosts:
- secure.example.com
secretName: secure-tls
rules:
- host: secure.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80
EOF
# Validation
kubectl describe ingress secure-ingress -n cks-exercises | grep -A3 TLS
# Nettoyage fichiers temporaires
rm /tmp/tls.key /tmp/tls.crt

Exercice 15 : Identifier ClusterRoleBindings cluster-admin

Section intitulée « Exercice 15 : Identifier ClusterRoleBindings cluster-admin »

Temps cible : 5 minutes

ContexteCluster complet
TâcheLister tous les ServiceAccounts liés à un ClusterRoleBinding vers cluster-admin
ValidationCommande retournant namespace/sa
Solution
Fenêtre de terminal
# Méthode 1 : jq complet
kubectl get clusterrolebindings -o json | jq -r '
.items[] |
select(.roleRef.name == "cluster-admin") |
.subjects[]? |
select(.kind == "ServiceAccount") |
"\(.namespace)/\(.name)"
'
# Méthode 2 : grep simple pour identifier rapidement
kubectl get clusterrolebindings -o wide | grep cluster-admin
# Méthode 3 : détail par binding
kubectl get clusterrolebindings -o json | jq -r '
.items[] |
select(.roleRef.name == "cluster-admin") |
"Binding: \(.metadata.name)\nSubjects: \(.subjects // [] | map("\(.kind)/\(.namespace // "cluster")/\(.name)") | join(", "))\n"
'

Point clé : Les ServiceAccounts avec cluster-admin ont des droits très élevés. Auditez régulièrement.


Ces exercices nécessitent des outils supplémentaires. Ils sont moins universels mais pertinents pour la préparation.


Temps cible : 4 minutes

ContexteTrivy installé
TâcheScanner nginx:1.24 et lister les vulnérabilités CRITICAL et HIGH
ValidationSortie Trivy avec tableau des vulnérabilités
Solution
Fenêtre de terminal
# Scan avec filtre severity
trivy image --severity HIGH,CRITICAL nginx:1.24
# Scan en mode quiet (tableau uniquement)
trivy image --severity HIGH,CRITICAL --format table nginx:1.24
# Compter les vulnérabilités (attention : résultats variables selon la date)
trivy image --severity CRITICAL --format json nginx:1.24 | jq '[.Results[]?.Vulnerabilities // [] | length] | add'

Note : Le nombre de vulnérabilités varie selon la base CVE du jour. L'objectif est de savoir utiliser Trivy, pas d'obtenir un compte exact.


Exercice 17 : Analyser les logs d'audit Kubernetes

Section intitulée « Exercice 17 : Analyser les logs d'audit Kubernetes »

Temps cible : 6 minutes

ContexteAccès aux logs d'audit du control plane
TâcheFiltrer les créations de Secrets des 10 dernières minutes avec l'utilisateur associé
ContrainteExercice dépendant d'un cluster avec audit logs activés (path typique : /var/log/kubernetes/audit/audit.log)
ValidationListe timestamp/user/namespace/secret
Solution
Fenêtre de terminal
# Sur le control plane (ssh si nécessaire)
AUDIT_LOG="/var/log/kubernetes/audit/audit.log"
# Vérifier que le fichier existe
ls -la $AUDIT_LOG
# Filtrer créations de secrets
cat $AUDIT_LOG | jq -r '
select(
.verb == "create" and
.objectRef.resource == "secrets"
) |
{
timestamp: .requestReceivedTimestamp,
user: .user.username,
namespace: .objectRef.namespace,
name: .objectRef.name
}
'
# Avec filtre temporel (dernières 10 minutes)
SINCE=$(date -d "10 minutes ago" -Iseconds 2>/dev/null || date -v-10M -Iseconds)
cat $AUDIT_LOG | jq --arg since "$SINCE" -r '
select(
.verb == "create" and
.objectRef.resource == "secrets" and
.requestReceivedTimestamp >= $since
) |
"\(.requestReceivedTimestamp) \(.user.username) \(.objectRef.namespace)/\(.objectRef.name)"
'

Point clé : L'audit logging doit être configuré sur l'API server. Les paths varient selon l'installation.


Temps cible : 8 minutes

ContexteNamespace cks-exercises avec plusieurs Pods
TâcheProduire une liste des Pods sans SecurityContext restrictif (privileged, no readOnlyRootFilesystem, allowPrivilegeEscalation true)
ValidationListe namespace/pod avec le problème identifié
Solution
Fenêtre de terminal
NS="cks-exercises"
echo "=== Pods sans SecurityContext restrictif dans $NS ==="
# Pods privileged
echo "-- Privileged:"
kubectl get pods -n $NS -o json | jq -r '
.items[] |
select(.spec.containers[]?.securityContext?.privileged == true) |
.metadata.name
'
# Pods sans readOnlyRootFilesystem
echo "-- Sans readOnlyRootFilesystem:"
kubectl get pods -n $NS -o json | jq -r '
.items[] |
select(.spec.containers[]?.securityContext?.readOnlyRootFilesystem != true) |
.metadata.name
' | sort -u
# Pods avec allowPrivilegeEscalation (explicite ou implicite)
echo "-- Avec allowPrivilegeEscalation possible:"
kubectl get pods -n $NS -o json | jq -r '
.items[] |
select(.spec.containers[]?.securityContext?.allowPrivilegeEscalation != false) |
.metadata.name
' | sort -u
# NetworkPolicies en place
echo ""
echo "=== NetworkPolicies actives ==="
kubectl get netpol -n $NS

Ces exercices sont plus spécialisés et dépendent d'outils runtime (Falco, kube-bench). Utiles pour approfondir, moins prioritaires pour l'examen.


Exercice 19 : Détection shell avec audit logs (sans Falco)

Section intitulée « Exercice 19 : Détection shell avec audit logs (sans Falco) »

Temps cible : 5 minutes

ContexteAudit logs activés
TâcheUtiliser les audit logs pour détecter les kubectl exec vers des Pods
ValidationTrouver les exec récents dans les logs
Solution
Fenêtre de terminal
# Filtrer les exec dans les audit logs
cat /var/log/kubernetes/audit/audit.log | jq -r '
select(
.verb == "create" and
.objectRef.subresource == "exec"
) |
"\(.requestReceivedTimestamp) \(.user.username) exec into \(.objectRef.namespace)/\(.objectRef.name)"
'
# Alternative : si responseStatus est loggué
cat /var/log/kubernetes/audit/audit.log | jq -r '
select(.objectRef.subresource == "exec") |
"\(.requestReceivedTimestamp) \(.verb) \(.user.username) → \(.objectRef.namespace)/\(.objectRef.name)"
'

Point clé : La détection d'exec peut aussi se faire avec Falco, mais les audit logs sont toujours disponibles si configurés.


Temps cible : 6 minutes

Contextekube-bench installé, accès control plane
TâcheExécuter kube-bench sur le control plane et identifier les checks FAIL de la section 1.1 (API Server)
ValidationListe des checks FAIL avec leurs IDs
Solution
Fenêtre de terminal
# Exécuter kube-bench (sur control plane)
kube-bench run --targets master
# Filtrer la section 1.1
kube-bench run --targets master --check "1.1" 2>/dev/null | grep -E "\[FAIL\]|\[WARN\]"
# Exemple de sortie :
# [FAIL] 1.1.1 Ensure that the API server pod specification file permissions are set to 600 or more restrictive
# [FAIL] 1.1.2 Ensure that the API server pod specification file ownership is set to root:root
# Vérification manuelle d'un check
ls -la /etc/kubernetes/manifests/kube-apiserver.yaml

Note : Les remédiations kube-bench doivent être appliquées avec discernement. Certaines peuvent casser le cluster. Cet exercice est d'analyse, pas de correction automatique.


Capstone : ouvrir une enclave à une équipe qui n'est pas de confiance

Section intitulée « Capstone : ouvrir une enclave à une équipe qui n'est pas de confiance »

Les exercices ci-dessus durcissent un mécanisme à la fois. Le capstone pose une situation entière : une équipe extérieure a besoin de place dans votre cluster, et il faut lui donner un espace qui reste sûr même si elle ne l'est pas. Le cluster doit refuser de lui-même ce qu'elle ne devrait pas déployer, son identité ne doit presque rien pouvoir, et rien hors de l'enclave ne doit atteindre ce qui y tourne.

Comptez 45 minutes, et traitez-le en dernier : il mobilise l'admission, le RBAC et l'isolation réseau ensemble, alors que chacun a son exercice séparé plus haut.

#ExerciceBlocTempsIndépendant
1NetworkPolicy Deny AllCKS cœur4 min✅
2NetworkPolicy Allow SpecificCKS cœur6 min⚠️ Ex.1
3RBAC lecture seuleCKS cœur5 min✅
4Désactiver automount tokenCKS cœur4 min⚠️ Ex.3
5SecurityContext restrictifCKS cœur6 min✅
6Seccomp RuntimeDefaultCKS cœur3 min✅
7Pod Security AdmissionCKS cœur5 min✅
8Secret volume readonlyCKS cœur4 min✅
9Identifier pods privilégiésCKS cœur5 min✅
10Image pinned by digestCKS cœur5 min✅
11Images non-immutablesCKS cœur5 min✅
12Sécuriser Pod existantCKS cœur8 min✅
13Isolation namespaceCKS cœur10 min✅
14Ingress TLSCKS cœur6 min✅
15ClusterRoleBindings cluster-adminCKS cœur5 min✅
16Trivy scanOutils4 min✅
17Audit logsOutils6 min⚠️ Env
18Audit sécurité simplifiéOutils8 min✅
19Détection exec (audit)Avancé5 min⚠️ Env
20kube-bench analyseAvancé6 min⚠️ Env

Temps total Bloc 1 (CKS cœur) : ~81 minutes Temps total complet : ~110 minutes

L'examen CKS dure 2 heures (120 min). Concentrez-vous sur le Bloc 1 pour un entraînement réaliste. Les Blocs 2 et 3 permettent d'approfondir après avoir maîtrisé le cœur.


Fenêtre de terminal
kubectl delete ns cks-exercises
kubectl delete ns psa-test 2>/dev/null
kubectl delete ns production 2>/dev/null

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  1. NetworkPolicy deny-all : première étape de tout namespace de production
  2. RBAC minimal : Role (namespace) vs ClusterRole (cluster), toujours kubectl auth can-i pour vérifier
  3. SecurityContext : runAsNonRoot, allowPrivilegeEscalation: false, readOnlyRootFilesystem, capabilities drop ALL
  4. Seccomp RuntimeDefault : profil standard sécurisé pour la plupart des workloads
  5. Pod Security Admission : labels namespace pour enforcer les standards restricted/baseline
  6. automountServiceAccountToken: false : désactiver sauf si nécessaire
  7. Images immutables : préférer digest, éviter :latest
  8. Audit : kubectl auth can-i --list, kubectl get pods -o json | jq

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens ce site gratuitement, sans publicité, sans profilage et sans compte à créer. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn