Aller au contenu
Conteneurs & Orchestration medium

Cheatsheet CKS : commandes sécurité Kubernetes

20 min de lecture

logo kubernetes

Cheatsheet sécurité CKS. Examen 100% pratique orienté sécurisation de clusters Kubernetes. Cette page couvre les commandes essentielles, organisées par domaine d'examen.

Ce tableau est le point d'entrée à consulter pendant l'épreuve : vous partez de l'énoncé, vous y trouvez la commande, les sections suivantes en donnent la variante complète. Le k des exemples est l'alias de kubectl défini juste après. Toutes les commandes supposent que vous avez déjà basculé sur le bon cluster.

BesoinCommande
Bloquer tout trafic entrantk apply -f deny-all-ingress.yaml
Créer un Role pour lire les podsk create role pod-reader --verb=get,list --resource=pods
Tester les permissions d'un SAk auth can-i get pods --as=system:serviceaccount:ns:sa
Appliquer PSA restrictedk label ns <ns> pod-security.kubernetes.io/enforce=restricted
Scanner une imagetrivy image --severity CRITICAL,HIGH nginx:1.25
Audit CIS Benchmarkkube-bench run --targets master
Voir les audit logscat /var/log/kubernetes/audit.log | jq 'select(.verb=="create")'
Identifier les alertes Falcotail -f /var/log/falco/falco.log
Vérifier le chiffrement etcdcat /etc/kubernetes/manifests/kube-apiserver.yaml | grep encryption
Obtenir le digest d'une imageskopeo inspect docker://nginx:1.25 | jq -r '.Digest'

Ces quatre lignes se tapent en trente secondes au début de l'examen et se rentabilisent sur chaque question. La variable $do abrège --dry-run=client -o yaml, la combinaison qui génère un manifeste sans rien créer : c'est le raccourci le plus rentable de l'épreuve, puisque presque toutes les ressources de sécurité se rédigent en YAML. L'autocomplétion doit être rattachée explicitement à l'alias, sinon elle ne fonctionne que sur kubectl.

Fenêtre de terminal
# Alias fondamentaux
alias k=kubectl
alias kn='kubectl config set-context --current --namespace'
export do="--dry-run=client -o yaml"
# Autocomplétion
source <(kubectl completion bash)
complete -F __start_kubectl k
# Vérifier le contexte
kubectl config current-context

Ce domaine porte sur la configuration initiale du cluster : isolation réseau, conformité aux référentiels et exposition en TLS. Les NetworkPolicy y occupent la place principale, avec une logique constante à l'examen : refuser tout par défaut, puis rouvrir précisément ce dont l'application a besoin. Attention, une NetworkPolicy n'a d'effet que si le plugin réseau du cluster les implémente.

Ce manifeste sélectionne tous les Pods du namespace et ne déclare aucune règle entrante, ce qui coupe tout trafic reçu.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress

But : Bloquer tout trafic entrant vers les Pods du namespace. Piège : podSelector: {} = tous les Pods. Sans règle ingress, tout est bloqué.

Même principe dans l'autre sens : les Pods ne peuvent plus initier aucune connexion sortante, y compris vers les services internes du cluster.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress

Piège : Bloque aussi le DNS ! Ajoutez une règle autorisant le port 53 vers CoreDNS si nécessaire.

Une fois le refus global posé, chaque flux légitime se rouvre par une politique dédiée : ici, seuls les Pods portant le label app: frontend atteignent le port 8080 du backend.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080

kube-bench exécute les contrôles du CIS Benchmark sur un nœud et classe chaque test en PASS, FAIL ou WARN. La cible s'indique explicitement, les contrôles du control plane et ceux des nœuds de travail étant différents.

Fenêtre de terminal
kube-bench run --targets master
kube-bench run --targets node
kube-bench run --json > cis-results.json

But : Vérifier la conformité CIS Benchmark. Piège : Les résultats FAIL nécessitent souvent d'éditer /etc/kubernetes/manifests/*.yaml.

Un Secret de type tls attend exactement deux clés, tls.crt et tls.key : c'est le format qu'exigent les Ingress pour terminer le chiffrement.

Fenêtre de terminal
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt -subj "/CN=app.example.com"
kubectl create secret tls app-tls --cert=tls.crt --key=tls.key -n production

Ce domaine traite du durcissement de l'accès à l'API Kubernetes : qui peut faire quoi, avec quelle identité, et ce que les Pods eux-mêmes sont autorisés à demander. Le RBAC en constitue l'essentiel, complété par la maîtrise des ServiceAccounts et par la vérification des paramètres de démarrage de l'API server. La règle qui guide toutes les réponses est le moindre privilège.

