Passer au contenu principal

Admission Webhook for K8s

1. Aperçu & Documents pour Casbin K8s-Gatekeeper

Casbin K8s-GateKeeper est un webhook d'admission Kubernetes qui intègre Casbin comme 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 un seul morceau de code, mais seulement quelques lignes de configurations déclaratives de modèles et de politiques 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 l'utilisation d'images avec certains tags spécifiés dans tous 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

Ceux-ci sont en langage ACL ordinaire de Casbin. Supposons que vous ayez déjà lu des chapitres à leur sujet, il sera très facile de comprendre.

Casbin K8s-Gatekeeper a les avantages suivants :

  • Facile à utiliser. Écrire plusieurs lignes d'ACL est bien mieux que d'écrire beaucoup de code.
  • Il permet des mises à jour à chaud des configurations. Vous n'avez pas besoin de fermer l'ensemble du plugin pour modifier les configurations.
  • Il est flexible. Des règles arbitraires peuvent être faites sur n'importe quelle ressource K8s, qui peuvent être explorées avec kubectl gatekeeper.
  • Il 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 voulez mettre des contraintes et ensuite écrire l'ACL de Casbin. Tout le monde sait que K8s est complexe, mais en utilisant Casbin K8s-Gatekeeper, votre temps peut être économisé.
  • Il est maintenu par la communauté Casbin. N'hésitez pas à nous contacter si quelque chose à propos de ce plugin vous confond ou si vous rencontrez des problèmes lors de son essai.

1.1 Comment fonctionne Casbin K8s-Gatekeeper ?

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 pour aider à prévenir toute opération sur K8s que l'administrateur ne souhaite pas.

Casbin est une bibliothèque de contrôle d'accès open-source puissante et efficace. Il fournit un support pour l'application de l'autorisation basée sur divers modèles de contrôle d'accès. Pour plus de détails sur Casbin, voir Aperçu.

