Vai al contenuto principale

Admission Webhook for K8s

1. Panoramica e documenti per Casbin K8s-Gatekeeper

Casbin K8s-GateKeeper è un webhook di ammissione Kubernetes che integra Casbin come strumento di Controllo di Accesso. Utilizzando Casbin K8s-GateKeeper, puoi stabilire regole flessibili per autorizzare o intercettare qualsiasi operazione su risorse K8s, SENZA scrivere alcun pezzo di codice, ma solo poche righe di configurazioni dichiarative di modelli e politiche di Casbin, che fanno parte del linguaggio ACL (Access Control List) di Casbin.

Casbin K8s-GateKeeper è sviluppato e mantenuto dalla comunità Casbin. Il repository di questo progetto è disponibile qui: https://github.com/casbin/k8s-gatekeeper

0.1 Un Esempio Semplice

Ad esempio, non è necessario scrivere alcun codice, ma utilizzare le seguenti righe di configurazione per ottenere questa funzione: "Proibire l'uso di immagini con alcuni tag specificati in qualsiasi distribuzione":

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

Questi sono scritti nel linguaggio ordinario Casbin ACL. Supponendo che tu abbia già letto i capitoli su di essi, sarà molto facile da capire.

Casbin K8s-GateKeeper presenta i seguenti vantaggi:

  • Facile da usare. Scrivere diverse righe di ACL è molto meglio che scrivere molto codice.
  • Permette aggiornamenti dinamici delle configurazioni. Non è necessario spegnere l'intero plugin per modificare le configurazioni.
  • È flessibile. È possibile creare regole arbitrarie su qualsiasi risorsa K8s, che possono essere esplorate con kubectl gatekeeper.
  • Semplifica l'implementazione del webhook di ammissione K8s, che è molto complicato. Non è necessario sapere cos'è il webhook di ammissione K8s o come scrivere codice per esso. Tutto ciò che devi fare è conoscere la risorsa su cui vuoi mettere vincoli e poi scrivere ACL di Casbin. Tutti sanno che K8s è complesso, ma utilizzando Casbin K8s-Gatekeeper, puoi risparmiare tempo.
  • È mantenuto dalla comunità di Casbin. Sentiti libero di contattarci se qualcosa riguardo questo plugin ti confonde o se incontri problemi durante il tentativo.

1.1 Come funziona Casbin K8s-Gatekeeper?

K8s-Gatekeeper è un webhook di ammissione per K8s che utilizza Casbin per applicare regole di controllo degli accessi arbitrarie definite dall'utente, aiutando a prevenire qualsiasi operazione su K8s che l'amministratore non desidera.

Casbin è una potente e efficiente libreria open-source per il controllo degli accessi. Fornisce supporto per l'applicazione dell'autorizzazione basata su vari modelli di controllo degli accessi. Per maggiori dettagli su Casbin, consulta Panoramica.

I webhook di ammissione in K8s sono callback HTTP che ricevono richieste di ammissione e fanno qualcosa con esse. In particolare, K8s-Gatekeeper è un tipo speciale di webhook di ammissione: 'ValidatingAdmissionWebhook', che può decidere se accettare o rifiutare questa richiesta di ammissione o meno. Per quanto riguarda le richieste di ammissione, sono richieste HTTP che descrivono un'operazione su risorse specifiche di K8s (ad esempio, la creazione/eliminazione di una distribuzione). Per ulteriori informazioni sui webhook di ammissione, consulta la documentazione ufficiale di K8s.

1.2 Un Esempio Illustrativo di Come Funziona

Ad esempio, quando qualcuno desidera creare un deployment contenente un pod che esegue nginx (utilizzando kubectl o i client K8s), K8s genererà una richiesta di ammissione, che (se tradotta in formato YAML) potrebbe essere simile a questa:

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