Un Role accorde des verbes sur des ressources dans un seul namespace ; sa contrepartie ClusterRole porte les mêmes droits à l'échelle du cluster et couvre les ressources sans namespace comme les nœuds.

Fenêtre de terminal
k create role pod-reader --verb=get,list,watch --resource=pods -n production
k create clusterrole node-viewer --verb=get,list --resource=nodes

But : Limiter les permissions au strict nécessaire. Piège : Role = namespaced, ClusterRole = cluster-scoped.

Un rôle ne produit aucun effet tant qu'aucune liaison ne le rattache à une identité : c'est le RoleBinding qui active réellement les permissions.

Fenêtre de terminal
k create rolebinding app-pod-reader \
--role=pod-reader \
--serviceaccount=production:app-sa \
-n production

Format SA : namespace:nom

L'option --as fait évaluer la requête par l'API server sous une autre identité, sans avoir à récupérer le moindre token : c'est la façon la plus rapide de prouver qu'une correction RBAC a bien pris effet.

Fenêtre de terminal
k auth can-i create pods -n production --as=system:serviceaccount:production:app-sa
k auth can-i --list --as=system:serviceaccount:production:app-sa -n production

But : Vérifier qu'un SA n'a pas trop de droits. Piège : Toujours tester avec --as= avant de valider.

Le rôle cluster-admin donne tous les droits sur tout : lister ses bénéficiaires est le premier audit à mener sur un cluster inconnu.

Fenêtre de terminal
# Qui a cluster-admin ?
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") | {name: .metadata.name, subjects: .subjects}'

Kubernetes monte par défaut un jeton d'accès à l'API dans chaque Pod : une application qui n'appelle pas l'API n'en a pas besoin, et ce jeton devient une clé offerte à qui obtient un shell dans le conteneur.

apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
serviceAccountName: restricted-sa
automountServiceAccountToken: false
containers:
- name: app
image: nginx:1.25@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c

But : Empêcher le Pod d'appeler l'API Kubernetes. Piège : Par défaut, le token est monté automatiquement.

L'API server tourne en Pod statique : sa configuration se lit et se corrige directement dans le manifeste du nœud, et le kubelet le redémarre dès que le fichier change.

Fenêtre de terminal
cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep -E "anonymous-auth|authorization-mode|admission-plugins"

Flags sécurisés :

  • --anonymous-auth=false
  • --authorization-mode=Node,RBAC
  • --enable-admission-plugins=NodeRestriction

Ce domaine descend au niveau du noyau : il s'agit de restreindre ce qu'un conteneur peut demander au système d'exploitation. Seccomp filtre les appels système, AppArmor restreint l'accès aux fichiers et aux capacités, le securityContext rassemble les garde-fous Kubernetes. Ces trois mécanismes se cumulent et n'ont aucune valeur par défaut protectrice : rien n'est appliqué tant que vous ne le déclarez pas.

Le profil RuntimeDefault applique la liste d'appels système bloqués fournie par le moteur de conteneurs, ce qui couvre l'essentiel du risque sans avoir à écrire un profil sur mesure.

apiVersion: v1
kind: Pod
metadata:
name: seccomp-pod
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.25@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c

But : Filtrer les syscalls dangereux (ptrace, mount, etc.). Piège : Sans Seccomp, le conteneur peut exécuter n'importe quel syscall.

Le profil doit être déjà chargé sur le nœud avant la création du Pod, sinon celui-ci reste bloqué. Depuis Kubernetes 1.30, le champ appArmorProfile du securityContext remplace cette annotation, encore acceptée mais dépréciée.

apiVersion: v1
kind: Pod
metadata:
name: apparmor-pod
annotations:
container.apparmor.security.beta.kubernetes.io/app: localhost/k8s-nginx
spec:
containers:
- name: app
image: nginx:1.25@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c

Format : container.apparmor.security.beta.kubernetes.io/<container-name>: localhost/<profile-name>

Fenêtre de terminal
# Vérifier les profils chargés
aa-status
cat /sys/kernel/security/apparmor/profiles

Ce manifeste réunit tout ce qu'attend le niveau restricted de Pod Security Admission : retenez-le tel quel, il sert de base à la plupart des questions de durcissement. Notez que le securityContext existe à deux étages, celui du Pod et celui du conteneur, et que les champs ne sont pas interchangeables entre les deux.

apiVersion: v1
kind: Pod
metadata:
name: hardened-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.25@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}

Champs à mémoriser :

  • runAsNonRoot: true
  • allowPrivilegeEscalation: false
  • readOnlyRootFilesystem: true
  • capabilities.drop: ["ALL"]

Un conteneur privileged dispose de presque tous les droits du nœud : c'est le premier motif à rechercher quand un énoncé demande de repérer les Pods dangereux d'un cluster.

