#Kubernetes
#OPA Gatekeeper
#Cybersecurity
Article publié le 1 juil. 2023
·11 min de lecture
- ⚙️ Le cluster est sous Kubernetes 1.26 et installé avec Minikube en utilisant Docker.
- ⚙️ Le client Kubernetes utilisé est
kubectl
à la version 1.26.
Dans le monde complexe de Kubernetes, il est essentiel de maintenir des normes strictes en matière de sécurité et de performances. Deux aspects cruciaux de cette gestion sont la vérification des labels et la mise en place de limites CPU et RAM. Dans cet article, nous explorerons en détail ces deux aspects clés et comment OPA Gatekeeper peut nous aider à les contrôler.
🛡️ Avec OPA Gatekeeper, vous pouvez créer des politiques de sécurité personnalisées qui contrôlent et valident automatiquement les labels attribués à vos ressources. De cette manière, vous pouvez assurer une organisation efficace de vos déploiements et vous conformer aux normes spécifiques de votre entreprise.
Dans les sections suivantes, nous vous guiderons à travers les étapes nécessaires pour intégrer et configurer OPA Gatekeeper dans votre infrastructure Kubernetes. Nous vous fournirons des exemples concrets de politiques de sécurité et de limites CPU, extraites directement du site de GateKeeper.
Quel est l'intérêt d'utiliser ce système d'admission ? 🤔
Bloquer les déploiements mal configurés ❌
Les labels sont des étiquettes attribuées aux ressources Kubernetes telles que les pods, les services et les déploiements. Ils fournissent des métadonnées supplémentaires qui aident à organiser et à filtrer les ressources. Avec OPA Gatekeeper, nous pouvons définir des politiques qui exigent que certaines ressources aient des labels spécifiques.
💡 Par exemple, nous pouvons définir une politique qui exige que tous les déploiements dans un cluster aient un label "owner" spécifiant la personne qui à crée une ressource. Grâce à OPA Gatekeeper, ces politiques peuvent être appliquées de manière centralisée, garantissant ainsi que toutes les ressources sont correctement étiquetées.
Bloquer les pods sans limites de ressources ❌
En ce qui concerne les limites CPU et RAM, Kubernetes offre des mécanismes pour gérer les ressources allouées aux conteneurs.
Les limits
permettent de spécifier la quantité maximale de puissance de calcul qu'un conteneur peut consommer.
Cela aide à éviter qu'un seul conteneur ne monopolise toutes les ressources du cluster.
OPA Gatekeeper peut être utilisé pour définir des politiques qui imposent des limites CPU pour les déploiements.
💡 Par exemple, nous pouvons définir une politique qui exige que tous les déploiements aient une limite CPU maximale de 100M
et
une limite de RAM de 1Gi
, empêchant ainsi les déploiements de consommer trop de ressources et garantissant une répartition équitable des ressources entre les applications.
Configuration du cluster pour Gatekeeper ⚙️
Activation des contrôleurs d'admission
Pour pouvoir contrôler les ressources qui seront déployées dans notre cluster, on devra au préalable vérifier que les Admission Controllers sont activés. On peut vérifier lesquels sont déjà activés par défaut avec la commande :
kube-apiserver -h | grep enable-admission-plugins
⭐️ Pour que Gatekeeper fonctionne, il faut que MutatingAdmissionWebhook
et ValidatingAdmissionWebhook
soient activés.
S'ils ne sont pas activés, voir dans la documentation comment les activés.
💡 Ici la documentation officielle de Kubernetes sur le MutatingAdmissionWebhook et le ValidatingAdmissionWebhook. Mais concrétement, ce sont des types de controllers d'admission intégrer à Kubernetes qui permettent d'attendre un signal (Webhook) avant d'autoriser la création/màj/suppression d'une ressource Kubernetes (Pods, ReplicaSet, ...).
Installation de Gatekeeper
Pour installer Gatekeeper
on peut directement utiliser la commande suivante:
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml
⭐️ Les pods seront créés dans le namespace gatekeeper-system
.
On peut également utiliser helm
pour l'installation:
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts --force-update
helm install gatekeeper/gatekeeper --name-template=gatekeeper --namespace gatekeeper-system --create-namespace
Une fois l'installation effectué, on devra attendre que les pods se lancent:
Configuration de Gatekeeper
Les ConstraintsTemplates sont les éléments de base qui définissent les politiques de sécurité dans Gatekeeper. Les templates sont écrits en Rego, qui est un langage déclaratif supportant Open Policy Agent (OPA). Ils fournissent un modèle pour créer des contraintes spécifiques, permettant ainsi de personnaliser les politiques en fonction de vos besoins. Chaque ConstraintsTemplate comprend plusieurs contraintes (Constraints), qui sont les règles de conformité que vous souhaitez appliquer.
💡 Les divers modèles de ConstraintTemplates sont disponibles sur le site de Gatekeeper !
Les Templates sont des objets Kubernetes utilisés pour créer des instances de ConstraintTemplates. Ils permettent de générer facilement des politiques de sécurité spécifiques à des cas d'utilisation particuliers. En utilisant les Templates, vous pouvez rapidement déployer et réutiliser des politiques de sécurité cohérentes dans votre cluster.
Mise en place de nos politiques de sécurité ✅
Imposer un label aux déploiements
Pour l'exemple, nous avons décider dans notre politique de déploiement que tous nos déploiements doivent contenir le label owner
. Ce label devra contenir le prénom
de la perssone qui crée le déploiement en respectant le format: [owner].mycompany.demo. Si le label n'est pas présent ou que le format n'est pas respecté,
alors le déploiement sera refusé.
On crée donc la ConstraintTemplate qui contiendra notre condition :
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
message:
type: string
labels:
type: array
items:
type: object
properties:
key:
type: string
allowedRegex:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
get_message(parameters, _default) = msg {
not parameters.message
msg := _default
}
get_message(parameters, _default) = msg {
msg := parameters.message
}
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_].key}
missing := required - provided
count(missing) > 0
def_msg := sprintf("you must provide labels: %v", [missing])
msg := get_message(input.parameters, def_msg)
}
violation[{"msg": msg}] {
value := input.review.object.metadata.labels[key]
expected := input.parameters.labels[_]
expected.key == key
expected.allowedRegex != ""
not re_match(expected.allowedRegex, value)
def_msg := sprintf("Label <%v: %v> does not satisfy allowed regex: %v", [key, value, expected.allowedRegex])
msg := get_message(input.parameters, def_msg)
}
On créer un fichier label-env-constraint-template.yml
, on y colle le contenu ci-dessus et on applique à notre cluster :
kubectl apply -f label-env-constraint-template.yml
-
Puis,la Constraint associé qui va définir que le nom exact du label qui doit être systématiquement présent :
apiVersion: constraints.gatekeeper.sh/v1beta1 kind: K8sRequiredLabels metadata: name: dep-must-have-owner spec: match: kinds: - apiGroups: ["apps"] kinds: ["Deployment"] parameters: message: "Les déploiements doivent contenir le label 'owner' en respectant la syntaxe [owner].mycompany.demo" labels: - key: owner allowedRegex: "^[a-zA-Z]+.mycompany.demo$"
On créer un fichier
label-owner-constraint.yml
, on y colle le contenu ci-dessus et on applique à notre cluster :kubectl apply -f label-owner-constraint.yml
Une fois les ressources crées, notre cluster est maintenant prêt à interrompre les déploiements qui ne posséderont pas le label owner
! 🚫
Ci-dessous, le message affiché lorsqu'un déploiement ne respecte pas la condition:
Imposer des limites CPU et RAM aux pods
Comme pour la configuration du label, on devra dans un premier temps créer la ConstraintTemplate suivante:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8scontainerlimits
annotations:
metadata.gatekeeper.sh/title: "Container Limits"
metadata.gatekeeper.sh/version: 1.0.0
spec:
crd:
spec:
names:
kind: K8sContainerLimits
validation:
openAPIV3Schema:
type: object
properties:
exemptImages:
type: array
items:
type: string
cpu:
type: string
memory:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8scontainerlimits
import data.lib.exempt_container.is_exempt
missing(obj, field) = true {
not obj[field]
}
missing(obj, field) = true {
obj[field] == ""
}
canonify_cpu(orig) = new {
is_number(orig)
new := orig * 1000
}
canonify_cpu(orig) = new {
not is_number(orig)
endswith(orig, "m")
new := to_number(replace(orig, "m", ""))
}
canonify_cpu(orig) = new {
not is_number(orig)
not endswith(orig, "m")
re_match("^[0-9]+(\\.[0-9]+)?$", orig)
new := to_number(orig) * 1000
}
# 10 ** 21
mem_multiple("E") = 1000000000000000000000 { true }
# 10 ** 18
mem_multiple("P") = 1000000000000000000 { true }
# 10 ** 15
mem_multiple("T") = 1000000000000000 { true }
# 10 ** 12
mem_multiple("G") = 1000000000000 { true }
# 10 ** 9
mem_multiple("M") = 1000000000 { true }
# 10 ** 6
mem_multiple("k") = 1000000 { true }
# 10 ** 3
mem_multiple("") = 1000 { true }
# Kubernetes accepts millibyte precision when it probably shouldn't.
# https://github.com/kubernetes/kubernetes/issues/28741
# 10 ** 0
mem_multiple("m") = 1 { true }
# 1000 * 2 ** 10
mem_multiple("Ki") = 1024000 { true }
# 1000 * 2 ** 20
mem_multiple("Mi") = 1048576000 { true }
# 1000 * 2 ** 30
mem_multiple("Gi") = 1073741824000 { true }
# 1000 * 2 ** 40
mem_multiple("Ti") = 1099511627776000 { true }
# 1000 * 2 ** 50
mem_multiple("Pi") = 1125899906842624000 { true }
# 1000 * 2 ** 60
mem_multiple("Ei") = 1152921504606846976000 { true }
get_suffix(mem) = suffix {
not is_string(mem)
suffix := ""
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) > 0
suffix := substring(mem, count(mem) - 1, -1)
mem_multiple(suffix)
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) > 1
suffix := substring(mem, count(mem) - 2, -1)
mem_multiple(suffix)
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) > 1
not mem_multiple(substring(mem, count(mem) - 1, -1))
not mem_multiple(substring(mem, count(mem) - 2, -1))
suffix := ""
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) == 1
not mem_multiple(substring(mem, count(mem) - 1, -1))
suffix := ""
}
get_suffix(mem) = suffix {
is_string(mem)
count(mem) == 0
suffix := ""
}
canonify_mem(orig) = new {
is_number(orig)
new := orig * 1000
}
canonify_mem(orig) = new {
not is_number(orig)
suffix := get_suffix(orig)
raw := replace(orig, suffix, "")
re_match("^[0-9]+(\\.[0-9]+)?$", raw)
new := to_number(raw) * mem_multiple(suffix)
}
violation[{"msg": msg}] {
general_violation[{"msg": msg, "field": "containers"}]
}
violation[{"msg": msg}] {
general_violation[{"msg": msg, "field": "initContainers"}]
}
# Ephemeral containers not checked as it is not possible to set field.
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not is_exempt(container)
cpu_orig := container.resources.limits.cpu
not canonify_cpu(cpu_orig)
msg := sprintf("container <%v> cpu limit <%v> could not be parsed", [container.name, cpu_orig])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not is_exempt(container)
mem_orig := container.resources.limits.memory
not canonify_mem(mem_orig)
msg := sprintf("container <%v> memory limit <%v> could not be parsed", [container.name, mem_orig])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not is_exempt(container)
not container.resources
msg := sprintf("container <%v> has no resource limits", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not is_exempt(container)
not container.resources.limits
msg := sprintf("container <%v> has no resource limits", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not is_exempt(container)
missing(container.resources.limits, "cpu")
msg := sprintf("container <%v> has no cpu limit", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not is_exempt(container)
missing(container.resources.limits, "memory")
msg := sprintf("container <%v> has no memory limit", [container.name])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not is_exempt(container)
cpu_orig := container.resources.limits.cpu
cpu := canonify_cpu(cpu_orig)
max_cpu_orig := input.parameters.cpu
max_cpu := canonify_cpu(max_cpu_orig)
cpu > max_cpu
msg := sprintf("container <%v> cpu limit <%v> is higher than the maximum allowed of <%v>", [container.name, cpu_orig, max_cpu_orig])
}
general_violation[{"msg": msg, "field": field}] {
container := input.review.object.spec[field][_]
not is_exempt(container)
mem_orig := container.resources.limits.memory
mem := canonify_mem(mem_orig)
max_mem_orig := input.parameters.memory
max_mem := canonify_mem(max_mem_orig)
mem > max_mem
msg := sprintf("container <%v> memory limit <%v> is higher than the maximum allowed of <%v>", [container.name, mem_orig, max_mem_orig])
}
libs:
- |
package lib.exempt_container
is_exempt(container) {
exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
img := container.image
exemption := exempt_images[_]
_matches_exemption(img, exemption)
}
_matches_exemption(img, exemption) {
not endswith(exemption, "*")
exemption == img
}
_matches_exemption(img, exemption) {
endswith(exemption, "*")
prefix := trim_suffix(exemption, "*")
startswith(img, prefix)
}
⭐️ Cette configuration est extraite de la documentation de GateKeeper.
On applique la ressource à notre cluster:
kubectl apply -f resources-limits-constraint-template.yml
On peut maintenant créer notre Constraint où l'on déterminera selon le besoin, les limites en CPU et en RAM qu'on souhaite établir:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sContainerLimits
metadata:
name: pod-must-have-limits
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
cpu: "200m"
memory: "1Gi"
On applique à notre cluster:
kubectl apply -f resources-limits-constraint.yml
Notre cluster est maintenant configuré pour bloquer la création des ressources Pods s'ils ne possèdent pas de limits définies ainsi que s'ils ne respectent pas les limits configurées ! 🚫
Ci-dessous, les messages affichés si les limits ne sont pas présentes ainsi que si les limits ne sont pas correctes:
Le mot de la fin ⭐️
Voilà, nous avons vu comment utiliser Gatekeeper pour la mise en place de politiques de sécurité dans notre cluster. Ici nous avons vu deux exemples, mais Gatekeeper peut bien évidemment couvrir un spectre plus large. Ce contrôleur constitue une barrière de protection supplémentaire avant qu'une ressoure Kubernetes ne soit déployée dans notre cluster.
💡 Ces politiques peuvent être personnalisées en fonction des besoins, la documentation se trouve sur le site officiel de Gatekeeper où se trouve les librairies par défaut.
Malgrès les possiblitées de blocage offertes par GateKeeper, il est cependant nécessaire pour une sécurité optimale de configurer en amont les politiques d'authentifications (User/Service Account) et d'autorisations (RBAC).
Billy PAYET
Billy est un expert en DevOps qui a une solide expérience en automatisation et en intégration continue. Il utilise des outils tels que Ansible, Jenkins et Docker pour améliorer les processus de déploiement et de livraison des applications. Il est passionné par l'amélioration continue et aider les équipes à atteindre leurs objectifs de qualité et de performance.