Questa richiesta passerà attraverso il processo di tutti i middleware mostrati nell'immagine, incluso il nostro K8s-Gatekeeper. K8s-Gatekeeper può rilevare tutti gli enforcer di Casbin memorizzati in etcd di K8s, che viene creato e mantenuto dall'utente (tramite kubectl o il client Go che forniamo). Ogni enforcer contiene un modello Casbin e una politica Casbin. La richiesta di ammissione sarà elaborata da ogni enforcer, uno alla volta, e solo superando tutti gli enforcer una richiesta può essere accettata da questo K8s-Gatekeeper.

(Se non capisci cosa sia un enforcer Casbin, un modello o una politica, consulta questo documento: Iniziare).

Ad esempio, per qualche motivo, l'amministratore vuole vietare l'uso dell'immagine 'nginx:1.14.1' mentre permette 'nginx:1.3.1'. Può essere creato un enforcer contenente la seguente regola e politica (Spiegheremo come creare un enforcer, cosa siano questi modelli e politiche e come scriverli nel seguente capitolo).

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

Creando un enforcer contenente il modello e la politica sopra menzionati, la precedente richiesta di ammissione verrà rifiutata da questo enforcer, il che significa che K8s non creerà questo deployment.

2 Installa K8s-gatekeeper

Esistono tre metodi disponibili per l'installazione di K8s-gatekeeper: Webhook esterno, Webhook interno e Helm.

nota

Nota: Questi metodi sono destinati solo agli utenti che desiderano provare K8s-gatekeeper e non sono sicuri. Se desideri utilizzarlo in un ambiente produttivo, ti preghiamo di assicurarti di aver letto Capitolo 5. Impostazioni avanzate e di apportare eventuali modifiche necessarie prima dell'installazione.

2.1 Webhook interno

2.1.1 Passo 1: Costruire l'immagine

Per il metodo del webhook interno, il webhook stesso sarà implementato come servizio all'interno di Kubernetes. Per creare il servizio e la distribuzione necessari, è necessario creare un'immagine di K8s-gatekeeper. Puoi creare la tua immagine eseguendo il seguente comando:

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

Questo comando creerà un'immagine locale chiamata 'k8s-gatekeeper:latest'.

nota

Nota: Se stai utilizzando minikube, esegui eval $(minikube -p minikube docker-env) prima di eseguire 'docker build'.

2.1.2 Passo 2: Configurare servizi e distribuzioni per K8s-gatekeeper

Esegui i seguenti comandi:

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

Questo farà partire l'esecuzione di K8s-gatekeeper, e puoi confermarlo eseguendo kubectl get pods.

2.1.3 Passo 3: Installare le risorse CRD per K8s-gatekeeper

Esegui i seguenti comandi:

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

2.2 Webhook esterno

Per il metodo del webhook esterno, K8s-gatekeeper sarà in esecuzione al di fuori di Kubernetes, e Kubernetes accederà a K8s-gatekeeper come farebbe per un sito web regolare. Kubernetes ha un requisito obbligatorio che il webhook di ammissione deve essere HTTPS. A scopo di prova di K8s-gatekeeper, abbiamo fornito un set di certificati e una chiave privata (anche se questo non è sicuro). Se preferisci utilizzare il tuo certificato, consulta Capitolo 5. Impostazioni avanzate per istruzioni su come regolare il certificato e la chiave privata.

Il certificato che forniamo è emesso per 'webhook.domain.local'. Quindi, modifica l'host (ad esempio, /etc/hosts) e indirizza 'webhook.domain.local' all'indirizzo IP su cui K8s-gatekeeper è in esecuzione.

Quindi esegui il seguente comando:

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 Installa K8s-gatekeeper tramite Helm

2.3.1 Passo 1: Costruisci l'immagine

Si prega di fare riferimento a Capitolo 2.1.1.

2.3.2 Installazione di Helm

Esegui il comando helm install k8sgatekeeper ./k8sgatekeeper.

3. Prova K8s-gatekeeper

3.1 Creare Modello e Policy di Casbin

Hai due metodi per creare un modello e una policy: tramite kubectl o tramite il go-client che forniamo.

3.1.1 Creare/Aggiornare Modello e Policy di Casbin tramite kubectl