Les webhooks d'admission dans K8s sont des callbacks HTTP qui reçoivent des 'demandes 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 demande d'admission ou non. Quant aux demandes d'admission, ce sont des requêtes HTTP décrivant une opération sur des ressources spécifiées de K8s (par exemple, la création/suppression d'un déploiement). Pour en savoir plus sur les webhooks d'admission, consultez la documentation officielle de K8s.

1.2 Un exemple illustrant comment cela fonctionne

Par exemple, lorsque quelqu'un veut créer un déploiement contenant un pod exécutant nginx (en utilisant kubectl ou les clients K8s), K8s générera une demande d'admission, qui (si elle est 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 demande passera par le processus de tous les intergiciels montrés dans l'image, y compris notre K8s-Gatekeeper. K8s-Gatekeeper peut détecter tous les exécuteurs 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 exécuteur contient un modèle Casbin et une politique Casbin. La demande d'admission sera traitée par chaque exécuteur, un par un, et seule une demande passant tous les exécuteurs peut être acceptée par ce K8s-Gatekeeper.

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

Par exemple, pour une raison quelconque, l'administrateur veut interdire l'apparition de l'image 'nginx:1.14.1' tout en autorisant 'nginx:1.3.1'. Un exécuteur contenant la règle et la politique suivantes peut être créé (Nous expliquerons comment créer un exécuteur, 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 exécuteur contenant le modèle et la politique ci-dessus, la demande d'admission précédente sera rejetée par cet exécuteur, 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 : External webhook, Internal webhook, et Helm.

note

Note : Ces méthodes sont uniquement destinées aux utilisateurs qui souhaitent essayer K8s-gatekeeper et ne sont pas sécurisées. Si vous souhaitez l'utiliser dans un environnement productif, veuillez vous assurer que vous lisez Chapitre 5. Paramètres avancés et effectuez les modifications nécessaires avant l'installation.

2.1 Webhook interne

2.1.1 Étape 1 : Construire l'image

Pour la méthode de webhook interne, le webhook lui-même sera mis en œuvre en tant que 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 d'exécuter '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 commencera à exécuter 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 de webhook externe, K8s-gatekeeper sera exécuté en dehors de Kubernetes, et Kubernetes accédera à K8s-gatekeeper comme il accéderait à un site web ordinaire. Kubernetes a une exigence obligatoire que le webhook d'admission doit être HTTPS. Dans le but d'essayer 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 au Chapitre 5. Paramètres avancés pour des instructions sur l'ajustement du certificat et de la clé privée.

Le certificat que nous fournissons est émis pour 'webhook.domain.local'. Donc, 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.

Puis 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 au Chapitre 2.1.1.

2.3.2 Installation 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 client-go 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 le même que le nom de l'objet CasbinPolicy lié à ce modèle, afin que K8s-gatekeeper puisse les associer et créer un exécuteur.
  • 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: the name of the policy. This name MUST be the same as the name of the CasbinModel object related to this policy, so that K8s-gatekeeper can pair them and create an enforcer.
  • spec.policyItem: a string that contains the policy text of a Casbin model.

After creating your own CasbinModel and CasbinPolicy files, use the following command to apply them:

kubectl apply -f <filename>

Once a pair of CasbinModel and CasbinPolicy is created, K8s-gatekeeper will be able to detect it within 5 seconds.

3.1.2 Create/Update Casbin Model and Policy via the go-client we provide

We understand that there may be situations where it is not convenient to use the shell to execute commands directly on a node of the K8s cluster, such as when you are building an automatic cloud platform for your corporation. Therefore, we have developed a go-client to create and maintain CasbinModel and CasbinPolicy.

The go-client library is located in pkg/client.

In client.go, we provide a function to create a client.

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

The externalClient parameter determines whether K8s-gatekeeper is running inside the K8s cluster or not.

In model.go, we provide various functions to create, delete, and modify CasbinModel. You can find out how to use these interfaces in model_test.go.

In policy.go, we provide various functions to create, delete, and modify CasbiPolicy. You can find out how to use these interfaces in policy_test.go.

3.1.2 Try Whether K8s-gatekeeper Works

Suppose you have already created the exact model and policy in example/allowed_repo. Now, try the following command:

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

You should find that K8s will reject this request and mention that the webhook was the reason why this request is rejected. However, when you try to apply example/allowed_repo/testcase/approve_2.yaml, it will be accepted.

4. How to Write Model and Policy with K8s-gatekeeper

First of all, make sure you are familiar with the basic grammar of Casbin Models and Policies. If you are not, please read the Get Started section first. In this chapter, we assume that you already understand what Casbin Models and Policies are.

4.1 Request Definition of Model

When K8s-gatekeeper is authorizing a request, the input is always an object: the Go object of the Admission Request. This means that the enforcer will always be used like this:

ok, err := enforcer.Enforce(admission)

where admission is an AdmissionReview object defined by K8s's official go api "k8s.io/api/admission/v1". You can find the definition of this struct in this repository: 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 Correspondances 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 cartes ou les tableaux (tranches), ni l'expansion des tableaux. Par conséquent, K8s-gatekeeper fournit diverses 'fonctions Casbin' en tant qu'extensions pour implémenter 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, ou à créer une demande de tirage.

Si vous n'êtes pas familier avec les fonctions Casbin, vous pouvez vous référer à 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 cartes ou les tableaux. L'exemple example/allowed_repo/model.yaml dé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 tranche.

Access peut également appeler des fonctions simples qui n'ont pas de paramètres et renvoient 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 carte, et Value() est une fonction simple qui n'a pas de paramètres et renvoie une seule valeur.

4.2.1.2 accessWithWildcard

Parfois, vous pouvez 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 carte/tranche", 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 tranche [aaa,bbb,ccc,ddd,eee]. En utilisant le joker *, la tranche 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 étendu en tant qu'argument de longueur variable. En utilisant cette fonctionnalité pour prendre en charge l'expansion de tableau/tranche/carte, nous avons également intégré plusieurs fonctions qui acceptent un tableau/tranche en tant que paramètre :

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

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

4.2.1.2 Fonctions de conversion de type

  • ParseFloat(): Convertit un entier en flottant (c'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(): Renvoie 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) qui délivre le certificat n'est pas l'une des CA bien connues. Par conséquent, vous devez faire connaître cette CA à k8s.

Actuellement, l'exemple dans ce dépôt utilise une CA auto-faite, dont la clé privée et le certificat sont stockés dans config/certificate/ca.crt et config/certificate/ca.key respectivement. Le certificat pour le webhook est config/certificate/server.crt, qui est délivré par la CA auto-faite. 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 sur la CA sont transmises à k8s via les fichiers de configuration du webhook. Les deux config/webhook_external.yaml et config/webhook_internal.yaml ont un champ appelé "CABundle", qui contient une chaîne encodée en base64 du certificat de la CA.

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

  1. Générer une nouvelle CA:

    • Générez la clé privée pour le faux CA :

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

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

    openssl genrsa -des3 -out server.key 2048
    openssl rsa -in server.key -out server.key
  3. Utilisez le CA auto-généré pour signer le certificat pour le webhook :

    • Copiez le fichier de configuration openssl de votre système pour une utilisation 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 au fichier :

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

        Note : Remplacez 'casbin-webhook-svc.default.svc' par le vrai nom de service 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 le CA auto-fait 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' : Les deux config/webhook_external.yaml et config/webhook_internal.yaml ont un champ appelé "CABundle", qui contient une chaîne encodée en base64 du certificat du CA. Mettez à jour ce champ avec le nouveau certificat.

  5. Si vous utilisez helm, des modifications similaires doivent être appliquées aux graphiques de 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.