25 mai 2023
Kubernetes est un élément critique des infrastructures, rendant obligatoire de bonnes pratiques de sécurité. Mais le control plane de Kubernetes n’offre pas la possibilité de définir des politiques de sécurité strictes. Kyverno est pour nous le meilleur outil pour imposer des règles de sécurité.
Théorie
Généralités
Dans tout l’article, nous allons utiliser le vocabulaire associé aux ressources Kyverno : Policy, Rule, ...
Kyverno est donc un policy engine pour Kubernetes. Il permet de :
- Définir des policies en tant que ressources Kubernetes ;
- Valider, modifier ou générer des ressources à la volée via ces policies ;
- Bloquer des ressources non conformes grâce à un admission controller ;
- Journaliser les violations de policies dans des rapports.
Avantages
- Définir des policies de sécurité pour interdire la création de ressources non sécurisées ;
- Simplifier la vie des Ops via des mutations de ressources à la volée ;
- Possibilité de configurer les policies en mode audit (sans blocage) ou enforce ;
- Une écriture de policy simple (par rapport à GateKeeper notamment)
Inconvénients
- Difficile de créer des policies avec une logique très spécifique et/ou complexe ;
- Kyverno est un Single Point Of Failure. Certains connaissent le côté sombre des adminssion controller : si les pods Kyverno ne sont plus disponibles, plus aucune ressource Kubernetes ne peut se déployer sur le cluster. Je vous donnerai quelques astuces pour éviter tout problème dans la suite de l’article.
Kubernetes Webhook
Kyverno fonctionne en tant que dynamic admission controller dans le cluster Kubernetes.
Le webhook Kyverno reçoit des requêtes de l'API server lors des étapes de "validating admission" et "mutating admission" :
Policy & Rule
Une Policy Kyverno est composée des champs suivant (pour plus d'infos: kubectl explain policy.spec
) :
rules
: une ou plusieurs rules définissent la policybackground
: si true, la policy s'applique à toutes les ressources Kubernetes existantes du cluster, sinon elle s'applique seulement aux nouvelles ressourcesvalidationFailureAction
: le mode d'action de la policy : audit ou enforce
Une Rule contient les champs suivants (pour plus d'infos : kubectl explain policy.spec.rules
) :
match
: pour sélectionner les ressourcesexclude
(optionnel) : pour exclure des ressources de la sélectionmutate
,validate
,generate
ouverifyImages
: selon le type de policy, permet de modifier, valider, générer une ressource ou vérifier la signature d’une image (en bêta)
Audit vs Enforce
Kyverno dispose de 2 modes de fonctionnements(validationFailureAction
) :
- audit : ne bloque aucun déploiement, mais génère un rapport indiquant quand les policies spécifiées ne sont pas respectées et pourquoi
- enforce : bloque complètement la création de ressources ne respectant pas les policies
Policy Report
Les rapports de Policies (Policy Reports) sont des ressources Kubernetes qui peuvent être listées simplement :
kubectl get policyreport -A
Pour un namespace donné, on peut lister les violations de policy avec la commande :
kubectl describe polr polr-ns-default | grep "Result: \\+fail" -B10
Installation
Kyverno peut être installé sur les clusters via un simple chart Helm. Rien de plus simple, c’est toute la puissance de Kubernetes :
kelm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno --namespace kyverno --create-namespace --values values.yaml
Voici les points importants à considérer dans les values.yaml
du chart :
---
# 3 replicas for High Availability
replicaCount: 3
# Necessary in EKS with custom Network CNI plugin
# https://cert-manager.io/docs/installation/compatibility/#aws-eks
hostNetwork: true
config:
webhooks:
# Exclude namespaces from scope
- namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values:
- kube-system
- kyverno
- calico-system
# Exclude objects from scope
- objectSelector:
matchExpressions:
- key: webhooks.kyverno.io/exclude
operator: DoesNotExist
Quelques remarques à propos de l’installation :
- L'accès au host network est nécessaire si vous utilisez EKS
- Kyverno doit être configuré avec au moins 3 replicas pour assurer sa haute disponibilité
- Les namespaces
kube-system
etkyverno
sont whitelistés afin de ne pas bloquer le déploiement de ressources Kubernetes critiques (kube-proxy, weave, ...).
Exemple de policy
Une liste d’exemples simples est fournie dans la documentation de Kyverno.
J’aimerais vous présenter un cas d’usage un peu plus avancé : la gestion dynamique des droits RBAC. Voici le cas d’usage que nous avons rencontré. Nous avons mis en place chez un client des environnements de développement à la volée dans Kubernetes.
Nous avons autorisé les développeurs, via un job de CI Gitlab, à tester leurs applications dans des environnements créés à la volée. Ces environnements sont dans des namespaces dédiés créés également à la volée.
Comment fournir au runner Gitlab associé des droits RBAC sur des namespaces qui n’existent pas encore ? Malheureusement, Kubernetes ne permet pas cela via RBAC, mais avec Kyverno, c’est très simple.
Il suffit en effet :
- De donner au runner les droits RBAC de créer des namespaces
- De donner les droits RBAC sur ce namespace via une Policy Kyverno : une Mutation Policy peut simplement créer une RoleBinding en réaction à la création du namespace
Voici les détails d’implémentation :
- Le compte de service k8s
gitlab-runner-ephemeral-env
est uniquement autorisé à créer des namespaces
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab-runner-ephemeral-env
labels:
app: gitlab-runner-ephemeral-env
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gitlab-runner-ephemeral-env
labels:
app: gitlab-runner-ephemeral-env
rules:
- apiGroups: ["*"]
resources: ["namespaces"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitlab-runner-ephemeral-env
labels:
app: gitlab-runner-ephemeral-env
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gitlab-runner-ephemeral-env
subjects:
- kind: ServiceAccount
name: gitlab-runner-ephemeral-env
namespace: gitlab
- À la création d’un namespace, un rolebinding est créé entre lui et le ClusterRole
cluster-admin
via une ClusterPolicy Kyverno
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-rbac-rules-env-volee
annotations:
policies.kyverno.io/title: Add RBAC permissions for ephemeral environments.
policies.kyverno.io/category: Multi-Tenancy
policies.kyverno.io/subject: RBAC
policies.kyverno.io/description: >-
Add RBAC rules when a namespace is created by a specific gitlab runner (gitlab-runner-env-volee), useful for ephemeral
environments.
spec:
background: false
rules:
- name: create-rbac
match:
resources:
kinds:
- Namespace
subjects:
- kind: ServiceAccount
name: gitlab-runner-ephemeral-env
namespace: gitlab
generate:
kind: RoleBinding
name: ephemeral-namespace-admin
namespace: ""
synchronize: true
data:
subjects:
- kind: ServiceAccount
name: gitlab-runner-ephemeral-env
namespace: gitlab
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
Limites de Kyverno
Je vais vous détailler dans cette partie plusieurs problèmes rencontrés lors de l’implémentation de Kyverno. Outre le fait que Kyverno soit un SPOF sur tous les namespaces qu’il surveille, les policies sont assez compliquées à écrire et à débugguer. Sans compter que Kyverno peut avoir des effets de bords avec d’autres outils comme ArgoCD.
Les policies sont complexes à écrire
Globalement, les policies Kyverno peuvent être assez difficiles à écrire. La documentation présente beaucoup d’exemples, mais tout le mécanisme de filtrage et de mutation des ressources peut être un peu déroutant au début.
Prenons un exemple concert. On veut interdire le paramètre privileged: true
sauf à deux types de pods (comme sur le schéma suivant) :
- Les pods dans le namespace
debug
- Les pods dans le namespace
gitlab
dont le nom comment parrunner
En suivant la documentation, on est tenté d’écrire la policy suivante :
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged-containers
annotations:
policies.kyverno.io/category: Pod Security Standards (Baseline)
policies.kyverno.io/severity: medium
policies.kyverno.io/subject: Pod
policies.kyverno.io/description: >-
Privileged mode disables most security mechanisms and must not be allowed. This policy
ensures Pods do not call for privileged mode.
spec:
validationFailureAction: audit
background: true
rules:
- name: priviledged-containers
match:
resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- "debug"
# Whitelisting
- resources:
namespaces:
- "gitlab"
names:
- "runner-*"
validate:
message: >-
Privileged mode is disallowed. The fields spec.containers[*].securityContext.privileged
and spec.initContainers[*].securityContext.privileged must not be set to true.
pattern:
spec:
=(initContainers):
- =(securityContext):
=(privileged): "false"
containers:
- =(securityContext):
=(privileged): "false"
Cette policy ne fonctionne pas, le mécanisme de filtrage n’est pas effectif. Après quelques recherches, voici le fix à appliquer :
18,20c18,21
< resources:
< kinds:
< - Pod
---
> all:
> - resources:
> kinds:
> - Pod
Rien n’indique dans la documentation un changement de comportement entre ces deux manières de filtrer des ressources. Pas facile de debug une policy qui ne fonctionne pas... heureusement, la communauté est active, et quelqu’un nous a rapidement proposé la solution sur Slack.
Attention aux Mutation Webhook
D’expérience, il faut toujours faire attention aux Mutation Webhooks, qui peuvent porter à confusion les équipes DevOps. Les Mutations Webhooks Kubernetes induisent par nature une différence entre les ressources spécifiées et les ressources réellement déployées sur le cluster.
Si un Ops n’est pas au fait de l’existence de ces mutations, il peut perdre beaucoup de temps à comprendre pourquoi telle ou telle ressource apparaît ou possède certains attributs.
De la même façon, si un cluster possède un nombre trop important de MutationPolicy, il peut y avoir des incompatibilités entre les policies, ou des effets de bords difficiles à identifier.
Je recommande d’utiliser les Mutations Webhook avec parcimonie, et de les documenter très clairement. Cela peut être extrêmement pratique (e.g : ajouter l’adresse d’un proxy HTTP en variable d’environnement de tous les pods d’un namespace), mais il vaut mieux éviter si possible d’en abuser.
Effets de bords avec ArgoCD
Nous avons également rencontré quelques difficultés avec les clusters Kubernetes dont la CD est gérée via ArgoCD.
Lorsqu’une policy Kyverno est créée et concerne une ressource qui déploie des conteneurs, comme des pods, Kyverno modifie intelligemment les rules
pour que les policies prennent en compte tous les types de ressource Kubernetes qui déploie des conteneurs.
Par exemple, si on crée cette policy :
apiVersion : kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: enforce
rules:
- name: validate-registries
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images may only come from our internal enterprise registry."
pattern:
spec:
containers:
- image: "registry.domain.com/*"
Kyverno va modifier la policy à la volée via un Mutation Webhook comme ceci :
aspec:
background: true
failurePolicy: Fail
rules:
- match:
any:
- resources:
kinds:
- Pod
name: validate-registries
validate:
message: Images may only come from our internal enterprise registry.
pattern:
spec:
containers:
- image: registry.domain.com/*
- match:
any:
- resources:
kinds:
- DaemonSet
- Deployment
- Job
- StatefulSet
name: autogen-validate-registries
validate:
message: Images may only come from our internal enterprise registry.
pattern:
spec:
template:
spec:
containers:
- image: registry.domain.com/*
- match:
any:
- resources:
kinds:
- CronJob
name: autogen-cronjob-validate-registries
validate:
message: Images may only come from our internal enterprise registry.
pattern:
spec:
jobTemplate:
spec:
template:
spec:
containers:
- image: registry.domain.com/*
validationFailureAction: enforce
Que se passe-t-il dans le cas où la policy Kyverno a été créée via Argo ? Argo va détecter un changement entre le fichier Yaml de la policy déclarée et la ressource effectivement déployée dans le cluster. On assiste alors à un va-et-vient permanent entre Argo et Kyverno, qui modifient tour à tour la Policy Kyverno.
Pour indiquer à Argo que ces changements ne sont pas à prendre en compte, il suffit d’utiliser le mot-clé ignoreDifferences
dans l’application Argo :
ignoreDifferences:
# Kyverno auto-generates rules to make policies smarter. We want ArgoCD to
# ignore the auto-generated rules.
# For more information: https://kyverno.io/docs/writing-policies/autogen/
- group: kyverno.io
kind: ClusterPolicy
jqPathExpressions:
- .spec.rules[] | select( .name | startswith("autogen-") )
Conclusion
Voilà, vous savez désormais en quoi consiste Kyverno, comment l’installer, puis l’utiliser pour sécuriser votre cluster Kubernetes ! Encore une fois, utilisez les Mutation Webhook avec parcimonie, testez bien vos policies en mode audit au préalable, et n’hésitez pas à contacter la communauté en cas de problème.