In K8s-gatekeeper, il modello di Casbin è memorizzato in una risorsa CRD chiamata 'CasbinModel'. La sua definizione si trova in config/auth.casbin.org_casbinmodels.yaml.

Ci sono esempi in example/allowed_repo/model.yaml. Presta attenzione ai seguenti campi:

  • metadata.name: il nome del modello. Questo nome DEVE essere lo stesso del nome dell'oggetto CasbinPolicy correlato a questo modello, in modo che K8s-gatekeeper possa accoppiarli e creare un enforcer.
  • spec.enable: se questo campo è impostato su "false", questo modello (così come l'oggetto CasbinPolicy correlato a questo modello) sarà ignorato.
  • spec.modelText: una stringa che contiene il testo del modello di un modello Casbin.

La Politica Casbin è memorizzata in un'altra risorsa CRD chiamata 'CasbinPolicy', la cui definizione può essere trovata in config/auth.casbin.org_casbinpolicies.yaml.

Ci sono esempi in example/allowed_repo/policy.yaml. Presta attenzione ai seguenti campi:

  • metadata.name: il nome della politica. Questo nome DEVE essere identico al nome dell'oggetto CasbinModel correlato a questa policy, in modo che K8s-gatekeeper possa accoppiarli e creare un enforcer.
  • spec.policyItem: una stringa che contiene il testo della policy di un modello Casbin.

Dopo aver creato i tuoi file CasbinModel e CasbinPolicy, utilizza il seguente comando per applicarli:

kubectl apply -f <filename>

Una volta creato un accoppiamento di CasbinModel e CasbinPolicy, K8s-gatekeeper sarà in grado di rilevarlo entro 5 secondi.

3.1.2 Creare/Aggiornare il Modello e la Policy Casbin tramite il go-client che forniamo

Capiamo che potrebbero esserci situazioni in cui non è conveniente utilizzare la shell per eseguire comandi direttamente su un nodo del cluster K8s, come quando si sta costruendo una piattaforma cloud automatica per la tua azienda. Pertanto, abbiamo sviluppato un go-client per creare e mantenere CasbinModel e CasbinPolicy.

La libreria go-client si trova in pkg/client.

In client.go, forniamo una funzione per creare un client.

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

Il parametro externalClient determina se K8s-gatekeeper è in esecuzione all'interno del cluster K8s o meno.

In model.go, forniamo varie funzioni per creare, eliminare e modificare CasbinModel. Puoi scoprire come utilizzare queste interfacce in model_test.go.

In policy.go, forniamo varie funzioni per creare, eliminare e modificare CasbiPolicy. Puoi scoprire come utilizzare queste interfacce in policy_test.go.

3.1.2 Prova se K8s-gatekeeper funziona

Supponiamo che tu abbia già creato il modello e la politica esatti in example/allowed_repo. Ora, prova il seguente comando:

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

Dovresti scoprire che K8s rifiuterà questa richiesta e menzionerà che il webhook è il motivo per cui questa richiesta è stata rifiutata. Tuttavia, quando provi ad applicare example/allowed_repo/testcase/approve_2.yaml, sarà accettato.

4. Come Scrivere Modelli e Politiche con K8s-gatekeeper

Prima di tutto, assicurati di essere familiare con la grammatica di base dei Modelli e delle Politiche di Casbin. Se non lo sei, per favore leggi prima la sezione Iniziare. In questo capitolo, supponiamo che tu già comprenda cosa sono i Modelli e le Politiche di Casbin.

4.1 Definizione della Richiesta del Modello

Quando K8s-gatekeeper autorizza una richiesta, l'input è sempre un oggetto: l'oggetto Go della Richiesta di Admissione. Ciò significa che l'enforcer sarà sempre utilizzato in questo modo:

ok, err := enforcer.Enforce(admission)

dove admission è un oggetto AdmissionReview definito dall'API ufficiale di Go di K8s "k8s.io/api/admission/v1". Puoi trovare la definizione di questa struct in questo repository: https://github.com/kubernetes/api/blob/master/admission/v1/types.go. Per ulteriori informazioni, puoi anche fare riferimento a https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-request-and-response.

Pertanto, per qualsiasi modello utilizzato da K8s-gatekeeper, la definizione di request_definition dovrebbe sempre essere come questa:

    [request_definition]
r = obj

Il nome 'obj' non è obbligatorio, purché il nome sia coerente con il nome utilizzato nella parte [matchers].

4.2 Matchers del Modello

Si suppone che si utilizzi la funzionalità ABAC di Casbin per scrivere le proprie regole. Tuttavia, l'evaluator di espressioni integrato in Casbin non supporta l'indicizzazione in mappe o array (slice), né l'espansione di array. Pertanto, K8s-gatekeeper fornisce varie 'funzioni Casbin' come estensioni per implementare queste funzionalità. Se ritieni che la tua richiesta non possa essere soddisfatta da queste estensioni, sentiti libero di aprire un problema (issue) o creare una richiesta di pull (pull request).

Se non sei familiare con le funzioni Casbin, puoi fare riferimento a Funzione per ulteriori informazioni.

Ecco le funzioni di estensione:

4.2.1 Funzioni di Estensione

4.2.1.1 access

Access viene utilizzato per risolvere il problema che Casbin non supporta l'indicizzazione in mappe o array. L'esempio example/allowed_repo/model.yaml dimostra l'uso di questa funzione:

[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

In questo matcher, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") è equivalente a r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Image, dove r.obj.Request.Object.Object.Spec.Template.Spec.Containers è una slice.

Access può anche chiamare funzioni semplici che non hanno parametri e restituiscono un singolo valore. L'esempio example/container_resource_limit/model.yaml dimostra questo:

[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)

In questo matcher, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value") è equivalente a r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits["cpu"].Value(), dove r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits è una mappa, e Value() è una funzione semplice che non ha parametri e restituisce un singolo valore.

4.2.1.2 accessWithWildcard

A volte, potresti avere una richiesta del genere: tutti gli elementi in un array devono avere un prefisso "aaa". Tuttavia, Casbin non supporta i cicli for. Con accessWithWildcard e la funzionalità di "espansione mappa/slice", puoi facilmente implementare una tale richiesta.

Ad esempio, supponiamo che a.b.c sia un array [aaa,bbb,ccc,ddd,eee], allora il risultato di accessWithWildcard(a,"b","c","*") sarà una slice [aaa,bbb,ccc,ddd,eee]. Utilizzando il carattere jolly *, la slice viene espansa.

Allo stesso modo, il carattere jolly può essere utilizzato più di una volta. Ad esempio, il risultato di accessWithWildcard(a,"b","c","*","*") sarà [a.b.c[0][0], a.b.c[0][1], ..., a.b.c[1][0], a.b.c[1][1], ...].

4.2.1.3 Funzioni che supportano argomenti a lunghezza variabile

Nell'espressione dell'evaluator di Casbin, quando un parametro è un array, verrà automaticamente espanso come argomento a lunghezza variabile. Utilizzando questa funzionalità per supportare l'espansione di array/slice/mappe, abbiamo anche integrato diverse funzioni che accettano un array/slice come parametro:

  • contain(): accetta più parametri e restituisce se qualche parametro (eccetto l'ultimo parametro) è uguale all'ultimo parametro.
  • split(a,b,c...,sep,index): restituisce una slice che contiene [splits(a,sep)[index], splits(b,sep)[index], splits(a,sep)[index], ...].
  • len(): restituisce la lunghezza dell'argomento a lunghezza variabile.
  • matchRegex(a, b, c..., regex): restituisce se tutti i parametri dati (a, b, c, ...) corrispondono alla regex data.

Ecco un esempio in 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)

Supponendo che accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers, "*", "Image") restituisca ["a:b", "c:d", "e:f", "g:h"], poiché splits supporta argomenti a lunghezza variabile e esegue l'operazione di split su ciascun elemento, verrà selezionato e restituito l'elemento all'indice 1. Pertanto, split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers, "*", "Image"), ":", 1) restituisce ["b", "d", "f", "h"]. E contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers, "*", "Image"), ":", 1), p.obj) restituisce se p.obj è contenuto in ["b", "d", "f", "h"].

4.2.1.2 Funzioni di Conversione del Tipo

  • ParseFloat(): Converte un numero intero in un float (ciò è necessario perché qualsiasi numero utilizzato in un confronto deve essere convertito in un float).
  • ToString(): Converte un oggetto in una stringa. Questo oggetto deve avere un tipo di base stringa (ad esempio, un oggetto di tipo XXX quando c'è una dichiarazione type XXX string).
  • IsNil(): Restituisce se il parametro è nil.

5. Impostazioni Avanzate

5.1 Informazioni sui Certificati

In Kubernetes (k8s), è obbligatorio che un webhook utilizzi HTTPS. Ci sono due approcci per raggiungere questo obiettivo:

  • Utilizzare certificati autofirmati (gli esempi in questo repository utilizzano questo metodo)
  • Utilizzare un certificato normale

5.1.1 Certificati autofirmati

Utilizzare un certificato autofirmato significa che l'Autorità di Certificazione (CA) che emette il certificato non è una delle CA ben note. Pertanto, devi far conoscere questa CA a k8s.

Attualmente, l'esempio in questo repository utilizza una CA autocostruita, la cui chiave privata e il certificato sono memorizzati rispettivamente in config/certificate/ca.crt e config/certificate/ca.key. Il certificato per il webhook è config/certificate/server.crt, che è emesso dalla CA autocostruita. I domini di questo certificato sono "webhook.domain.local" (per il webhook esterno) e "casbin-webhook-svc.default.svc" (per il webhook interno).

Le informazioni sulla CA vengono passate a k8s tramite i file di configurazione del webhook. Sia config/webhook_external.yaml che config/webhook_internal.yaml hanno un campo chiamato "CABundle", che contiene una stringa codificata in base64 del certificato della CA.

Nel caso in cui sia necessario modificare il certificato/dominio (ad esempio, se desideri inserire questo webhook in un altro namespace di k8s utilizzando un webhook interno, o se desideri modificare il dominio utilizzando un webhook esterno), è necessario seguire le seguenti procedure:

  1. Generare una nuova CA:

    • Generare la chiave privata per la CA fittizia:

      openssl genrsa -des3 -out ca.key 2048
    • Rimuovi la protezione tramite password della chiave privata:

      openssl rsa -in ca.key -out ca.key
  2. Genera una chiave privata per il server webhook:

    openssl genrsa -des3 -out server.key 2048
    openssl rsa -in server.key -out server.key
  3. Utilizza la CA autogenerata per firmare il certificato per il webhook:

    • Copia il file di configurazione di openssl del tuo sistema per uso temporaneo. Puoi scoprire la posizione del file di configurazione eseguendo openssl version -a, di solito chiamato openssl.cnf.

    • Nel file di configurazione:

      • Trova il paragrafo [req] e aggiungi la seguente riga: req_extensions = v3_req

      • Trova il paragrafo [v3_req] e aggiungi la seguente riga: subjectAltName = @alt_names

      • Aggiungi le seguenti righe al file:

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

        Nota: Sostituisci 'casbin-webhook-svc.default.svc' con il nome del servizio reale del tuo servizio se decidi di modificare il nome del servizio.

    • Utilizza il file di configurazione modificato per generare un file di richiesta di certificato:

      openssl req -new -nodes -keyout server.key -out server.csr -config openssl.cnf
    • Utilizza la CA auto-prodotta per rispondere alla richiesta e firmare il certificato:

      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. Sostituisci il campo 'CABundle': Aggiorna questo campo con il nuovo certificato.

  5. Se stai utilizzando helm, è necessario applicare modifiche simili ai grafici helm.

5.1.2 Certificati legali

Se utilizzi certificati legali, non è necessario seguire tutte queste procedure. Rimuovi il campo "CABundle" in config/webhook_external.yaml e config/webhook_internal.yaml, e modifica il dominio in questi file con il dominio di tua proprietà.