Fenêtre de terminal
# Pods privilégiés
kubectl get pods -A -o json | jq '.items[] | select(.spec.containers[].securityContext.privileged == true) | .metadata.namespace + "/" + .metadata.name'
# Pods root
kubectl get pods -A -o json | jq '.items[] | select(.spec.securityContext.runAsNonRoot != true) | .metadata.namespace + "/" + .metadata.name'

Ce domaine consiste à empêcher qu'une configuration dangereuse entre dans le cluster, plutôt qu'à la corriger après coup. Pod Security Admission est le mécanisme natif qui refuse les Pods non conformes à l'admission, OPA Gatekeeper couvre les règles que PSA ne sait pas exprimer, et le chiffrement d'etcd protège les Secrets stockés. Ces contrôles s'appliquent au moment de la création : ils ne modifient pas les ressources déjà en place.

PSA se pilote uniquement par des labels de namespace : aucune ressource supplémentaire à créer, et l'étiquette de version fige le référentiel appliqué.

Fenêtre de terminal
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest

Niveaux : privileged (aucune restriction) < baseline < restricted (strict). Modes : enforce (bloque), warn (avertit), audit (log).

Le --dry-run=server soumet la demande à l'API server et déclenche donc réellement les contrôles d'admission, contrairement au --dry-run=client qui s'arrête sur votre poste.

Fenêtre de terminal
kubectl run test --image=nginx -n production --dry-run=server

Sortie si enforce=restricted : Error from server (Forbidden): violates PodSecurity "restricted:latest"

Le chiffrement au repos s'active par un fichier EncryptionConfiguration référencé dans les paramètres de l'API server ; sans lui, quiconque lit la base etcd lit vos Secrets.

Fenêtre de terminal
# Vérifier si configuré
cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep encryption
cat /etc/kubernetes/enc/enc.yaml

Piège : Les Secrets sont encodés base64 par défaut, pas chiffrés !

Gatekeeper fonctionne en deux temps : un ConstraintTemplate définit la règle en langage Rego, une Constraint l'applique à un périmètre donné. Ce sont ces contraintes que la commande ci-dessous liste.

Fenêtre de terminal
kubectl get constraints
kubectl describe constraint no-latest-tag

But : Politiques d'admission personnalisées (bloquer tag latest, etc.).


Ce domaine porte sur ce que vous déployez, avant même que cela tourne : d'où vient l'image, quelles vulnérabilités elle contient, et comment garantir que celle qui démarre est bien celle que vous avez auditée. Trivy répond à la deuxième question, le digest à la troisième. Les questions d'examen combinent presque toujours un scan et une correction de manifeste.

Le scan lit les paquets installés dans l'image et les compare aux bases de vulnérabilités publiques ; filtrer sur les sévérités hautes évite de se noyer dans des centaines de résultats sans portée pratique.

Fenêtre de terminal
trivy image nginx:1.25
trivy image --severity CRITICAL,HIGH nginx:1.25
trivy image --ignore-unfixed nginx:1.25
trivy image --format json -o results.json nginx:1.25

But : Détecter les CVE avant déploiement. Piège : --ignore-unfixed = montre uniquement les CVE avec patch disponible.

Le mode k8s interroge l'API pour découvrir les images en cours d'exécution et les analyse toutes, ce qui donne une vue d'ensemble sans lister les Pods à la main.

Fenêtre de terminal
trivy k8s --report summary cluster

Cette boucle est la solution de repli quand trivy k8s n'est pas disponible : le sort -u évite de scanner dix fois la même image partagée par plusieurs Pods.

Fenêtre de terminal
for img in $(kubectl get pods -A -o jsonpath='{.items[*].spec.containers[*].image}' | tr ' ' '\n' | sort -u); do
echo "=== $img ==="
trivy image --severity CRITICAL --quiet "$img"
done

Un tag peut être réattribué à tout moment à un contenu différent ; le digest est l'empreinte du contenu lui-même et désigne donc toujours la même image.

Fenêtre de terminal
skopeo inspect docker://nginx:1.25 | jq -r '.Digest'

Usage dans un Pod :

image: nginx@sha256:6db391d1c0cfb30588ba0bf72ea999404f2764febf0f1f196acd5867ac7efa7e

But : Image immuable, protège contre le tag poisoning.

L'expression régulière retient les images dont la référence se termine par un tag sans @sha256, autrement dit celles dont le contenu peut changer sous vos pieds.

Fenêtre de terminal
kubectl get pods -A -o json | jq '.items[] | select(.spec.containers[].image | test(":[^@]+$")) | .metadata.namespace + "/" + .metadata.name'

