diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c62b8d2 Binary files /dev/null and b/.DS_Store differ diff --git a/partie-01-installation/.DS_Store b/partie-01-installation/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/partie-01-installation/.DS_Store differ diff --git a/partie-01-installation/01-prereqs.sh b/partie-01-installation/01-prereqs.sh index 67940f0..f5ea67b 100755 --- a/partie-01-installation/01-prereqs.sh +++ b/partie-01-installation/01-prereqs.sh @@ -111,6 +111,28 @@ fs.inotify.max_user_watches = 524288 fs.inotify.max_user_instances = 512 EOF +# Hardening kernel (defense in depth pour le Kube Battle) +# POURQUOI: +# kptr_restrict=2 : masque les adresses kernel dans /proc/kallsyms (anti-leak) +# dmesg_restrict=1 : dmesg réservé à root (évite leak d'infos kernel) +# ptrace_scope=2 : ptrace réservé à root (bloque attaques mémoire inter-process) +# bpf_jit_harden=2 : JIT BPF durci contre les attaques Spectre +# protected_* : durcit les liens symboliques et FIFO dans /tmp +# NOTE: unprivileged_bpf_disabled=1 est DÉLIBÉRÉMENT omis car Cilium en a besoin. +echo "Configuration sysctl hardening kernel..." +cat < /dev/null echo "Activation du driver cgroup systemd pour containerd..." sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml +# Forcer le profil seccomp par défaut sur tous les containers +# POURQUOI: Sans cette option, les pods démarrent en seccomp "Unconfined" — tous les +# syscalls sont permis, y compris ceux utilisés pour l'évasion de container +# (keyctl, add_key, bpf, perf_event_open, etc.). +# Avec "runtime/default", containerd applique le profil seccomp standard +# qui bloque ~50 syscalls dangereux. Un pod qui en a besoin doit explicitement +# demander securityContext.seccompProfile.type=Unconfined (bloqué par PSA). +echo "Configuration du profil seccomp par défaut dans containerd..." +if ! grep -q 'unset_seccomp_profile' /etc/containerd/config.toml; then + sudo sed -i '/\[plugins\."io\.containerd\.grpc\.v1\.cri"\]/a\ unset_seccomp_profile = "runtime/default"' /etc/containerd/config.toml +fi + sudo systemctl restart containerd sudo systemctl enable containerd @@ -208,6 +242,40 @@ fi sudo systemctl enable kubelet +# Installation et configuration d'auditd +# POURQUOI: Lors du Kube Battle, on veut savoir QUI a fait QUOI sur les VMs. +# auditd trace les execve, l'écriture de fichiers sensibles, les changements +# de configuration K8s. Indispensable pour le post-mortem et la détection +# d'évasion de container (un syscall depuis l'hôte = trace). +echo "Installation d'auditd..." +sudo dnf install -y audit +sudo systemctl enable --now auditd + +sudo tee /etc/audit/rules.d/k8s.rules > /dev/null <<'EOF' +-w /etc/kubernetes/ -p wa -k k8s-config +-w /var/lib/kubelet/ -p wa -k kubelet-data +-w /var/lib/etcd/ -p wa -k etcd-data +-w /etc/containerd/ -p wa -k containerd-config +-w /usr/bin/kubectl -p x -k kubectl-exec +-w /usr/bin/kubeadm -p x -k kubeadm-exec +-w /usr/bin/crictl -p x -k crictl-exec +-a always,exit -F arch=b64 -S execve -F euid=0 -k root-exec +-a always,exit -F arch=b64 -S mount -k mount-syscall +EOF +sudo augenrules --load 2>/dev/null || sudo systemctl restart auditd +echo " ✓ auditd configuré" + +# Durcissement des permissions sur les répertoires sensibles +# POURQUOI: Les répertoires K8s contiennent les certificats, les kubeconfigs et les +# données etcd. En mode 755 (défaut), tout utilisateur de la VM peut les lire. +echo "Durcissement des permissions des répertoires Kubernetes..." +sudo install -d -m 0700 /etc/kubernetes 2>/dev/null || sudo chmod 700 /etc/kubernetes 2>/dev/null || true +sudo install -d -m 0700 /var/lib/kubelet 2>/dev/null || sudo chmod 700 /var/lib/kubelet 2>/dev/null || true +if [[ "$NODE_ROLE" == "master" ]]; then + sudo install -d -m 0700 /var/lib/etcd 2>/dev/null || sudo chmod 700 /var/lib/etcd 2>/dev/null || true +fi +echo " ✓ /etc/kubernetes, /var/lib/kubelet, /var/lib/etcd en mode 0700" + echo "" echo "=== Vérifications ===" echo "Swap désactivé: $(free -h | grep Swap | awk '{print $2}') (doit être 0)" @@ -219,8 +287,12 @@ else fi echo "Modules kernel: $(lsmod | grep -E 'overlay|br_netfilter|ip_vs|nf_conntrack' | wc -l)/7 chargés" echo "firewalld: $(systemctl is-active firewalld)" +echo "auditd: $(systemctl is-active auditd)" echo "containerd: $(systemctl is-active containerd)" echo "SystemdCgroup: $(grep 'SystemdCgroup = true' /etc/containerd/config.toml > /dev/null && echo 'activé ✓' || echo 'ATTENTION: non activé!')" +echo "seccomp default: $(grep 'unset_seccomp_profile' /etc/containerd/config.toml > /dev/null && echo 'runtime/default ✓' || echo 'ATTENTION: Unconfined!')" +echo "kptr_restrict: $(sysctl -n kernel.kptr_restrict 2>/dev/null) (doit être 2)" +echo "ptrace_scope: $(sysctl -n kernel.yama.ptrace_scope 2>/dev/null) (doit être 2)" echo "" echo "✓ Pré-requis installés avec succès!" echo "Version kubeadm: $(kubeadm version -o short)" diff --git a/partie-01-installation/02-init-control-plane.sh b/partie-01-installation/02-init-control-plane.sh index f938247..e51c05a 100755 --- a/partie-01-installation/02-init-control-plane.sh +++ b/partie-01-installation/02-init-control-plane.sh @@ -6,11 +6,44 @@ set -e echo "=== Initialisation du Control Plane Kubernetes (hardened) ===" -APISERVER_IP=$(hostname -I | awk '{print $1}') +# --- Détermination de l'IP API server --- +# POURQUOI: `hostname -I | awk '{print $1}'` peut renvoyer l'IP PUBLIQUE en premier +# sur certains providers cloud (Exoscale, AWS, ...). Conséquence: l'API +# server (port 6443) serait advertisé sur Internet — un cauchemar pour un CTF. +# Priorité : variable d'env APISERVER_IP > argument $1 > détection sur l'interface +# par défaut (route par défaut). +if [[ -n "$APISERVER_IP" ]]; then + echo "IP API server : $APISERVER_IP (variable d'environnement)" +elif [[ -n "$1" ]] && [[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + APISERVER_IP="$1" + echo "IP API server : $APISERVER_IP (argument positionnel)" +elif [[ -n "$1" ]]; then + echo "ERREUR: '$1' n'est pas une IP valide." + echo "Usage: $0 [] ou APISERVER_IP= $0" + exit 1 +else + DEFAULT_IFACE=$(ip -o -4 route show to default | awk '{print $5}' | head -n1) + APISERVER_IP=$(ip -o -4 addr show dev "$DEFAULT_IFACE" 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -n1) + if [[ -z "$APISERVER_IP" ]]; then + echo "ERREUR: impossible de détecter l'IP de l'API server." + echo " Fournir explicitement: APISERVER_IP=10.0.0.1 $0" + echo " ou: $0 10.0.0.1" + exit 1 + fi + echo "IP API server : $APISERVER_IP (interface: $DEFAULT_IFACE)" +fi + +# Validation : l'IP doit être privée (RFC1918) pour éviter l'exposition publique +if ! echo "$APISERVER_IP" | grep -qE '^(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.)'; then + echo "⚠ ATTENTION: $APISERVER_IP n'est pas une IP privée RFC1918." + echo " Confirmer l'exposition publique de l'API server ? (y/N)" + read -r CONFIRM + [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]] && { echo "Abandon."; exit 1; } +fi + POD_CIDR="10.244.0.0/16" SERVICE_CIDR="10.96.0.0/12" -echo "IP API server : $APISERVER_IP" echo "Pod CIDR : $POD_CIDR" echo "Service CIDR : $SERVICE_CIDR" echo "" @@ -123,11 +156,11 @@ plugins: warn: "restricted" warn-version: "latest" exemptions: + # Cilium est déployé dans kube-system (pas cilium-system) namespaces: - kube-system - kubearmor - kyverno - - cilium-system usernames: [] runtimeClasses: [] EOF @@ -177,6 +210,9 @@ apiServer: value: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - name: request-timeout value: "300s" + # Désactiver l'endpoint /debug/pprof (leak d'informations de débogage) + - name: profiling + value: "false" extraVolumes: - name: audit-log hostPath: /var/log/kubernetes/audit @@ -203,12 +239,43 @@ controllerManager: value: "50" - name: use-service-account-credentials value: "true" + - name: profiling + value: "false" +scheduler: + extraArgs: + - name: profiling + value: "false" --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd -protectKernelDefaults: true +# protectKernelDefaults désactivé car CentOS 10 a kernel.panic=0 et kernel.panic_on_oops=0 +# par défaut (kubelet attend ≥10 et ≥1) — kubelet crasherait au démarrage. +# La sécurité kernel est gérée par /etc/sysctl.d/99-kube-hardening.conf sur l'hôte. +protectKernelDefaults: false +# Le port 10255 (lecture seule, sans auth) DOIT être désactivé readOnlyPort: 0 +# Coupe les exec/attach après 5 minutes d'inactivité (un attaquant peut laisser un shell ouvert) +streamingConnectionIdleTimeout: "5m" +# Limite les events kubelet (anti-flooding) +eventRecordQPS: 5 +eventBurst: 10 +# Recrée les chaînes iptables à chaque sync (cohérent avec kube-proxy/Cilium) +makeIPTablesUtilChains: true +# TLS minimum +tlsMinVersion: "VersionTLS12" +# Authentification kubelet stricte +authentication: + anonymous: + enabled: false + webhook: + enabled: true + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: Webhook +# Rotation auto des certificats serveur kubelet (CSR à approuver côté master) +serverTLSBootstrap: true EOF # --- Initialisation du cluster --- @@ -236,10 +303,17 @@ rm -f /tmp/kubeadm-config.yaml echo "" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ AVERTISSEMENT SÉCURITÉ ║" -echo "║ admin.conf = system:masters = bypass RBAC complet ║" -echo "║ Usage : bootstrap initial uniquement ║" -echo "║ Utiliser 08-generate-restricted-kubeconfig.sh pour ║" -echo "║ générer un accès limité pour l'équipe externe ║" +echo "║ admin.conf = bypass RBAC ║" +echo "║ super-admin.conf = bypass RBAC + bypass kube-apiserver TLS ║" +echo "║ ║" +echo "║ ACTIONS CRITIQUES À FAIRE IMMÉDIATEMENT : ║" +echo "║ 1. Mettre /etc/kubernetes/super-admin.conf hors-ligne ║" +echo "║ (clé USB, gestionnaire de secrets, etc.) ║" +echo "║ 2. Une fois les workers joints, révoquer les tokens : ║" +echo "║ kubeadm token list ║" +echo "║ kubeadm token delete ║" +echo "║ 3. Utiliser 08-generate-restricted-kubeconfig.sh pour ║" +echo "║ générer le kubeconfig de l'équipe externe ║" echo "╚══════════════════════════════════════════════════════════════╝" echo "" echo "✓ Control plane initialisé avec succès!" diff --git a/partie-01-installation/03-join-workers.sh b/partie-01-installation/03-join-workers.sh index 63f85aa..aaf8337 100755 --- a/partie-01-installation/03-join-workers.sh +++ b/partie-01-installation/03-join-workers.sh @@ -48,4 +48,21 @@ sudo kubeadm join "${MASTER_IP}:6443" \ echo "" echo "✓ Worker joint au cluster avec succès!" +echo "" +echo "============================================================" +echo " RAPPEL CRITIQUE — à faire sur le MASTER une fois TOUS les" +echo " workers joints :" +echo "" +echo " 1. Lister les bootstrap tokens encore valides :" +echo " kubeadm token list" +echo "" +echo " 2. Révoquer chaque token (ils ont un TTL 24h par défaut," +echo " mais autant les invalider tout de suite) :" +echo " kubeadm token delete " +echo "" +echo " POURQUOI: Tant qu'un bootstrap token est valide, n'importe" +echo " qui qui le récupère peut joindre un nouveau nœud (potentiellement" +echo " malveillant) au cluster. C'est un vecteur d'attaque classique." +echo "============================================================" +echo "" echo "Retournez sur le master et vérifiez avec: kubectl get nodes" diff --git a/partie-01-installation/04-install-cilium.sh b/partie-01-installation/04-install-cilium.sh index c877297..b7ba54e 100755 --- a/partie-01-installation/04-install-cilium.sh +++ b/partie-01-installation/04-install-cilium.sh @@ -61,6 +61,7 @@ helm upgrade --install cilium cilium/cilium \ --set hubble.enabled=true \ --set hubble.relay.enabled=true \ --set hubble.ui.enabled=true \ + --set hubble.metrics.enabled="{dns,drop,tcp,flow,icmp,httpV2:exemplars=true;labelsContext=source_ip\,source_namespace\,source_workload\,destination_ip\,destination_namespace\,destination_workload\,traffic_direction}" \ --set policyEnforcementMode=default \ --set nodeinit.enabled=true \ --set ipam.mode=kubernetes \ @@ -87,13 +88,17 @@ helm upgrade --install tetragon cilium/tetragon \ echo "" echo "Application des TracingPolicies de base..." -# Surveiller toutes les exécutions de processus (détecte les shells lancés dans des containers, -# les outils de reconnaissance, les tentatives d'escalade) +# Surveiller UNIQUEMENT les exécutions de shells et d'outils suspects. +# POURQUOI: tracer TOUS les execve cluster-wide génère plusieurs Mo/seconde de logs +# (kubelet, scheduler, controller-manager, etc.). On filtre sur les binaires +# qui sont les premiers utilisés par un attaquant après un foothold: +# shells (reconnaissance), outils réseau (exfiltration), tools d'évasion. +# Le filtrage `Postfix` matche aussi /bin/sh, /usr/bin/bash, etc. kubectl apply -f - <<'EOF' apiVersion: cilium.io/v1alpha1 kind: TracingPolicy metadata: - name: monitor-process-exec + name: monitor-suspicious-exec spec: kprobes: - call: "sys_execve" @@ -103,6 +108,26 @@ spec: type: "string" - index: 1 type: "string_array" + selectors: + - matchArgs: + - index: 0 + operator: "Postfix" + values: + - "/sh" + - "/bash" + - "/dash" + - "/zsh" + - "/ash" + - "/nc" + - "/ncat" + - "/curl" + - "/wget" + - "/nmap" + - "/tcpdump" + - "/nsenter" + - "/unshare" + - "/capsh" + - "/socat" EOF # Surveiller les accès aux fichiers sensibles du cluster et de l'hôte diff --git a/partie-01-installation/05-install-kubearmor.sh b/partie-01-installation/05-install-kubearmor.sh index e591b03..e3bdae7 100755 --- a/partie-01-installation/05-install-kubearmor.sh +++ b/partie-01-installation/05-install-kubearmor.sh @@ -52,16 +52,25 @@ echo "Application des ClusterKubeArmorPolicies de base..." # POURQUOI: Un attaquant qui obtient l'exécution de code dans un container essaiera # immédiatement d'ouvrir un shell pour explorer l'environnement. # Bloquer /bin/sh, /bin/bash, etc. coupe ce vecteur. +# SCOPING : on EXCLUT les namespaces système (kube-system, kubearmor, kyverno) car +# certains pods système (CoreDNS, Cilium init, kubelet probes) lancent +# légitimement /bin/sh — bloquer cela casserait le control plane. kubectl apply -f - <<'EOF' apiVersion: security.kubearmor.com/v1 kind: ClusterKubeArmorPolicy metadata: name: block-shell-execution annotations: - description: "Bloque l'exécution de shells dans tous les containers" + description: "Bloque l'exécution de shells dans tous les containers (hors namespaces système)" spec: selector: - matchLabels: {} + matchExpressions: + - key: namespace + operator: NotIn + values: + - kube-system + - kubearmor + - kyverno process: matchPaths: - path: /bin/sh @@ -87,7 +96,13 @@ metadata: description: "Bloque l'accès aux paths sensibles de l'hôte depuis les containers" spec: selector: - matchLabels: {} + matchExpressions: + - key: namespace + operator: NotIn + values: + - kube-system + - kubearmor + - kyverno file: matchDirectories: - dir: /proc/1/ @@ -114,7 +129,13 @@ metadata: description: "Audit (puis potentiellement Block) des outils réseau dans les containers" spec: selector: - matchLabels: {} + matchExpressions: + - key: namespace + operator: NotIn + values: + - kube-system + - kubearmor + - kyverno process: matchPaths: - path: /usr/bin/curl @@ -124,6 +145,7 @@ spec: - path: /usr/bin/ncat - path: /usr/bin/nmap - path: /usr/bin/tcpdump + - path: /usr/bin/socat action: Audit EOF @@ -139,7 +161,13 @@ metadata: description: "Bloque les outils d'évasion de containers (nsenter, unshare, capsh)" spec: selector: - matchLabels: {} + matchExpressions: + - key: namespace + operator: NotIn + values: + - kube-system + - kubearmor + - kyverno process: matchPaths: - path: /usr/bin/nsenter @@ -149,7 +177,78 @@ spec: action: Block EOF -echo " ✓ 4 ClusterKubeArmorPolicies appliquées" +# Policy 5 : Bloquer l'accès aux sockets containerd / docker +# POURQUOI: Si un attaquant peut monter le socket containerd/docker dans son pod +# (Kyverno bloque hostPath, mais defense in depth), il peut piloter +# directement le container runtime et créer des containers privileged. +# Vecteur classique des CVE de breakout. +kubectl apply -f - <<'EOF' +apiVersion: security.kubearmor.com/v1 +kind: ClusterKubeArmorPolicy +metadata: + name: block-container-runtime-sockets + annotations: + description: "Bloque l'accès aux sockets containerd/docker depuis les containers" +spec: + selector: + matchExpressions: + - key: namespace + operator: NotIn + values: + - kube-system + - kubearmor + - kyverno + file: + matchPaths: + - path: /run/containerd/containerd.sock + - path: /var/run/containerd/containerd.sock + - path: /var/run/docker.sock + - path: /run/crio/crio.sock + action: Block +EOF + +# Policy 6 : Bloquer l'écriture dans les répertoires système de l'hôte +# POURQUOI: Si un attaquant arrive à monter un répertoire hôte (ou exploite un +# breakout), bloquer l'écriture dans /etc, /usr, /bin, /sbin l'empêche +# de modifier sudoers, d'écrire un binaire setuid, ou de planter un +# CronJob de persistance dans /etc/cron.d. +kubectl apply -f - <<'EOF' +apiVersion: security.kubearmor.com/v1 +kind: ClusterKubeArmorPolicy +metadata: + name: block-host-system-writes + annotations: + description: "Bloque l'écriture dans les répertoires système hôte depuis les containers" +spec: + selector: + matchExpressions: + - key: namespace + operator: NotIn + values: + - kube-system + - kubearmor + - kyverno + file: + matchDirectories: + - dir: /etc/ + recursive: true + readOnly: true + - dir: /usr/ + recursive: true + readOnly: true + - dir: /bin/ + recursive: true + readOnly: true + - dir: /sbin/ + recursive: true + readOnly: true + - dir: /boot/ + recursive: true + readOnly: true + action: Block +EOF + +echo " ✓ 6 ClusterKubeArmorPolicies appliquées" # --- Vérifications --- echo "" @@ -159,10 +258,8 @@ echo "1. Pods KubeArmor:" kubectl get pods -n kubearmor echo "" echo "2. ClusterKubeArmorPolicies actives:" -kubectl get clusterubearmorpolicies 2>/dev/null || kubectl get clusterkubearmorpholicies 2>/dev/null || \ - kubectl get -f - <<< "$(kubectl api-resources --api-group=security.kubearmor.com -o name 2>/dev/null | head -1)" 2>/dev/null || \ - kubectl get kubearmorpholicies --all-namespaces 2>/dev/null || \ - echo " (CRDs en cours d'initialisation — vérifier dans 30s)" +kubectl get clusterkubearmorpolicies 2>/dev/null || \ + echo " (CRDs en cours d'initialisation — vérifier dans 30s avec: kubectl get clusterkubearmorpolicies)" echo "" echo "✓ KubeArmor installé avec succès!" echo "" @@ -171,4 +268,4 @@ echo " kubectl logs -n kubearmor -l app=kubearmor -f" echo "" echo "NOTE: La policy 'audit-network-recon-tools' est en mode Audit." echo " Après validation des apps légitimes, passer en Block :" -echo " kubectl patch clusterkubearmorpholicy audit-network-recon-tools --type=merge -p '{\"spec\":{\"action\":\"Block\"}}'" +echo " kubectl patch clusterkubearmorpolicy audit-network-recon-tools --type=merge -p '{\"spec\":{\"action\":\"Block\"}}'" diff --git a/partie-01-installation/06-install-kyverno.sh b/partie-01-installation/06-install-kyverno.sh index 0f55e5c..98e08fa 100755 --- a/partie-01-installation/06-install-kyverno.sh +++ b/partie-01-installation/06-install-kyverno.sh @@ -39,7 +39,7 @@ kubectl wait --for=condition=ready pod \ echo " ✓ Kyverno opérationnel" echo "" -echo "Application des 8 ClusterPolicies de sécurité..." +echo "Application des 14 ClusterPolicies de sécurité..." # Policy 1 : Interdire les containers privileged # POURQUOI: Un container privileged a accès complet au kernel de l'hôte (comme root sur l'hôte). @@ -306,7 +306,222 @@ spec: - Egress EOF -echo " ✓ 8 ClusterPolicies appliquées" +# Policy 9 : Forcer le drop de toutes les capabilities (sauf NET_BIND_SERVICE) +# POURQUOI: Par défaut, un container hérite de capabilities (CAP_CHOWN, CAP_DAC_OVERRIDE, +# CAP_NET_RAW, etc.) qui permettent à root dans le container de bypass certains +# contrôles. Drop ALL + add seulement NET_BIND_SERVICE = principe du moindre privilège. +kubectl apply -f - <<'EOF' +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-drop-all-capabilities + annotations: + policies.kyverno.io/description: "Force le drop de toutes les capabilities Linux." +spec: + validationFailureAction: Enforce + background: true + rules: + - name: require-drop-all + match: + any: + - resources: + kinds: [Pod] + exclude: + any: + - resources: + namespaces: [kube-system, kyverno, kubearmor] + validate: + message: "Les containers doivent drop ALL capabilities (securityContext.capabilities.drop)." + foreach: + - list: "request.object.spec.containers" + deny: + conditions: + all: + - key: "ALL" + operator: AnyNotIn + value: "{{ element.securityContext.capabilities.drop[] || `[]` }}" +EOF + +# Policy 10 : Interdire allowPrivilegeEscalation +# POURQUOI: Sans cette restriction, un binaire setuid (ou un appel à execve avec +# transmission des privilèges) peut élever le processus au-delà de ce qu'il +# devrait avoir. Avec false, le bit no_new_privs est posé sur le process tree. +kubectl apply -f - <<'EOF' +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-privilege-escalation + annotations: + policies.kyverno.io/description: "Interdit allowPrivilegeEscalation: true." +spec: + validationFailureAction: Enforce + background: true + rules: + - name: deny-privesc + match: + any: + - resources: + kinds: [Pod] + exclude: + any: + - resources: + namespaces: [kube-system, kyverno, kubearmor] + validate: + message: "allowPrivilegeEscalation doit être false." + pattern: + spec: + containers: + - securityContext: + allowPrivilegeEscalation: false +EOF + +# Policy 11 : Restreindre les registries d'images autorisés +# POURQUOI: Empêche qu'un attaquant pull une image arbitraire (cryptominer, backdoor). +# Whitelist des registries publics réputés + l'éventuel registry interne de l'école. +# AJUSTER les valeurs pour le Kube Battle (ajouter votre registry si besoin). +kubectl apply -f - <<'EOF' +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: restrict-image-registries + annotations: + policies.kyverno.io/description: "Limite les registries autorisés pour les images de container." +spec: + validationFailureAction: Enforce + background: false + rules: + - name: validate-registries + match: + any: + - resources: + kinds: [Pod] + exclude: + any: + - resources: + namespaces: [kube-system, kyverno, kubearmor] + validate: + message: "Image refusée : registry non autorisé. Autorisés : docker.io, registry.k8s.io, quay.io, ghcr.io." + pattern: + spec: + containers: + - image: "docker.io/* | registry.k8s.io/* | quay.io/* | ghcr.io/* | nginx:* | busybox:*" +EOF + +# Policy 12 : Bloquer les services NodePort et LoadBalancer +# POURQUOI: NodePort expose un service sur un port de chaque nœud (accessible depuis +# l'extérieur du cluster). Permet à l'équipe externe d'exposer publiquement +# un service malveillant (C2, exfiltration HTTP). LoadBalancer fait pareil. +# L'équipe externe doit utiliser ClusterIP + Ingress contrôlé. +kubectl apply -f - <<'EOF' +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-nodeport-loadbalancer + annotations: + policies.kyverno.io/description: "Interdit les services type NodePort et LoadBalancer." +spec: + validationFailureAction: Enforce + background: true + rules: + - name: deny-nodeport + match: + any: + - resources: + kinds: [Service] + exclude: + any: + - resources: + namespaces: [kube-system, kyverno, kubearmor] + validate: + message: "Les services NodePort et LoadBalancer sont interdits. Utiliser ClusterIP + Ingress." + pattern: + spec: + =(type): "!NodePort & !LoadBalancer" +EOF + +# Policy 13 : Bloquer les RBAC avec wildcards (verbe ou ressource = *) +# POURQUOI: Un Role avec verbs=["*"] ou resources=["*"] = équivalent cluster-admin +# dans son scope. Si l'équipe externe arrive (par bug RBAC) à créer un Role, +# elle ne pourra au moins pas se donner tous les pouvoirs. +kubectl apply -f - <<'EOF' +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: block-rbac-wildcards + annotations: + policies.kyverno.io/description: "Interdit les Roles/ClusterRoles avec verbe ou ressource '*'." +spec: + validationFailureAction: Enforce + background: true + rules: + - name: deny-wildcard-verbs + match: + any: + - resources: + kinds: [Role, ClusterRole] + exclude: + any: + - clusterRoles: ["cluster-admin"] + - subjects: + - kind: User + name: "system:admin" + validate: + message: "Les Roles/ClusterRoles avec verbe '*' ou ressource '*' sont interdits." + deny: + conditions: + any: + - key: "{{ request.object.rules[].verbs[] | contains(@, '*') }}" + operator: Equals + value: true + - key: "{{ request.object.rules[].resources[] | contains(@, '*') }}" + operator: Equals + value: true + - key: "{{ request.object.rules[].verbs[] | contains(@, 'escalate') }}" + operator: Equals + value: true + - key: "{{ request.object.rules[].verbs[] | contains(@, 'bind') }}" + operator: Equals + value: true + - key: "{{ request.object.rules[].verbs[] | contains(@, 'impersonate') }}" + operator: Equals + value: true +EOF + +# Policy 14 : Interdire hostPort dans les containers +# POURQUOI: hostPort bind un port de l'hôte directement (bypass du Service K8s). +# Permet à un attaquant d'écouter sur un port haut de l'hôte (par ex 31337) +# pour un C2 ou un reverse shell, contournant les Services et NetworkPolicies. +kubectl apply -f - <<'EOF' +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: disallow-host-ports + annotations: + policies.kyverno.io/description: "Interdit les hostPort dans les containers." +spec: + validationFailureAction: Enforce + background: true + rules: + - name: deny-host-port + match: + any: + - resources: + kinds: [Pod] + exclude: + any: + - resources: + namespaces: [kube-system, kyverno, kubearmor] + validate: + message: "Les hostPort sont interdits." + deny: + conditions: + any: + - key: "{{ request.object.spec.containers[].ports[].hostPort | length(@) }}" + operator: GreaterThan + value: 0 +EOF + +echo " ✓ 14 ClusterPolicies appliquées" # --- Vérifications --- echo "" diff --git a/partie-01-installation/07-verify-cluster.sh b/partie-01-installation/07-verify-cluster.sh index 9a4e805..dc26282 100755 --- a/partie-01-installation/07-verify-cluster.sh +++ b/partie-01-installation/07-verify-cluster.sh @@ -71,11 +71,11 @@ else check_fail "Pods KubeArmor non Running" fi -KUBEARMOR_POLICIES=$(kubectl get clusterkubearmorpholicies 2>/dev/null | grep -c "block\|audit" || echo "0") -if [[ "$KUBEARMOR_POLICIES" -ge 4 ]]; then +KUBEARMOR_POLICIES=$(kubectl get clusterkubearmorpolicies --no-headers 2>/dev/null | wc -l || echo "0") +if [[ "$KUBEARMOR_POLICIES" -ge 6 ]]; then check_ok "ClusterKubeArmorPolicies actives: ${KUBEARMOR_POLICIES}" else - check_fail "ClusterKubeArmorPolicies insuffisantes (trouvées: ${KUBEARMOR_POLICIES}, attendues: 4)" + check_fail "ClusterKubeArmorPolicies insuffisantes (trouvées: ${KUBEARMOR_POLICIES}, attendues: 6)" fi echo "" @@ -87,12 +87,11 @@ else check_fail "Pods Kyverno non Running" fi -KYVERNO_POLICIES=$(kubectl get clusterpolicies 2>/dev/null | grep -c "pass\|fail" || \ - kubectl get clusterpolicies 2>/dev/null | tail -n +2 | wc -l || echo "0") -if [[ "$KYVERNO_POLICIES" -ge 8 ]]; then +KYVERNO_POLICIES=$(kubectl get clusterpolicies --no-headers 2>/dev/null | wc -l || echo "0") +if [[ "$KYVERNO_POLICIES" -ge 14 ]]; then check_ok "ClusterPolicies Kyverno actives: ${KYVERNO_POLICIES}" else - check_fail "ClusterPolicies Kyverno insuffisantes (trouvées: ${KYVERNO_POLICIES}, attendues: 8)" + check_fail "ClusterPolicies Kyverno insuffisantes (trouvées: ${KYVERNO_POLICIES}, attendues: 14)" fi echo "" @@ -190,6 +189,93 @@ kubectl wait --for=condition=available deployment/cluster-verify-test \ check_fail "Déploiement légitime échoué (vérifier les policies)" echo "" +# --- 11. Test KubeArmor : blocage de l'exécution de shell --- +echo "11. KubeArmor - blocage shell dans pod externe:" +# Créer un pod test dans un namespace simulant l'équipe externe +kubectl create namespace kubearmor-test --dry-run=client -o yaml | kubectl apply -f - > /dev/null 2>&1 +CLEANUP_RESOURCES+=("namespace/kubearmor-test") + +kubectl run kubearmor-test-pod \ + --image=busybox:1.36 \ + --restart=Never \ + -n kubearmor-test \ + -- sleep 3600 2>/dev/null || true +kubectl wait --for=condition=ready pod/kubearmor-test-pod -n kubearmor-test --timeout=60s 2>/dev/null || true + +# Tenter d'exécuter /bin/sh — KubeArmor doit refuser via la policy block-shell-execution +SHELL_OUTPUT=$(kubectl exec -n kubearmor-test kubearmor-test-pod -- /bin/sh -c 'echo blocked-test' 2>&1 || true) +if echo "$SHELL_OUTPUT" | grep -qiE "permission denied|operation not permitted|block"; then + check_ok "KubeArmor bloque l'exécution de /bin/sh" +else + # Note: KubeArmor peut nécessiter ~30s pour propager la policy, et certains + # busybox embarquent /bin/sh avec un chemin différent. Pas critique en CTF. + check_fail "KubeArmor n'a pas bloqué /bin/sh (policy peut-être pas propagée, ou chemin différent)" +fi +echo "" + +# --- 12. Test RBAC : SA externe ne doit pas pouvoir lister les nodes --- +echo "12. RBAC - SA external-deployer ne peut pas lister les nodes:" +SA_NS="external-app" +SA_NAME="external-deployer" +if kubectl get sa "$SA_NAME" -n "$SA_NS" >/dev/null 2>&1; then + if kubectl auth can-i list nodes \ + --as="system:serviceaccount:${SA_NS}:${SA_NAME}" 2>/dev/null | grep -q "^no$"; then + check_ok "SA externe ne peut pas lister les nodes" + else + check_fail "PROBLÈME: SA externe peut lister les nodes (énumération possible)" + fi + + if kubectl auth can-i create clusterrolebindings \ + --as="system:serviceaccount:${SA_NS}:${SA_NAME}" 2>/dev/null | grep -q "^no$"; then + check_ok "SA externe ne peut pas créer de ClusterRoleBinding" + else + check_fail "PROBLÈME: SA externe peut créer des ClusterRoleBindings" + fi + + if kubectl auth can-i get secrets \ + --as="system:serviceaccount:${SA_NS}:${SA_NAME}" -n kube-system 2>/dev/null | grep -q "^no$"; then + check_ok "SA externe ne peut pas lire les secrets de kube-system" + else + check_fail "PROBLÈME: SA externe peut lire les secrets de kube-system" + fi +else + echo " ⓘ SA $SA_NAME pas encore créé (lancer 08-generate-restricted-kubeconfig.sh)" +fi +echo "" + +# --- 13. Test Kyverno : NodePort doit être refusé --- +echo "13. Kyverno - rejet service NodePort:" +NODEPORT_OUTPUT=$(kubectl create service nodeport kyverno-test-nodeport \ + --tcp=80:80 \ + --node-port=31999 \ + --dry-run=server 2>&1 || true) +if echo "$NODEPORT_OUTPUT" | grep -qiE "disallow-nodeport|admission webhook|denied"; then + check_ok "Service NodePort correctement refusé par Kyverno" +else + check_fail "Service NodePort NON refusé — vérifier la ClusterPolicy disallow-nodeport-loadbalancer" +fi +echo "" + +# --- 14. Test Kyverno : ClusterRole avec wildcard refusé --- +echo "14. Kyverno - rejet ClusterRole avec verbe '*':" +WILDCARD_OUTPUT=$(kubectl apply --dry-run=server -f - 2>&1 <<'YAML' || true +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kyverno-wildcard-test +rules: + - apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +YAML +) +if echo "$WILDCARD_OUTPUT" | grep -qiE "block-rbac-wildcards|admission webhook|denied"; then + check_ok "ClusterRole avec wildcards correctement refusé par Kyverno" +else + check_fail "ClusterRole avec wildcards NON refusé — vérifier la ClusterPolicy block-rbac-wildcards" +fi +echo "" + # --- Résumé --- echo "=============================================" echo " RÉSUMÉ DES VÉRIFICATIONS" diff --git a/partie-01-installation/08-generate-restricted-kubeconfig.sh b/partie-01-installation/08-generate-restricted-kubeconfig.sh index d76182f..f66f3c1 100755 --- a/partie-01-installation/08-generate-restricted-kubeconfig.sh +++ b/partie-01-installation/08-generate-restricted-kubeconfig.sh @@ -39,6 +39,68 @@ kubectl label namespace "$NAMESPACE" \ echo " ✓ Namespace créé avec PodSecurity=restricted" +# --- ResourceQuota : empêche le DoS par création massive de ressources --- +# POURQUOI: Sans quota, l'équipe externe peut créer 10 000 pods, des centaines de PVC, +# ou allouer des dizaines de Go de RAM. Cela peut saturer le scheduler, +# remplir le disque etcd ou faire OOM le nœud. +echo "" +echo "1bis. ResourceQuota anti-DoS dans ${NAMESPACE}..." +kubectl apply -f - <