Aller au contenu
Conteneurs & Orchestration medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

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

35 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
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
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
EOF
# Validation
kubectl get pod seccomp-pod -n cks-exercises -o jsonpath='{.spec.securityContext.seccompProfile.type}'
# RuntimeDefault

Exercice 7 : Pod Security Admission — restricted

Section intitulée « Exercice 7 : Pod Security Admission — restricted »

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 -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
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
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}}'
# Exemple de digest (à remplacer par le vrai)
# nginx@sha256:a8281ce42034b078dc8d0abf0d3ec56a9e5c41a3f8a95a5a8c8e12b7a3a0d1e5
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pinned-pod
namespace: cks-exercises
spec:
containers:
- name: nginx
# Remplacer par le digest réel obtenu ci-dessus
image: nginx@sha256:a8281ce42034b078dc8d0abf0d3ec56a9e5c41a3f8a95a5a8c8e12b7a3a0d1e5
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:latest
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
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}'

Exercice 13 : Isolation complète d’un namespace

Section intitulée « Exercice 13 : Isolation complète d’un namespace »

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
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.


#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

  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 +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn