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 :
Avantages
Inconvénients
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" :
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 enforceUne 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
ou verifyImages
: 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)Kyverno dispose de 2 modes de fonctionnements(validationFailureAction
) :
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
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 :
kube-system
et kyverno
sont whitelistés afin de ne pas bloquer le déploiement de ressources Kubernetes critiques (kube-proxy, weave, ...).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 :
Voici les détails d’implémentation :
gitlab-runner-ephemeral-env
est uniquement autorisé à créer des namespacesapiVersion: 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
cluster-admin
via une ClusterPolicy KyvernoapiVersion: 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
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.
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) :
debug
gitlab
dont le nom comment par runner
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.
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.
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-") )
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.