BIN
partie-01-installation/.DS_Store
vendored
Normal file
BIN
partie-01-installation/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -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 <<EOF | sudo tee /etc/sysctl.d/99-kube-hardening.conf
|
||||
kernel.kptr_restrict = 2
|
||||
kernel.dmesg_restrict = 1
|
||||
kernel.yama.ptrace_scope = 2
|
||||
kernel.kexec_load_disabled = 1
|
||||
net.core.bpf_jit_harden = 2
|
||||
fs.protected_hardlinks = 1
|
||||
fs.protected_symlinks = 1
|
||||
fs.protected_fifos = 2
|
||||
fs.protected_regular = 2
|
||||
fs.suid_dumpable = 0
|
||||
EOF
|
||||
|
||||
sudo sysctl --system
|
||||
|
||||
# Configurer firewalld avec les règles Kubernetes
|
||||
@@ -160,6 +182,18 @@ containerd config default | sudo tee /etc/containerd/config.toml > /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)"
|
||||
|
||||
@@ -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 [<IP>] ou APISERVER_IP=<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 <token> ║"
|
||||
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!"
|
||||
|
||||
@@ -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 <TOKEN_ID>"
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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\"}}'"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 - <<EOF
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: external-app-quota
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
hard:
|
||||
pods: "20"
|
||||
requests.cpu: "4"
|
||||
requests.memory: "8Gi"
|
||||
limits.cpu: "8"
|
||||
limits.memory: "16Gi"
|
||||
persistentvolumeclaims: "5"
|
||||
requests.storage: "20Gi"
|
||||
services: "10"
|
||||
services.nodeports: "0"
|
||||
services.loadbalancers: "0"
|
||||
secrets: "20"
|
||||
configmaps: "20"
|
||||
EOF
|
||||
echo " ✓ ResourceQuota créé (max 20 pods, 8 CPU, 16 Gi RAM)"
|
||||
|
||||
# --- LimitRange : valeurs par défaut + max par container ---
|
||||
# POURQUOI: Sans LimitRange, un pod peut consommer tout le CPU/RAM du nœud.
|
||||
# Cela force des valeurs raisonnables même si l'équipe externe oublie
|
||||
# de définir resources.requests/limits dans ses manifests.
|
||||
echo ""
|
||||
echo "1ter. LimitRange dans ${NAMESPACE}..."
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: LimitRange
|
||||
metadata:
|
||||
name: external-app-limits
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
limits:
|
||||
- type: Container
|
||||
default:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
ephemeral-storage: "1Gi"
|
||||
defaultRequest:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
ephemeral-storage: "256Mi"
|
||||
max:
|
||||
cpu: "2"
|
||||
memory: "2Gi"
|
||||
ephemeral-storage: "4Gi"
|
||||
min:
|
||||
cpu: "10m"
|
||||
memory: "16Mi"
|
||||
EOF
|
||||
echo " ✓ LimitRange créé (max 2 CPU / 2 Gi par container)"
|
||||
|
||||
# --- ServiceAccount ---
|
||||
echo ""
|
||||
echo "2. Création du ServiceAccount ${SA_NAME}..."
|
||||
@@ -128,10 +190,12 @@ kind: ClusterPolicy
|
||||
metadata:
|
||||
name: restrict-external-deployer-escalation
|
||||
annotations:
|
||||
policies.kyverno.io/description: "Interdit au SA ${SA_NAME} de créer des ressources cluster-scoped."
|
||||
policies.kyverno.io/description: "Interdit au SA ${SA_NAME} d'escalader ses privilèges via RBAC."
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
rules:
|
||||
# Interdire toute création de ressources cluster-scoped (déjà bloqué par RBAC,
|
||||
# mais defense in depth — si un bug RBAC laisse passer, Kyverno bloque)
|
||||
- name: deny-clusterrole-creation
|
||||
match:
|
||||
any:
|
||||
@@ -144,8 +208,67 @@ spec:
|
||||
validate:
|
||||
message: "Le compte ${SA_NAME} ne peut pas créer de ressources cluster-scoped."
|
||||
deny: {}
|
||||
# Interdire la création de Role/RoleBinding qui font référence à un ClusterRole privilégié
|
||||
# POURQUOI: Le RBAC standard permet de créer un RoleBinding qui lie un SA à un
|
||||
# ClusterRole comme "cluster-admin" (escalade par référence). On bloque
|
||||
# explicitement cette voie d'escalade.
|
||||
- name: deny-rolebinding-to-clusteradmin
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds: [RoleBinding]
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: ${SA_NAME}
|
||||
namespace: ${NAMESPACE}
|
||||
validate:
|
||||
message: "Le compte ${SA_NAME} ne peut pas binder un ClusterRole privilégié."
|
||||
deny:
|
||||
conditions:
|
||||
any:
|
||||
- key: "{{ request.object.roleRef.name }}"
|
||||
operator: AnyIn
|
||||
value:
|
||||
- cluster-admin
|
||||
- admin
|
||||
- edit
|
||||
- system:masters
|
||||
- key: "{{ request.object.roleRef.kind }}"
|
||||
operator: Equals
|
||||
value: ClusterRole
|
||||
# Interdire les Roles avec verbes/ressources wildcards (couvert par block-rbac-wildcards
|
||||
# global, mais ciblé spécifiquement sur ce SA pour les messages d'erreur)
|
||||
- name: deny-wildcard-role-creation
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds: [Role]
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: ${SA_NAME}
|
||||
namespace: ${NAMESPACE}
|
||||
validate:
|
||||
message: "Le compte ${SA_NAME} ne peut pas créer de Role avec wildcards (*) ou verbes d'escalade."
|
||||
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
|
||||
echo " ✓ Policy anti-escalade Kyverno créée"
|
||||
echo " ✓ Policy anti-escalade Kyverno créée (3 règles)"
|
||||
|
||||
# --- Génération du token ---
|
||||
echo ""
|
||||
@@ -215,11 +338,22 @@ echo ""
|
||||
echo " IMPORTANT : Ce token expire dans ${TOKEN_DURATION}."
|
||||
echo " Pour régénérer : kubectl create token ${SA_NAME} -n ${NAMESPACE} --duration=${TOKEN_DURATION}"
|
||||
echo ""
|
||||
echo " Vecteurs d'attaque que l'équipe PEUT tenter :"
|
||||
echo " - Escape via les NetworkPolicies (tenter de contacter d'autres namespaces)"
|
||||
echo " - Escalade RBAC (tenter de créer des ClusterRoles/ClusterRoleBindings)"
|
||||
echo " - Container breakout (pod privileged → refusé par Kyverno)"
|
||||
echo " - Shell dans container (bloqué par KubeArmor)"
|
||||
echo " - Lecture secrets (interdit par RBAC)"
|
||||
echo " - Déploiement d'image malveillante avec :latest (refusé par Kyverno)"
|
||||
echo " Vecteurs d'attaque que l'équipe PEUT tenter (et qui sont bloqués) :"
|
||||
echo " - Container breakout (pod privileged) → refusé par Kyverno + PSA"
|
||||
echo " - Évasion namespace (hostNetwork/PID/IPC) → refusé par Kyverno"
|
||||
echo " - Mount hostPath → refusé par Kyverno"
|
||||
echo " - Shell dans container → bloqué par KubeArmor"
|
||||
echo " - Outils d'évasion (nsenter, unshare) → bloqués par KubeArmor"
|
||||
echo " - Accès socket containerd → bloqué par KubeArmor"
|
||||
echo " - Écriture /etc, /usr, /bin → bloquée par KubeArmor"
|
||||
echo " - Lecture secrets existants → interdit par RBAC"
|
||||
echo " - Création ClusterRole/Binding → interdit par RBAC + Kyverno"
|
||||
echo " - RoleBinding vers cluster-admin → bloqué par Kyverno"
|
||||
echo " - Service NodePort/LoadBalancer → bloqué par Kyverno"
|
||||
echo " - hostPort → bloqué par Kyverno"
|
||||
echo " - Image :latest ou registry inconnu → refusé par Kyverno"
|
||||
echo " - DoS par création massive de pods → bloqué par ResourceQuota (max 20 pods)"
|
||||
echo " - DoS RAM/CPU → bloqué par LimitRange (max 2 CPU / 2 Gi par container)"
|
||||
echo " - Communication inter-namespace → bloquée par NetworkPolicy default-deny"
|
||||
echo " - Énumération nodes/namespaces → interdit par RBAC"
|
||||
echo "============================================="
|
||||
|
||||
Reference in New Issue
Block a user