556 lines
18 KiB
Bash
Executable File
556 lines
18 KiB
Bash
Executable File
#!/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/<pid>/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"
|