Ce dernier domaine porte sur ce qui se passe une fois les conteneurs démarrés : tracer les appels à l'API, détecter les comportements anormaux et réduire la surface d'attaque à l'exécution. Les audit logs répondent à la question « qui a fait quoi », Falco à la question « que se passe-t-il d'anormal maintenant ». Les deux se lisent depuis le nœud, pas depuis kubectl.

Chaque ligne du journal d'audit est un objet JSON complet : jq est donc l'outil de lecture, et le filtre porte selon les cas sur le verbe, l'utilisateur ou le code de réponse.

Fenêtre de terminal
# Créations de Secrets
cat /var/log/kubernetes/audit.log | jq 'select(.verb=="create" and .objectRef.resource=="secrets")'
# Actions d'un utilisateur
cat /var/log/kubernetes/audit.log | jq 'select(.user.username=="suspect")'
# Échecs
cat /var/log/kubernetes/audit.log | jq 'select(.responseStatus.code >= 400)'

But : Investigation post-incident.

Les règles sont évaluées dans l'ordre et la première qui correspond fixe le niveau retenu : l'ordre des entrées fait donc partie de la réponse attendue, une exclusion placée après une règle plus large n'a aucun effet.

/etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
- level: RequestResponse
resources:
- group: ""
resources: ["pods"]
- level: None
users: ["system:kube-proxy"]

Niveaux : None < Metadata < Request < RequestResponse

Falco surveille les appels système du nœud et déclenche une alerte dès qu'un comportement correspond à l'une de ses règles ; il rapporte, il ne bloque pas.

Fenêtre de terminal
tail -f /var/log/falco/falco.log
grep "Critical\|Error\|Warning" /var/log/falco/falco.log
systemctl status falco

But : Détection runtime (shell dans conteneur, accès fichiers sensibles, etc.).

Un système de fichiers en lecture seule empêche l'installation d'un outil ou la modification d'un binaire après une intrusion ; il faut en contrepartie ouvrir explicitement les répertoires temporaires dont l'application a besoin.

securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}

But : L'attaquant ne peut pas modifier les binaires. Piège : L'application doit écrire quelque part → ajouter des volumes emptyDir.


Ces commandes doivent sortir sans réflexion : chaque hésitation coûte des minutes sur une épreuve de deux heures. Elles sont volontairement écrites en forme impérative, sans passer par un fichier YAML, car c'est la manière la plus rapide de créer une ressource simple. Les valeurs entre chevrons sont à remplacer par celles de l'énoncé.

Fenêtre de terminal
# NetworkPolicy
k get netpol -A
k apply -f deny-all.yaml
# RBAC
k create role <name> --verb=get,list --resource=pods
k create rolebinding <name> --role=<role> --serviceaccount=<ns>:<sa>
k auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa>
# PSA
k label ns <ns> pod-security.kubernetes.io/enforce=restricted
# Scan
trivy image --severity CRITICAL,HIGH <image>
kube-bench run --targets master
# Audit/Falco
cat /var/log/kubernetes/audit.log | jq '...'
tail -f /var/log/falco/falco.log
# AppArmor/Seccomp
aa-status
ls /var/lib/kubelet/seccomp/

Ne perdez pas de temps à mémoriser, utilisez kubectl explain :

  • Syntaxe complète NetworkPolicy (k explain networkpolicy.spec)
  • Champs securityContext (k explain pod.spec.securityContext)
  • Format EncryptionConfiguration etcd
  • Options ImagePolicyWebhook
  • Règles Falco personnalisées
  • Policy Gatekeeper ConstraintTemplate (syntaxe Rego)

Ces six situations reviennent en salle d'examen et ont toutes la même cause profonde : une configuration acceptée par l'API mais sans effet réel. La commande indiquée sert à le confirmer en quelques secondes, avant de repartir sur une fausse piste.

ProblèmeCommande
Pod rejeté par PSAk run test --dry-run=server
NetworkPolicy inactivek describe netpol <name>
Audit logs videsgrep audit /etc/kubernetes/manifests/kube-apiserver.yaml
Falco silencieuxsystemctl status falco
API Server downjournalctl -u kubelet + vérifier YAML
kube-bench FAILkube-bench run --targets master

  1. NetworkPolicies : deny-all d'abord, whitelist ensuite
  2. RBAC : moindre privilège, jamais cluster-admin sauf pour admins
  3. ServiceAccounts : automountServiceAccountToken: false par défaut
  4. SecurityContext : drop ALL, readOnlyRootFilesystem: true, runAsNonRoot: true
  5. PSA : restricted sur les namespaces de production
  6. Secrets : chiffrement etcd obligatoire, RBAC strict
  7. Trivy : scanner CRITICAL/HIGH avant déploiement
  8. Digests : utiliser @sha256:... pour images immuables
  9. Audit logs : tracer les actions pour investigation
  10. Falco : surveiller les alertes runtime en continu

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracking. 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