336 lines
12 KiB
Bash
Executable File
336 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
# Partie 1 - Initialisation du control plane
|
|
# À exécuter UNIQUEMENT sur le nœud MASTER
|
|
|
|
set -e
|
|
|
|
echo "=== Initialisation du Control Plane Kubernetes (hardened) ==="
|
|
|
|
# --- 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 "Pod CIDR : $POD_CIDR"
|
|
echo "Service CIDR : $SERVICE_CIDR"
|
|
echo ""
|
|
|
|
# --- Audit logging ---
|
|
echo "Création de la politique d'audit..."
|
|
sudo mkdir -p /var/log/kubernetes/audit
|
|
sudo mkdir -p /etc/kubernetes/audit
|
|
|
|
sudo tee /etc/kubernetes/audit/audit-policy.yaml > /dev/null <<'EOF'
|
|
apiVersion: audit.k8s.io/v1
|
|
kind: Policy
|
|
omitStages:
|
|
- RequestReceived
|
|
rules:
|
|
# Tracer tous les accès aux secrets et configmaps (données sensibles)
|
|
- level: RequestResponse
|
|
resources:
|
|
- group: ""
|
|
resources: ["secrets", "configmaps"]
|
|
|
|
# Tracer les modifications RBAC (vecteur d'escalade de privilèges)
|
|
- level: RequestResponse
|
|
resources:
|
|
- group: "rbac.authorization.k8s.io"
|
|
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
|
|
|
|
# Tracer exec/portforward/attach (accès interactif aux pods — vecteur d'attaque courant)
|
|
- level: RequestResponse
|
|
resources:
|
|
- group: ""
|
|
resources: ["pods/exec", "pods/portforward", "pods/attach"]
|
|
|
|
# Tracer toutes les créations/suppressions/modifications (niveau Metadata pour réduire le volume)
|
|
- level: Metadata
|
|
verbs: ["create", "delete", "patch", "update"]
|
|
|
|
# Ignorer le bruit des health checks et composants systèmes
|
|
- level: None
|
|
users: ["system:kube-proxy"]
|
|
verbs: ["watch"]
|
|
resources:
|
|
- group: ""
|
|
resources: ["endpoints", "services", "services/status"]
|
|
- level: None
|
|
users: ["system:apiserver"]
|
|
verbs: ["get"]
|
|
resources:
|
|
- group: ""
|
|
resources: ["namespaces"]
|
|
- level: None
|
|
nonResourceURLs: ["/healthz*", "/readyz*", "/livez*", "/metrics"]
|
|
|
|
# Défaut : niveau Metadata pour tout le reste
|
|
- level: Metadata
|
|
EOF
|
|
|
|
# --- Chiffrement etcd at-rest ---
|
|
echo "Génération de la clé de chiffrement etcd..."
|
|
sudo mkdir -p /etc/kubernetes/encryption
|
|
|
|
ENCRYPTION_KEY=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64)
|
|
|
|
sudo tee /etc/kubernetes/encryption/encryption-config.yaml > /dev/null <<EOF
|
|
apiVersion: apiserver.config.k8s.io/v1
|
|
kind: EncryptionConfiguration
|
|
resources:
|
|
- resources:
|
|
- secrets
|
|
- configmaps
|
|
providers:
|
|
- aescbc:
|
|
keys:
|
|
- name: key1
|
|
secret: ${ENCRYPTION_KEY}
|
|
- identity: {}
|
|
EOF
|
|
sudo chmod 600 /etc/kubernetes/encryption/encryption-config.yaml
|
|
echo " ✓ Clé AES-CBC générée (32 bytes, base64)"
|
|
|
|
# --- Config admission controllers ---
|
|
echo "Création de la configuration des admission controllers..."
|
|
sudo mkdir -p /etc/kubernetes/admission
|
|
|
|
sudo tee /etc/kubernetes/admission/admission-config.yaml > /dev/null <<'EOF'
|
|
apiVersion: apiserver.config.k8s.io/v1
|
|
kind: AdmissionConfiguration
|
|
plugins:
|
|
- name: EventRateLimit
|
|
configuration:
|
|
apiVersion: eventratelimit.admission.k8s.io/v1alpha1
|
|
kind: Configuration
|
|
limits:
|
|
- type: Namespace
|
|
qps: 50
|
|
burst: 100
|
|
cacheSize: 2000
|
|
- type: User
|
|
qps: 10
|
|
burst: 50
|
|
- name: PodSecurity
|
|
configuration:
|
|
apiVersion: pod-security.admission.config.k8s.io/v1
|
|
kind: PodSecurityConfiguration
|
|
defaults:
|
|
enforce: "baseline"
|
|
enforce-version: "latest"
|
|
audit: "restricted"
|
|
audit-version: "latest"
|
|
warn: "restricted"
|
|
warn-version: "latest"
|
|
exemptions:
|
|
# Cilium est déployé dans kube-system (pas cilium-system)
|
|
namespaces:
|
|
- kube-system
|
|
- kubearmor
|
|
- kyverno
|
|
usernames: []
|
|
runtimeClasses: []
|
|
EOF
|
|
|
|
# --- kubeadm config ---
|
|
echo "Création de la configuration kubeadm..."
|
|
|
|
cat > /tmp/kubeadm-config.yaml <<EOF
|
|
apiVersion: kubeadm.k8s.io/v1beta4
|
|
kind: InitConfiguration
|
|
localAPIEndpoint:
|
|
advertiseAddress: ${APISERVER_IP}
|
|
bindPort: 6443
|
|
nodeRegistration:
|
|
criSocket: unix:///run/containerd/containerd.sock
|
|
---
|
|
apiVersion: kubeadm.k8s.io/v1beta4
|
|
kind: ClusterConfiguration
|
|
kubernetesVersion: v1.34.0
|
|
networking:
|
|
podSubnet: "${POD_CIDR}"
|
|
serviceSubnet: "${SERVICE_CIDR}"
|
|
dnsDomain: "cluster.local"
|
|
apiServer:
|
|
extraArgs:
|
|
- name: audit-log-path
|
|
value: /var/log/kubernetes/audit/audit.log
|
|
- name: audit-policy-file
|
|
value: /etc/kubernetes/audit/audit-policy.yaml
|
|
- name: audit-log-maxage
|
|
value: "30"
|
|
- name: audit-log-maxbackup
|
|
value: "10"
|
|
- name: audit-log-maxsize
|
|
value: "100"
|
|
- name: encryption-provider-config
|
|
value: /etc/kubernetes/encryption/encryption-config.yaml
|
|
- name: enable-admission-plugins
|
|
value: NodeRestriction,PodSecurity,EventRateLimit
|
|
- name: admission-control-config-file
|
|
value: /etc/kubernetes/admission/admission-config.yaml
|
|
- name: anonymous-auth
|
|
value: "false"
|
|
- name: tls-min-version
|
|
value: VersionTLS12
|
|
- name: tls-cipher-suites
|
|
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
|
|
mountPath: /var/log/kubernetes/audit
|
|
pathType: DirectoryOrCreate
|
|
- name: audit-policy
|
|
hostPath: /etc/kubernetes/audit/audit-policy.yaml
|
|
mountPath: /etc/kubernetes/audit/audit-policy.yaml
|
|
readOnly: true
|
|
pathType: File
|
|
- name: encryption-config
|
|
hostPath: /etc/kubernetes/encryption/encryption-config.yaml
|
|
mountPath: /etc/kubernetes/encryption/encryption-config.yaml
|
|
readOnly: true
|
|
pathType: File
|
|
- name: admission-config
|
|
hostPath: /etc/kubernetes/admission/admission-config.yaml
|
|
mountPath: /etc/kubernetes/admission/admission-config.yaml
|
|
readOnly: true
|
|
pathType: File
|
|
controllerManager:
|
|
extraArgs:
|
|
- name: terminated-pod-gc-threshold
|
|
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 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 ---
|
|
echo ""
|
|
echo "Initialisation de kubeadm..."
|
|
echo " Paramètres : audit logs + etcd chiffré AES + admission controllers"
|
|
echo " --skip-phases=addon/kube-proxy : Cilium remplacera kube-proxy (eBPF natif)"
|
|
echo ""
|
|
|
|
sudo kubeadm init \
|
|
--config=/tmp/kubeadm-config.yaml \
|
|
--skip-phases=addon/kube-proxy
|
|
|
|
# Configuration kubectl
|
|
echo ""
|
|
echo "Configuration de kubectl pour l'utilisateur courant..."
|
|
mkdir -p "$HOME/.kube"
|
|
sudo cp -i /etc/kubernetes/admin.conf "$HOME/.kube/config"
|
|
sudo chown "$(id -u):$(id -g)" "$HOME/.kube/config"
|
|
chmod 600 "$HOME/.kube/config"
|
|
|
|
# Nettoyer les fichiers temporaires sensibles
|
|
rm -f /tmp/kubeadm-config.yaml
|
|
|
|
echo ""
|
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
|
echo "║ AVERTISSEMENT SÉCURITÉ ║"
|
|
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!"
|
|
echo ""
|
|
echo "Sécurité activée:"
|
|
echo " ✓ Audit logs → /var/log/kubernetes/audit/audit.log"
|
|
echo " ✓ etcd chiffré → AES-CBC 256 bits at-rest"
|
|
echo " ✓ Admission → NodeRestriction + PodSecurity + EventRateLimit"
|
|
echo " ✓ Auth anonyme → désactivée"
|
|
echo " ✓ TLS min → 1.2"
|
|
echo ""
|
|
echo "Prochaines étapes:"
|
|
echo " 1. Joindre les workers : 03-join-workers.sh"
|
|
echo " 2. Installer Cilium : 04-install-cilium.sh"
|
|
echo " (commande join à récupérer ci-dessus ou via: kubeadm token create --print-join-command)"
|
|
echo ""
|
|
echo "Statut du cluster:"
|
|
kubectl get nodes
|
|
kubectl get pods -n kube-system
|