Durcir Kubernetes
Kubernetes multiplie le problème du container unique par un plan de contrôle, un réseau plat et un système d’autorisation et chacun d’eux a un défaut commode plutôt que sûr.
Les clés du cluster entier sont au nombre de trois: un pod qui peut atteindre l’hôte, un octroi RBAC trop large et un réseau qui laisse chaque pod parler à tous les autres.
Durcissez ces trois-là et les attaques exotiques perdent leur première marche.
Laissez-en une ouverte et le reste du durcissement est de la décoration, parce qu’un pod privilégié, un binding cluster-admin ou un etcd joignable sont déjà la fin de la partie.
Les pods : empêcher l’un de devenir le nœud
Un pod qui tourne en privilégié, monte un chemin hôte ou partage le namespace PID ou réseau de l’hôte n’est pas isolé du nœud et le nœud fait tourner les identifiants kubelet qui atteignent le cluster. Les contrôles au niveau pod sont donc là où une compromission de container est empêchée de devenir une compromission de nœud:
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
Les specs dangereuses sont le miroir de cela: privileged: true, hostPID/hostNetwork: true et surtout un volume hostPath montant /, /var/lib/kubelet ou /etc/kubernetes, dont chacun remet au pod le système de fichiers du nœud et avec lui les certificats du cluster.
Imposez la bonne forme plutôt que de l’espérer, avec la Pod Security Admission intégrée réglée sur restricted sur chaque namespace qui n’a pas de raison documentée d’être plus lâche:
# label de namespace — l'admission rejette les pods non conformes
pod-security.kubernetes.io/enforce: restricted
Les Pod Security Standards ont remplacé l’ancienne PodSecurityPolicy et restricted encode l’essentiel du securityContext ci-dessus en règle d’admission, de sorte qu’un développeur ne peut livrer un pod privilégié dans ce namespace en oubliant le contexte.
Pour ce que les niveaux intégrés ne savent pas exprimer, un moteur de politiques (Kyverno, OPA Gatekeeper) est la couche suivante, mais commencez par l’intégré, qui est gratuit.
RBAC : l’octroi est le rayon d’impact
Le RBAC est là où les clusters se perdent en silence, parce qu’un octroi trop large est invisible jusqu’à ce que quelqu’un l’utilise.
Les règles sont brutes: personne ni rien n’obtient cluster-admin sans besoin prouvé, aucun Role n’emploie un verbe ou une ressource en wildcard et une poignée de verbes sont des primitives d’escalade qui méritent leur propre examen.
create pods dans un namespace avec un compte de service privilégié est un accès au nœud; get secrets est chaque identifiant du namespace; et impersonate, escalate et bind laissent un principal s’octroyer plus qu’il n’a, ce qui transforme un pied modeste en n’importe quel rôle qu’il peut nommer.
Un compte de service reçoit un Role cadré sur les ressources et verbes exacts que sa charge appelle, lié dans son propre namespace et rien à l’échelle du cluster sauf s’il opère vraiment à l’échelle du cluster:
kind: Role
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"] # pas "*", pas "secrets"
Les jetons de compte de service par défaut sont l’autre exposition discrète: un pod qui n’appelle pas l’API ne devrait pas porter un jeton qu’il peut faire fuir, donc posez automountServiceAccountToken: false par défaut et optez pour l’inclusion là où l’API est réellement utilisée.
Réseau : le défaut est tout-autorisé, donc default-deny
Le réseau plat des pods est le défaut que personne ne retient: sans aucune NetworkPolicy en place, chaque pod peut atteindre tous les autres, à travers les namespaces, y compris les services proches du plan de contrôle et les bases. Un seul front-end compromis peut donc scanner les ports et atteindre tout l’intérieur du cluster. La correction est une politique default-deny par namespace, puis des autorisations explicites pour les flux qui doivent exister:
kind: NetworkPolicy
metadata: { name: default-deny, namespace: app }
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"] # refuse les deux jusqu'à ce qu'une politique permette
Cela ne marche que si le CNI applique les NetworkPolicy (Calico, Cilium le font; certains non), chose à vérifier plutôt qu’à supposer. Une fois le default-deny en place, un pod compromis n’atteint que ce que vous lui avez explicitement autorisé, c’est-à-dire la segmentation que le réseau plat refuse autrement.
Secrets et etcd : le base64 n’est pas du chiffrement
Un Secret Kubernetes est encodé en base64, pas chiffré et par défaut il siège dans etcd sous cette forme, ce qui veut dire qu’un accès à etcd ou une sauvegarde de celui-ci, est chaque secret en clair. Deux choses suivent. Activez le chiffrement au repos pour que l’API server chiffre les secrets avant qu’ils ne touchent etcd, idéalement avec un fournisseur KMS plutôt qu’une clé locale:
# EncryptionConfiguration référencée par l'API server
resources:
- resources: ["secrets"]
providers:
- kms: { name: cluster-kms, endpoint: ... }
- identity: {}
Et gardez les secrets hors des endroits qui les font fuir: pas en variables d’environnement (lisibles par l’API et tout RCE dans le pod), pas en ConfigMaps (pas secrètes du tout) et de plus en plus pas dans etcd du tout mais tirées à l’exécution d’un magasin externe par le Secrets Store CSI driver. etcd lui-même est le joyau, donc il reçoit du TLS pair et client, son propre réseau pare-feu et des sauvegardes chiffrées, parce qu’une sauvegarde etcd lisible est une compromission de cluster qui survit au cluster.
Les bords du plan de contrôle
Deux points méritent une vérification explicite parce que leurs réglages non sûrs ont été de vrais incidents.
L’API server ne doit pas autoriser l’authentification anonyme vers quoi que ce soit de significatif et le kubelet est une seconde API facile à laisser sous-authentifiée: --anonymous-auth=false et --authorization-mode=Webhook sur le kubelet ferment le chemin où un pod parle au kubelet de son nœud et obtient l’exécution de commandes sur chaque pod présent.
La journalisation d’audit sur l’API server est le relevé de qui a fait quoi et elle est coupée ou minimale par défaut.
Les images bouclent la boucle comme pour Docker simple: scanner dans le pipeline, signer avec cosign et vérifier à l’admission pour qu’une image non signée ou vulnérable ne puisse être planifiée, plutôt que de la découvrir en train de tourner.
Où atterrir
Imposez la Pod Security restricted sur chaque namespace pour qu’aucun pod n’atteigne le nœud par le privilège ou un hostPath.
Cadrez chaque octroi RBAC sur les verbes et ressources exacts, gardez cluster-admin presque vide et surveillez secrets, impersonate, escalate et bind comme les verbes d’escalade qu’ils sont.
Posez une NetworkPolicy default-deny dans chaque namespace sur un CNI qui l’applique, parce que le réseau plat est tout-autorisé jusqu’à ce que vous disiez le contraire.
Chiffrez les secrets au repos ou tirez-les d’un magasin externe et protégez etcd comme la copie en clair de tout ce qu’il est.
Puis le durcissement ordinaire de container et d’image par-dessus.
Le motif dans tout cela est le même: Kubernetes a un défaut ouvert et chaque contrôle ici est vous en train de fermer une porte précise qu’il a laissée entrouverte.