Passer au contenu principal

Admission Webhook for K8s

1. Aperçu et documents pour Casbin K8s-Gatekeeper

Casbin K8s-GateKeeper est un webhook d'admission Kubernetes qui intègre Casbin en tant qu'outil de contrôle d'accès. En utilisant Casbin K8s-GateKeeper, vous pouvez établir des règles flexibles pour autoriser ou intercepter toute opération sur les ressources K8s, SANS écrire une seule ligne de code, mais seulement quelques lignes de configurations déclaratives des modèles et politiques de Casbin, qui font partie du langage ACL (Access Control List) de Casbin.

Casbin K8s-GateKeeper est développé et maintenu par la communauté Casbin. Le dépôt de ce projet est disponible ici : https://github.com/casbin/k8s-gatekeeper

0.1 Un exemple simple

Par exemple, vous n'avez pas besoin d'écrire de code, mais utilisez les lignes de configuration suivantes pour réaliser cette fonction : "Interdire les images avec certaines balises spécifiées d'être utilisées dans les déploiements" :

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(${OBJECT}.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

And Policy:

p, "1.14.1",deny

Ce sont en langage ACL ordinaire de Casbin. Supposons que vous avez déjà lu les chapitres à leur sujet, ce sera très facile à comprendre.

Casbin K8s-Gatekeeper présente les avantages suivants :

  • Facile à utiliser. Écrire quelques lignes d'ACL est bien meilleur que d'écrire beaucoup de code.
  • Il permet des mises à jour à chaud des configurations. Vous n'avez pas besoin de fermer le plugin entier pour modifier les configurations.
  • C'est flexible. Des règles arbitraires peuvent être établies sur n'importe quelle ressource K8s, qui peuvent être explorées avec kubectl gatekeeper.
  • Cela simplifie la mise en œuvre du webhook d'admission K8s, qui est très compliqué. Vous n'avez pas besoin de savoir ce qu'est un webhook d'admission K8s ou comment écrire du code pour cela. Tout ce que vous avez à faire est de connaître la ressource sur laquelle vous souhaitez imposer des contraintes, puis d'écrire une ACL Casbin. Tout le monde sait que K8s est complexe, mais en utilisant Casbin K8s-Gatekeeper, vous pouvez gagner du temps.
  • Il est maintenu par la communauté Casbin. N'hésitez pas à nous contacter si quelque chose concernant ce plugin vous perturbe ou si vous rencontrez des problèmes lors de sa mise en œuvre.

1.1 Comment Casbin K8s-Gatekeeper fonctionne-t-il ?

K8s-Gatekeeper est un webhook d'admission pour K8s qui utilise Casbin pour appliquer des règles de contrôle d'accès définies par l'utilisateur afin d'empêcher toute opération sur K8s que l'administrateur ne souhaite pas.

Casbin est une bibliothèque open-source puissante et efficace pour le contrôle d'accès. Elle offre un support pour appliquer l'autorisation basée sur divers modèles de contrôle d'accès. Pour plus de détails sur Casbin, consultez Vue d'ensemble.

Les webhooks d'admission dans K8s sont des callbacks HTTP qui reçoivent des 'requêtes d'admission' et font quelque chose avec elles. En particulier, K8s-Gatekeeper est un type spécial de webhook d'admission : 'ValidatingAdmissionWebhook', qui peut décider d'accepter ou de rejeter cette requête d'admission ou non. Quant aux requêtes d'admission, ce sont des requêtes HTTP décrivant une opération sur des ressources spécifiées de K8s (par exemple, créer/supprimer un déploiement). Pour plus d'informations sur les webhooks d'admission, consultez la documentation officielle de K8s.

1.2 Un exemple illustrant son fonctionnement

Par exemple, lorsque quelqu'un souhaite créer un déploiement contenant un pod exécutant nginx (en utilisant kubectl ou les clients K8s), K8s génère une requête d'admission, qui (si traduite en format YAML) peut ressembler à ceci :

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.1
ports:
- containerPort: 80

Cette requête passera par le processus de tous les middlewares indiqués dans l'image, y compris notre K8s-Gatekeeper. K8s-Gatekeeper peut détecter tous les enforcers Casbin stockés dans l'etcd de K8s, qui est créé et maintenu par l'utilisateur (via kubectl ou le client Go que nous fournissons). Chaque enforcer contient un modèle Casbin et une politique Casbin. La demande d'admission sera traitée par chaque enforcer, un par un, et seulement en passant tous les enforcers une demande peut être acceptée par ce K8s-Gatekeeper.

(Si vous ne comprenez pas ce qu'est un enforcer Casbin, un modèle ou une politique, consultez ce document : Commencer).

Par exemple, pour une raison quelconque, l'administrateur souhaite interdire l'apparition de l'image 'nginx:1.14.1' tout en autorisant 'nginx:1.3.1'. Un enforcer contenant la règle et la politique suivantes peut être créé (Nous expliquerons comment créer un enforcer, ce que sont ces modèles et politiques, et comment les écrire dans les chapitres suivants).

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

Policy:

p, "nginx:1.13.1",allow
p, "nginx:1.14.1",deny

En créant un enforcer contenant le modèle et la politique ci-dessus, la demande d'admission précédente sera rejetée par cet enforcer, ce qui signifie que K8s ne créera pas ce déploiement.

2 Installer K8s-gatekeeper

Il existe trois méthodes disponibles pour installer K8s-gatekeeper : Webhook externe, Webhook interne, et Helm.

note

Remarque : Ces méthodes sont uniquement destinées aux utilisateurs pour essayer K8s-gatekeeper et ne sont pas sécurisées. Si vous souhaitez l'utiliser dans un environnement productif, veuillez vous assurer de lire Chapitre 5. Paramètres avancés et d'apporter les modifications nécessaires avant l'installation.

2.1 Webhook interne

2.1.1 Étape 1 : Construire l'image

Pour la méthode du webhook interne, le webhook lui-même sera implémenté comme un service au sein de Kubernetes. Pour créer le service et le déploiement nécessaires, vous devez construire une image de K8s-gatekeeper. Vous pouvez construire votre propre image en exécutant la commande suivante :

docker build --target webhook -t k8s-gatekeeper .

Cette commande créera une image locale appelée 'k8s-gatekeeper:latest'.

note

Note : Si vous utilisez minikube, veuillez exécuter eval $(minikube -p minikube docker-env) avant de lancer 'docker build'.

2.1.2 Étape 2 : Configurer les services et les déploiements pour K8s-gatekeeper

Exécutez les commandes suivantes :

kubectl apply -f config/rbac.yaml
kubectl apply -f config/webhook_deployment.yaml
kubectl apply -f config/webhook_internal.yaml

Cela démarrera K8s-gatekeeper, et vous pouvez le confirmer en exécutant kubectl get pods.

2.1.3 Étape 3 : Installer les ressources CRD pour K8s-gatekeeper

Exécutez les commandes suivantes :

kubectl apply -f config/auth.casbin.org_casbinmodels.yaml 
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml

2.2 Webhook externe

Pour la méthode du webhook externe, K8s-gatekeeper sera exécuté en dehors de Kubernetes, et Kubernetes accédera à K8s-gatekeeper comme il accéderait à un site web régulier. Kubernetes a une exigence obligatoire que le webhook d'admission doit être HTTPS. Dans le but de tester K8s-gatekeeper, nous avons fourni un ensemble de certificats et une clé privée (bien que cela ne soit pas sécurisé). Si vous préférez utiliser votre propre certificat, veuillez vous référer à Chapitre 5. Paramètres avancés pour obtenir des instructions sur la modification du certificat et de la clé privée.

Le certificat que nous fournissons est émis pour 'webhook.domain.local'. Par conséquent, modifiez l'hôte (par exemple, /etc/hosts) et pointez 'webhook.domain.local' vers l'adresse IP sur laquelle K8s-gatekeeper est en cours d'exécution.

Ensuite, exécutez la commande suivante :

go mod tidy
go mod vendor
go run cmd/webhook/main.go
kubectl apply -f config/auth.casbin.org_casbinmodels.yaml
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml
kubectl apply -f config/webhook_external.yaml

2.3 Installer K8s-gatekeeper via Helm

2.3.1 Étape 1 : Construire l'image

Veuillez vous référer à Chapitre 2.1.1.

2.3.2 Installation via Helm

Exécutez la commande helm install k8sgatekeeper ./k8sgatekeeper.

3. Essayez K8s-gatekeeper

3.1 Créer un modèle et une politique Casbin

Vous avez deux méthodes pour créer un modèle et une politique : via kubectl ou via le go-client que nous fournissons.

3.1.1 Créer/Mettre à jour le modèle et la politique Casbin via kubectl

Dans K8s-gatekeeper, le modèle Casbin est stocké dans une ressource CRD appelée 'CasbinModel'. Sa définition se trouve dans config/auth.casbin.org_casbinmodels.yaml.

Il y a des exemples dans example/allowed_repo/model.yaml. Faites attention aux champs suivants :

  • metadata.name : le nom du modèle. Ce nom DOIT être identique au nom de l'objet CasbinPolicy lié à ce modèle, afin que K8s-gatekeeper puisse les associer et créer un enforcer.
  • spec.enable : si ce champ est défini sur "false", ce modèle (ainsi que l'objet CasbinPolicy lié à ce modèle) sera ignoré.
  • spec.modelText : une chaîne qui contient le texte du modèle d'un modèle Casbin.

La politique Casbin est stockée dans une autre ressource CRD appelée 'CasbinPolicy', dont la définition peut être trouvée dans config/auth.casbin.org_casbinpolicies.yaml.

Il y a des exemples dans example/allowed_repo/policy.yaml. Faites attention aux champs suivants :

  • metadata.name : le nom de la politique. Ce nom DOIT être identique au nom de l'objet CasbinModel lié à cette politique, afin que K8s-gatekeeper puisse les associer et créer un enforcer.
  • spec.policyItem : une chaîne qui contient le texte de la politique d'un modèle Casbin.

Après avoir créé vos propres fichiers CasbinModel et CasbinPolicy, utilisez la commande suivante pour les appliquer :

kubectl apply -f <filename>

Une fois qu'une paire de CasbinModel et CasbinPolicy est créée, K8s-gatekeeper sera capable de la détecter dans les 5 secondes.

3.1.2 Créer/Mettre à jour le modèle Casbin et la politique via le go-client que nous fournissons

Nous comprenons qu'il peut y avoir des situations où il n'est pas pratique d'utiliser le shell pour exécuter des commandes directement sur un nœud du cluster K8s, comme lorsque vous construisez une plateforme cloud automatique pour votre entreprise. Par conséquent, nous avons développé un go-client pour créer et maintenir CasbinModel et CasbinPolicy.

La bibliothèque go-client se trouve dans pkg/client.

Dans client.go, nous fournissons une fonction pour créer un client.

func NewK8sGateKeeperClient(externalClient bool) (*K8sGateKeeperClient, error) 

Le paramètre externalClient détermine si K8s-gatekeeper est en cours d'exécution à l'intérieur du cluster K8s ou non.

Dans model.go, nous fournissons diverses fonctions pour créer, supprimer et modifier CasbinModel. Vous pouvez découvrir comment utiliser ces interfaces dans model_test.go.

Dans policy.go, nous fournissons diverses fonctions pour créer, supprimer et modifier CasbinPolicy. Vous pouvez découvrir comment utiliser ces interfaces dans policy_test.go.

3.1.2 Essayez si K8s-gatekeeper fonctionne

Supposons que vous avez déjà créé le modèle exact et la politique dans example/allowed_repo. Maintenant, essayez la commande suivante :

kubectl apply -f example/allowed_repo/testcase/reject_1.yaml

Vous devriez constater que K8s refusera cette demande et mentionnera que le webhook est la raison pour laquelle cette demande est rejetée. Cependant, lorsque vous essayez d'appliquer example/allowed_repo/testcase/approve_2.yaml, il sera accepté.

4. Comment écrire un modèle et une politique avec K8s-gatekeeper

Tout d'abord, assurez-vous de connaître la grammaire de base des modèles et des politiques Casbin. Si ce n'est pas le cas, veuillez d'abord lire la section Démarrer. Dans ce chapitre, nous supposons que vous comprenez déjà ce que sont les Modèles et les Politiques de Casbin.

4.1 Définition de la Requête du Modèle

Lorsque K8s-gatekeeper autorise une requête, l'entrée est toujours un objet : l'objet Go de la Requête d'Admission. Cela signifie que l'exécuteur sera toujours utilisé de cette manière :

ok, err := enforcer.Enforce(admission)

admission est un objet AdmissionReview défini par l'API Go officielle de K8s "k8s.io/api/admission/v1". Vous pouvez trouver la définition de cette structure dans ce dépôt : https://github.com/kubernetes/api/blob/master/admission/v1/types.go. Pour plus d'informations, vous pouvez également vous référer à https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-request-and-response.

Par conséquent, pour tout modèle utilisé par K8s-gatekeeper, la définition de request_definition devrait toujours être comme ceci :

    [request_definition]
r = obj

Le nom 'obj' n'est pas obligatoire, tant que le nom est cohérent avec le nom utilisé dans la partie [matchers].

4.2 Matchers du Modèle

Vous êtes censé utiliser la fonctionnalité ABAC de Casbin pour écrire vos règles. Cependant, l'évaluateur d'expression intégré dans Casbin ne prend pas en charge l'indexation dans les maps ou les tableaux (slices), ni l'expansion des tableaux. Par conséquent, K8s-gatekeeper fournit diverses 'fonctions Casbin' en tant qu'extensions pour mettre en œuvre ces fonctionnalités. Si vous constatez toujours que votre demande ne peut être satisfaite par ces extensions, n'hésitez pas à ouvrir un problème (issue) ou à créer une pull request.

Si vous n'êtes pas familier avec les fonctions Casbin, vous pouvez consulter Function pour plus d'informations.

Voici les fonctions d'extension :

4.2.1 Fonctions d'extension

4.2.1.1 accès

L'accès est utilisé pour résoudre le problème que Casbin ne prend pas en charge l'indexation dans les maps ou les tableaux. L'exemple example/allowed_repo/model.yaml montre l'utilisation de cette fonction :

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

Dans ce comparateur, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") est équivalent à r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Image, où r.obj.Request.Object.Object.Spec.Template.Spec.Containers est une slice.

L'accès peut également appeler des fonctions simples qui n'ont pas de paramètres et retournent une seule valeur. L'exemple example/container_resource_limit/model.yaml démontre cela :

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value")) >= parseFloat(p.cpu) && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","memory","Value")) >= parseFloat(p.memory)

Dans ce comparateur, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value") est équivalent à r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits["cpu"].Value(), où r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits est une map, et Value() est une fonction simple qui n'a pas de paramètres et retourne une seule valeur.

4.2.1.2 accessWithWildcard

Parfois, vous pourriez avoir une demande comme celle-ci : tous les éléments d'un tableau doivent avoir un préfixe "aaa". Cependant, Casbin ne prend pas en charge les boucles for. Avec accessWithWildcard et la fonctionnalité "expansion de map/slice", vous pouvez facilement mettre en œuvre une telle demande.

Par exemple, supposons que a.b.c soit un tableau [aaa,bbb,ccc,ddd,eee], alors le résultat de accessWithWildcard(a,"b","c","*") sera une slice [aaa,bbb,ccc,ddd,eee]. En utilisant le joker *, la slice est étendue.

De même, le joker peut être utilisé plus d'une fois. Par exemple, le résultat de accessWithWildcard(a,"b","c","*","*") sera [a.b.c[0][0], a.b.c[0][1], ..., a.b.c[1][0], a.b.c[1][1], ...].

4.2.1.3 Fonctions prenant en charge les arguments de longueur variable

Dans l'évaluateur d'expression de Casbin, lorsqu'un paramètre est un tableau, il sera automatiquement développé en argument de longueur variable. En utilisant cette fonctionnalité pour prendre en charge l'expansion de tableau/slice/map, nous avons également intégré plusieurs fonctions qui acceptent un tableau/slice en paramètre :

  • contain() : accepte plusieurs paramètres et retourne si l'un des paramètres (à l'exception du dernier) est égal au dernier paramètre.
  • split(a,b,c...,sep,index) : retourne un slice qui contient [splits(a,sep)[index], splits(b,sep)[index], splits(a,sep)[index], ...].
  • len() : retourne la longueur de l'argument de longueur variable.
  • matchRegex(a,b,c...,regex) : retourne si tous les paramètres donnés (a, b, c, ...) correspondent à l'expression régulière donnée.

