#!/bin/bash # Partie 1 - Installation de Kyverno + policies d'admission # À exécuter sur le nœud MASTER # # Kyverno = admission controller déclaratif (policies YAML, intercepte l'API server) # Agit AVANT le démarrage du pod (vs KubeArmor qui agit pendant l'exécution). # Les deux couches sont complémentaires : # Kyverno → bloque la création de workloads dangereux # KubeArmor → bloque les actions dangereuses dans les workloads qui tournent set -e KYVERNO_VERSION="3.4.1" echo "=== Installation Kyverno ${KYVERNO_VERSION} ===" echo "" # --- Installation via Helm --- echo "Ajout du repo Helm Kyverno..." helm repo add kyverno https://kyverno.github.io/kyverno/ 2>/dev/null || true helm repo update echo "" echo "Installation de Kyverno ${KYVERNO_VERSION}..." helm upgrade --install kyverno kyverno/kyverno \ --namespace kyverno \ --create-namespace \ --version "${KYVERNO_VERSION}" \ --set replicaCount=1 \ --set features.policyExceptions.enabled=true \ --wait --timeout=5m echo "" echo "Attente que Kyverno soit opérationnel..." kubectl wait --for=condition=ready pod \ -l app.kubernetes.io/name=kyverno \ -n kyverno \ --timeout=120s echo " ✓ Kyverno opérationnel" echo "" 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). # C'est le vecteur d'escalade le plus courant — un container privileged peut lire # /dev/kmem, charger des modules kernel, modifier les tables iptables de l'hôte. kubectl apply -f - <<'EOF' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: disallow-privileged-containers annotations: policies.kyverno.io/description: "Interdit les containers privileged." spec: validationFailureAction: Enforce background: true rules: - name: deny-privileged match: any: - resources: kinds: [Pod] validate: message: "Les containers privileged sont interdits." pattern: spec: containers: - =(securityContext): =(privileged): "false" =(initContainers): - =(securityContext): =(privileged): "false" EOF # Policy 2 : Interdire hostNetwork, hostPID, hostIPC # POURQUOI: # hostNetwork : accès direct aux interfaces réseau de l'hôte (bypass complet du CNI) # hostPID : accès à tous les processus de l'hôte (lecture mémoire via /proc//mem) # hostIPC : accès aux segments de mémoire partagée de l'hôte kubectl apply -f - <<'EOF' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: disallow-host-namespaces annotations: policies.kyverno.io/description: "Interdit hostNetwork, hostPID, hostIPC." spec: validationFailureAction: Enforce background: true rules: - name: deny-host-namespaces match: any: - resources: kinds: [Pod] validate: message: "hostNetwork, hostPID et hostIPC sont interdits." pattern: spec: =(hostNetwork): "false" =(hostPID): "false" =(hostIPC): "false" EOF # Policy 3 : Interdire les volumes hostPath # POURQUOI: hostPath monte un répertoire de l'hôte dans le container. # Vecteur classique : monter /etc pour modifier sudoers, /var/lib/kubelet pour # lire les tokens de ServiceAccount, ou / pour accès complet au système de fichiers hôte. kubectl apply -f - <<'EOF' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: disallow-hostpath-volumes annotations: policies.kyverno.io/description: "Interdit les volumes hostPath." spec: validationFailureAction: Enforce background: true rules: - name: deny-hostpath match: any: - resources: kinds: [Pod] validate: message: "Les volumes hostPath sont interdits." deny: conditions: any: - key: "{{ request.object.spec.volumes[].hostPath | length(@) }}" operator: GreaterThan value: 0 EOF # Policy 4 : Forcer les resource limits # POURQUOI: Sans limits, un pod compromis peut consommer tout le CPU/RAM du nœud # et provoquer un déni de service sur les autres pods (DoS interne). kubectl apply -f - <<'EOF' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-resource-limits annotations: policies.kyverno.io/description: "Oblige à définir des limits CPU et mémoire." spec: validationFailureAction: Enforce background: true rules: - name: require-limits match: any: - resources: kinds: [Pod] namespaces: - "external-app" validate: message: "Les limits CPU et mémoire sont obligatoires." pattern: spec: containers: - resources: limits: cpu: "?*" memory: "?*" EOF # Policy 5 : Forcer runAsNonRoot # POURQUOI: Un container tournant en root (UID 0) peut, en cas de breakout, opérer # en root sur l'hôte si les protections kernel (SELinux, seccomp) sont contournées. kubectl apply -f - <<'EOF' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-run-as-non-root annotations: policies.kyverno.io/description: "Oblige les containers à tourner en non-root." spec: validationFailureAction: Enforce background: true rules: - name: require-non-root match: any: - resources: kinds: [Pod] namespaces: - "external-app" validate: message: "Les containers doivent tourner en non-root (runAsNonRoot: true)." pattern: spec: =(securityContext): =(runAsNonRoot): true containers: - =(securityContext): =(runAsNonRoot): true EOF # Policy 6 : Forcer readOnlyRootFilesystem # POURQUOI: Un FS racine en lecture seule empêche l'attaquant d'écrire des binaires, # de modifier des scripts de démarrage ou de persister dans le container. kubectl apply -f - <<'EOF' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-readonly-rootfs annotations: policies.kyverno.io/description: "Oblige le filesystem racine des containers à être en lecture seule." spec: validationFailureAction: Enforce background: true rules: - name: require-readonly-fs match: any: - resources: kinds: [Pod] namespaces: - "external-app" validate: message: "readOnlyRootFilesystem doit être true." pattern: spec: containers: - securityContext: readOnlyRootFilesystem: true EOF # Policy 7 : Interdire le tag :latest sur les images # POURQUOI: :latest est non-reproductible et peut changer silencieusement. # Permet aussi l'injection d'une image malveillante dans le registry. # Forcer les tags immuables (sha256 digest ou version sémantique) garantit # que le code déployé est bien celui qui a été validé. kubectl apply -f - <<'EOF' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: disallow-latest-tag annotations: policies.kyverno.io/description: "Interdit l'utilisation du tag :latest." spec: validationFailureAction: Enforce background: true rules: - name: deny-latest match: any: - resources: kinds: [Pod] validate: message: "Le tag ':latest' est interdit. Utiliser un tag de version ou un digest sha256." foreach: - list: "request.object.spec.containers" deny: conditions: any: - key: "{{ element.image }}" operator: Equals value: "*:latest" - key: "{{ element.image }}" operator: NotIn value: ["*:*"] EOF # Policy 8 : Générer automatiquement une NetworkPolicy deny-all dans chaque nouveau namespace # POURQUOI: Tout namespace sans NetworkPolicy est un réseau ouvert — tous les pods # peuvent se parler librement, y compris cross-namespace. # Kyverno génère la policy au moment de la création du namespace, # garantissant qu'aucun namespace n'est créé sans isolation réseau. kubectl apply -f - <<'EOF' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: generate-default-deny-networkpolicy annotations: policies.kyverno.io/description: "Génère une NetworkPolicy deny-all dans chaque nouveau namespace." spec: rules: - name: generate-deny-all match: any: - resources: kinds: [Namespace] exclude: any: - resources: names: - kube-system - kube-public - kube-node-lease - kubearmor - kyverno - cilium-system generate: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy name: default-deny-all namespace: "{{request.object.metadata.name}}" synchronize: true data: spec: podSelector: {} policyTypes: - Ingress - Egress EOF # 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 "" echo "=== Vérifications ===" echo "" echo "1. Pods Kyverno:" kubectl get pods -n kyverno echo "" echo "2. ClusterPolicies actives:" kubectl get clusterpolicies echo "" echo "3. Test de la policy 1 (pod privileged doit être refusé):" if kubectl run kyverno-test-privileged \ --image=nginx:1.25 \ --restart=Never \ --overrides='{"spec":{"containers":[{"name":"kyverno-test-privileged","image":"nginx:1.25","securityContext":{"privileged":true}}]}}' \ --dry-run=server 2>&1 | grep -q "disallow-privileged"; then echo " ✓ Pod privileged correctement refusé par Kyverno" else kubectl run kyverno-test-privileged \ --image=nginx:1.25 \ --restart=Never \ --overrides='{"spec":{"containers":[{"name":"kyverno-test-privileged","image":"nginx:1.25","securityContext":{"privileged":true}}]}}' \ --dry-run=server 2>&1 || echo " ✓ Pod privileged refusé (vérifier le message ci-dessus)" fi echo "" echo "✓ Kyverno installé avec succès!" echo "" echo "Voir les violations Kyverno :" echo " kubectl get policyreport --all-namespaces" echo " kubectl get clusterpolicyreport"