Voici un exemple dans example/disallowed_tag/model.yaml :

    [matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

En supposant que accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image") retourne ["a:b", "c:d", "e:f", "g:h"], car splits supporte les arguments de longueur variable et effectue l'opération de split sur chaque élément, l'élément à l'index 1 sera sélectionné et retourné. Par conséquent, split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) retourne ["b","d","f","h"]. Et contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj) retourne si p.obj est contenu dans ["b","d","f","h"].

4.2.1.2 Fonctions de Conversion de Type

  • ParseFloat() : Analyse un entier en un flottant (cela est nécessaire car tout nombre utilisé dans une comparaison doit être converti en flottant).
  • ToString() : Convertit un objet en chaîne de caractères. Cet objet doit avoir un type de base de chaîne (par exemple, un objet de type XXX lorsqu'il y a une déclaration type XXX string).
  • IsNil() : Retourne si le paramètre est nil.

5. Paramètres Avancés

5.1 À Propos des Certificats

Dans Kubernetes (k8s), il est obligatoire qu'un webhook utilise HTTPS. Il existe deux approches pour y parvenir :

  • Utiliser des certificats auto-signés (les exemples dans ce dépôt utilisent cette méthode)
  • Utiliser un certificat normal

5.1.1 Certificats auto-signés

Utiliser un certificat auto-signé signifie que l'Autorité de Certification (CA) émettrice du certificat n'est pas l'une des CA bien connues. Par conséquent, vous devez informer k8s de cette CA.

Actuellement, l'exemple dans ce dépôt utilise une CA auto-réalisée, dont la clé privée et le certificat sont stockés respectivement dans config/certificate/ca.crt et config/certificate/ca.key. Le certificat pour le webhook se trouve dans config/certificate/server.crt, qui est émis par l'autorité de certification auto-créée. Les domaines de ce certificat sont "webhook.domain.local" (pour le webhook externe) et "casbin-webhook-svc.default.svc" (pour le webhook interne).

Les informations concernant l'autorité de certification sont transmises à k8s via les fichiers de configuration du webhook. Les deux fichiers config/webhook_external.yaml et config/webhook_internal.yaml contiennent un champ appelé "CABundle", qui contient une chaîne encodée en base64 du certificat de l'autorité de certification.

Au cas où vous auriez besoin de changer le certificat/domaine (par exemple, si vous souhaitez placer ce webhook dans un autre namespace de k8s tout en utilisant un webhook interne, ou si vous souhaitez changer le domaine tout en utilisant un webhook externe), les procédures suivantes devraient être suivies :

  1. Générer une nouvelle autorité de certification :

    • Générer la clé privée pour l'autorité de certification factice :

      openssl genrsa -des3 -out ca.key 2048
    • Supprimer la protection par mot de passe de la clé privée :

      openssl rsa -in ca.key -out ca.key
  2. Générer une clé privée pour le serveur du webhook :

    openssl genrsa -des3 -out server.key 2048
    openssl rsa -in server.key -out server.key
  3. Utiliser l'autorité de certification auto-générée pour signer le certificat du webhook :

    • Copiez le fichier de configuration openssl de votre système pour un usage temporaire. Vous pouvez trouver l'emplacement du fichier de configuration en exécutant openssl version -a, généralement appelé openssl.cnf.

    • Dans le fichier de configuration :

      • Trouvez le paragraphe [req] et ajoutez la ligne suivante : req_extensions = v3_req

      • Trouvez le paragraphe [v3_req] et ajoutez la ligne suivante : subjectAltName = @alt_names

      • Ajoutez les lignes suivantes à la fin du fichier :

        [alt_names]
        DNS.2=<The domain you want>

        Remarque : Remplacez 'casbin-webhook-svc.default.svc' par le nom de service réel de votre propre service si vous décidez de modifier le nom du service.

    • Utilisez le fichier de configuration modifié pour générer un fichier de demande de certificat :

      openssl req -new -nodes -keyout server.key -out server.csr -config openssl.cnf
    • Utilisez l'autorité de certification auto-créée pour répondre à la demande et signer le certificat :

      openssl x509 -req -days 3650 -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -extensions v3_req -extensions SAN -extfile openssl.cnf
  4. Remplacez le champ 'CABundle' : Mettez à jour ce champ avec le nouveau certificat.

  5. Si vous utilisez Helm, des modifications similaires doivent être appliquées aux charts Helm.

5.1.2 Certificats légaux

Si vous utilisez des certificats légaux, vous n'avez pas besoin de passer par toutes ces procédures. Supprimez le champ "CABundle" dans config/webhook_external.yaml et config/webhook_internal.yaml, et changez le domaine dans ces fichiers pour le domaine que vous possédez.