Ir al contenido principal

Admission Webhook for K8s

1. Visión general y Documentos para Casbin K8s-Gatekeeper

Casbin K8s-GateKeeper es un webhook de admisión de Kubernetes que integra Casbin como herramienta de Control de Acceso. Al usar Casbin K8s-GateKeeper, puedes establecer reglas flexibles para autorizar o interceptar cualquier operación en recursos de K8s, SIN escribir ninguna línea de código, sino solo varias líneas de configuraciones declarativas de modelos y políticas de Casbin, que son parte del lenguaje de Lista de Control de Acceso (ACL) de Casbin.

Casbin K8s-GateKeeper es desarrollado y mantenido por la comunidad de Casbin. El repositorio de este proyecto está disponible aquí: https://github.com/casbin/k8s-gatekeeper

0.1 Un Ejemplo Simple

Por ejemplo, no necesitas escribir ningún código, sino usar las siguientes líneas de configuración para lograr esta función: "Prohibir que se usen imágenes con ciertas etiquetas especificadas en cualquier despliegue":

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

Estas están en el lenguaje ordinario de ACL de Casbin. Suponiendo que ya has leído capítulos sobre ellos, será muy fácil de entender.

Casbin K8s-Gatekeeper tiene las siguientes ventajas:

  • Fácil de usar. Escribir varias líneas de ACL es mucho mejor que escribir montones de código.
  • Permite actualizaciones en caliente de configuraciones. No necesitas apagar todo el plugin para modificar configuraciones.
  • Es flexible. Se pueden hacer reglas arbitrarias en cualquier recurso de K8s, que se pueden explorar con kubectl gatekeeper.
  • Simplifica la implementación del webhook de admisión de K8s, que es muy complicado. No necesitas saber qué es un webhook de admisión de K8s o cómo escribir código para él. Todo lo que necesitas hacer es conocer el recurso en el que quieres poner restricciones y luego escribir ACL de Casbin. Todos saben que K8s es complejo, pero al usar Casbin K8s-Gatekeeper, tu tiempo puede ser ahorrado.
  • Es mantenido por la comunidad de Casbin. No dudes en contactarnos si algo sobre este plugin te confunde o si encuentras algún problema al intentar esto.

1.1 ¿Cómo Funciona Casbin K8s-Gatekeeper?

K8s-Gatekeeper es un webhook de admisión para K8s que usa Casbin para aplicar reglas de control de acceso definidas por el usuario de manera arbitraria para ayudar a prevenir cualquier operación en K8s que el administrador no desee.

Casbin es una biblioteca de control de acceso de código abierto poderosa y eficiente. Proporciona soporte para hacer cumplir la autorización basada en varios modelos de control de acceso. Para más detalles sobre Casbin, ver Visión general.

Los webhooks de admisión en K8s son callbacks HTTP que reciben 'solicitudes de admisión' y hacen algo con ellas. En particular, K8s-Gatekeeper es un tipo especial de webhook de admisión: 'ValidatingAdmissionWebhook', que puede decidir si aceptar o rechazar esta solicitud de admisión o no. En cuanto a las solicitudes de admisión, son peticiones HTTP que describen una operación en recursos especificados de K8s (por ejemplo, crear/eliminar un despliegue). Para más sobre webhooks de admisión, ver documentación oficial de K8s.

1.2 Un Ejemplo Ilustrativo de Cómo Funciona

Por ejemplo, cuando alguien quiere crear un despliegue que contenga un pod ejecutando nginx (usando kubectl o clientes de K8s), K8s generará una solicitud de admisión, que (si se traduce al formato YAML) puede ser algo así:

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

Esta solicitud pasará por el proceso de todos los middleware mostrados en la imagen, incluyendo nuestro K8s-Gatekeeper. K8s-Gatekeeper puede detectar todos los ejecutores de Casbin almacenados en el etcd de K8s, que es creado y mantenido por el usuario (a través de kubectl o el cliente de Go que proporcionamos). Cada ejecutor contiene un modelo de Casbin y una política de Casbin. La solicitud de admisión será procesada por cada ejecutor, uno por uno, y solo al pasar todos los ejecutores puede una solicitud ser aceptada por este K8s-Gatekeeper.

(Si no entiendes qué es un ejecutor, modelo o política de Casbin, ver este documento: Comenzar).

Por ejemplo, por alguna razón, el administrador quiere prohibir la aparición de la imagen 'nginx:1.14.1' mientras permite 'nginx:1.3.1'. Se puede crear un ejecutor que contenga la siguiente regla y política (Explicaremos cómo crear un ejecutor, qué son estos modelos y políticas, y cómo escribirlos en los siguientes capítulos).

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

Al crear un ejecutor que contenga el modelo y la política anteriores, la solicitud de admisión previa será rechazada por este ejecutor, lo que significa que K8s no creará este despliegue.

2 Instalar K8s-gatekeeper

Hay tres métodos disponibles para instalar K8s-gatekeeper: webhook externo, webhook interno y Helm.

nota

Nota: Estos métodos solo están destinados para que los usuarios prueben K8s-gatekeeper y no son seguros. Si deseas usarlo en un entorno productivo, asegúrate de leer Capítulo 5. Configuraciones avanzadas y hacer las modificaciones necesarias antes de la instalación. 2.1 Webhook interno

2.1.1 Paso 1: Construir la imagen

2.1.1 Paso 1: Construir la imagen

Para el método de webhook interno, el propio webhook se implementará como un servicio dentro de Kubernetes. Para crear el servicio y despliegue necesarios, necesitas construir una imagen de K8s-gatekeeper. Puedes construir tu propia imagen ejecutando el siguiente comando:

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

Este comando creará una imagen local llamada 'k8s-gatekeeper:latest'.

nota

Nota: Si estás utilizando minikube, por favor ejecuta eval $(minikube -p minikube docker-env) antes de correr 'docker build'.

2.1.2 Paso 2: Configurar servicios y despliegues para K8s-gatekeeper

Ejecuta los siguientes comandos:

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

Esto comenzará a ejecutar K8s-gatekeeper, y puedes confirmarlo ejecutando kubectl get pods.

2.1.3 Paso 3: Instalar Recursos CRD para K8s-gatekeeper

Ejecuta los siguientes comandos:

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

2.2 Webhook externo

Para el método de webhook externo, K8s-gatekeeper se ejecutará fuera de Kubernetes, y Kubernetes accederá a K8s-gatekeeper como si accediera a un sitio web regular. Kubernetes tiene un requisito obligatorio de que el webhook de admisión debe ser HTTPS. Con el propósito de probar K8s-gatekeeper, hemos proporcionado un conjunto de certificados y una clave privada (aunque esto no es seguro). Si prefieres usar tu propio certificado, por favor consulta el Capítulo 5. Configuración avanzada para instrucciones sobre cómo ajustar el certificado y la clave privada. El certificado que proporcionamos está emitido para 'webhook.domain.local'.

Entonces, modifica el host (por ejemplo, /etc/hosts) y apunta 'webhook.domain.local' a la dirección IP en la que K8s-gatekeeper está ejecutándose. Luego ejecuta el siguiente comando:

2.3 Instalar K8s-gatekeeper a través de Helm

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.1 Paso 1: Construir la imagen

Por favor consulta el Capítulo 2.1.1.

2.3.2 Instalación de Helm

Ejecuta el comando helm install k8sgatekeeper ./k8sgatekeeper.

Prueba K8s-gatekeeper 3.1 Crear Modelo y Política de Casbin

Tienes dos métodos para crear un modelo y política: a través de kubectl o a través del cliente go que proporcionamos.

3.1.1 Crear/Actualizar Modelo y Política de Casbin a través de kubectl

En K8s-gatekeeper, el modelo de Casbin se almacena en un recurso CRD llamado 'CasbinModel'.

Su definición se encuentra en config/auth.casbin.org_casbinmodels.yaml. Hay ejemplos en example/allowed_repo/model.yaml.

Presta atención a los siguientes campos: metadata.name: el nombre del modelo.

  • Este nombre DEBE ser el mismo que el nombre del objeto CasbinPolicy relacionado con este modelo, para que K8s-gatekeeper pueda emparejarlos y crear un ejecutor. spec.enable: si este campo se establece en 'false', este modelo (así como el objeto CasbinPolicy relacionado con este modelo) será ignorado.
  • spec.modelText: una cadena que contiene el texto del modelo de un modelo Casbin.
  • La Política de Casbin se almacena en otro recurso CRD llamado 'CasbinPolicy', cuya definición se puede encontrar en config/auth.casbin.org_casbinpolicies.yaml.

Hay ejemplos en example/allowed_repo/policy.yaml.

Presta atención a los siguientes campos: metadata.name: el nombre de la política.

  • Este nombre DEBE ser el mismo que el nombre del objeto CasbinModel relacionado con esta política, para que K8s-gatekeeper pueda emparejarlos y crear un ejecutor. spec.policyItem: una cadena que contiene el texto de la política de un modelo Casbin.
  • Después de crear tus propios archivos CasbinModel y CasbinPolicy, usa el siguiente comando para aplicarlos:

Una vez que se crea un par de CasbinModel y CasbinPolicy, K8s-gatekeeper podrá detectarlo en 5 segundos.

kubectl apply -f <filename>

3.1.2 Crear/Actualizar Modelo y Política de Casbin a través del cliente go que proporcionamos

Entendemos que puede haber situaciones en las que no sea conveniente usar la shell para ejecutar comandos directamente en un nodo del clúster de K8s, como cuando estás construyendo una plataforma en la nube automática para tu corporación.

Por lo tanto, hemos desarrollado un cliente go para crear y mantener CasbinModel y CasbinPolicy. La biblioteca del cliente go se encuentra en pkg/client.

En client.go, proporcionamos una función para crear un cliente.

En client.go, proporcionamos una función para crear un cliente.

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

El parámetro externalClient determina si K8s-gatekeeper está ejecutándose dentro del clúster de K8s o no.

En model.go, proporcionamos varias funciones para crear, eliminar y modificar CasbinModel. Puedes descubrir cómo usar estas interfaces en model_test.go.

En policy.go, proporcionamos varias funciones para crear, eliminar y modificar CasbiPolicy. Puedes descubrir cómo usar estas interfaces en policy_test.go.

3.1.2 Prueba si K8s-gatekeeper funciona

Supón que ya has creado el modelo y la política exactos en example/allowed_repo. Ahora, prueba el siguiente comando:

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

Deberías encontrar que K8s rechazará esta solicitud y mencionará que el webhook fue la razón por la cual esta solicitud es rechazada. Sin embargo, cuando intentes aplicar example/allowed_repo/testcase/approve_2.yaml, será aceptado.

4. Cómo Escribir Modelo y Política con K8s-gatekeeper

Primero que nada, asegúrate de estar familiarizado con la gramática básica de los Modelos y Políticas de Casbin. Si no lo estás, por favor lee la sección Get Started primero. En este capítulo, asumimos que ya entiendes qué son los Modelos y Políticas de Casbin.

4.1 Definición de Solicitud de Modelo

Cuando K8s-gatekeeper está autorizando una solicitud, la entrada siempre es un objeto: el objeto Go de la Solicitud de Admisión. Esto significa que el enforcer siempre se usará de esta manera:

ok, err := enforcer.Enforce(admission)

donde admission es un objeto AdmissionReview definido por la api oficial de go de K8s "k8s.io/api/admission/v1". Puedes encontrar la definición de esta estructura en este repositorio: https://github.com/kubernetes/api/blob/master/admission/v1/types.go. Para más información, también puedes referirte a https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-request-and-response.

Por lo tanto, para cualquier modelo utilizado por K8s-gatekeeper, la definición de request_definition siempre debe ser así:

    [request_definition]
r = obj

El nombre 'obj' no es obligatorio, siempre y cuando el nombre sea consistente con el nombre utilizado en la parte [matchers].

4.2 Coincidencias de Modelo

Se supone que debes usar la característica ABAC de Casbin para escribir tus reglas. Sin embargo, el evaluador de expresiones integrado en Casbin no admite indexación en mapas o arreglos (slices), ni la expansión de arreglos. Por lo tanto, K8s-gatekeeper proporciona varias 'funciones de Casbin' como extensiones para implementar estas características. Si aún encuentras que tu demanda no puede ser satisfecha por estas extensiones, no dudes en iniciar un issue, o crear un pull request.

Si no estás familiarizado con las funciones de Casbin, puedes referirte a Function para más información.

Aquí están las funciones de extensión:

4.2.1 Funciones de extensión

4.2.1.1 access

Access se utiliza para resolver el problema de que Casbin no admite indexación en mapas o arreglos. El ejemplo example/allowed_repo/model.yaml demuestra el uso de esta función:

[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

En este coincidente, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") es equivalente a r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Image, donde r.obj.Request.Object.Object.Spec.Template.Spec.Containers es un slice.

Access también puede llamar a funciones simples que no tienen parámetros y devuelven un solo valor. El ejemplo example/container_resource_limit/model.yaml demuestra esto:

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

En este coincidente, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value") es equivalente a r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits["cpu"].Value(), donde r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits es un mapa, y Value() es una función simple que no tiene parámetros y devuelve un solo valor.

4.2.1.2 accessWithWildcard

A veces, puedes tener una demanda como esta: todos los elementos en un arreglo deben tener un prefijo "aaa". Sin embargo, Casbin no admite bucles for. Con accessWithWildcard y la característica de "expansión de map/slice", puedes implementar fácilmente tal demanda.

Por ejemplo, supongamos que a.b.c es un arreglo [aaa,bbb,ccc,ddd,eee], entonces el resultado de accessWithWildcard(a,"b","c","*") será un slice [aaa,bbb,ccc,ddd,eee]. Al usar el comodín *, el slice se expande.

De manera similar, el comodín puede usarse más de una vez. Por ejemplo, el resultado de accessWithWildcard(a,"b","c","*","*") será [a.b.c[0][0], a.b.c[0][1], ..., a.b.c[1][0], a.b.c[1][1], ...].

4.2.1.3 Funciones que admiten argumentos de longitud variable

En el evaluador de expresiones de Casbin, cuando un parámetro es un arreglo, se expandirá automáticamente como un argumento de longitud variable. Utilizando esta característica para admitir la expansión de arreglo/slice/mapa, también hemos integrado varias funciones que aceptan un arreglo/slice como parámetro:

  • contain(): acepta múltiples parámetros y devuelve si algún parámetro (excepto el último parámetro) es igual al último parámetro.
  • split(a,b,c...,sep,index): devuelve un segmento que contiene [splits(a,sep)[index], splits(b,sep)[index], splits(a,sep)[index], ...].
  • len(): devuelve la longitud del argumento de longitud variable.
  • matchRegex(a,b,c...,regex): devuelve si todos los parámetros dados (a, b, c, ...) coinciden con la expresión regular dada.

Aquí hay un ejemplo en 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)

Suponiendo que accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image") devuelve ['a:b', 'c:d', 'e:f', 'g:h'], porque splits soporta argumentos de longitud variable y realiza la operación de división en cada elemento, se seleccionará y devolverá el elemento en el índice 1. Por lo tanto, split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) devuelve ['b','d','f','h']. Y contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj) devuelve si p.obj está contenido en ['b','d','f','h'].

4.2.1.2 Funciones de Conversión de Tipos

  • ParseFloat(): Convierte un entero a un flotante (esto es necesario porque cualquier número utilizado en comparación debe ser convertido en un flotante).
  • ToString(): Convierte un objeto a una cadena de texto. Este objeto debe tener un tipo básico de cadena (por ejemplo, un objeto de tipo XXX cuando hay una declaración type XXX string).
  • IsNil(): Devuelve si el parámetro es nulo.

5. Configuración Avanzada

5.1 Acerca de los Certificados

En Kubernetes (k8s), es obligatorio que un webhook utilice HTTPS. Hay dos enfoques para lograr esto:

  • Usar certificados autofirmados (los ejemplos en este repositorio utilizan este método)
  • Usar un certificado normal

5.1.1 Certificados autofirmados

Usar un certificado autofirmado significa que la Autoridad de Certificación (CA) que emite el certificado no es una de las CA bien conocidas. Por lo tanto, debes informar a k8s sobre esta CA.

Actualmente, el ejemplo en este repositorio utiliza una CA hecha por sí misma, cuya clave privada y certificado se almacenan en config/certificate/ca.crt y config/certificate/ca.key respectivamente. El certificado para el webhook es config/certificate/server.crt, que es emitido por la CA hecha por sí misma. Los dominios de este certificado son "webhook.domain.local" (para webhook externo) y "casbin-webhook-svc.default.svc" (para webhook interno).

La información sobre la CA se pasa a k8s a través de archivos de configuración de webhook. Ambos config/webhook_external.yaml y config/webhook_internal.yaml tienen un campo llamado "CABundle", que contiene una cadena codificada en base64 del certificado de la CA.

En caso de que necesites cambiar el certificado/dominio (por ejemplo, si quieres poner este webhook en otro espacio de nombres de k8s mientras usas un webhook interno, o si quieres cambiar el dominio mientras usas un webhook externo), se deben seguir los siguientes procedimientos:

  1. Generar una nueva CA:

    • Generar la clave privada para la CA falsa:

      openssl genrsa -des3 -out ca.key 2048
    • Eliminar la protección por contraseña de la clave privada:

      openssl rsa -in ca.key -out ca.key
  2. Generar una clave privada para el servidor webhook:

    openssl genrsa -des3 -out server.key 2048
    openssl rsa -in server.key -out server.key
  3. Usar la CA autogenerada para firmar el certificado para el webhook:

    • Copiar el archivo de configuración de openssl de tu sistema para uso temporal. Puedes encontrar la ubicación del archivo de configuración ejecutando openssl version -a, generalmente llamado openssl.cnf.

    • En el archivo de configuración:

      • Encuentra el párrafo [req] y añade la siguiente línea: req_extensions = v3_req

      • Encuentra el párrafo [v3_req] y añade la siguiente línea: subjectAltName = @alt_names

      • Añade las siguientes líneas al archivo:

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

        Nota: Reemplaza 'casbin-webhook-svc.default.svc' con el nombre real del servicio de tu propio servicio si decides modificar el nombre del servicio.

    • Usa el archivo de configuración modificado para generar un archivo de solicitud de certificado:

      openssl req -new -nodes -keyout server.key -out server.csr -config openssl.cnf
    • Usa la CA hecha por sí misma para responder a la solicitud y firmar el certificado:

      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. Reemplazar el campo 'CABundle': Ambos config/webhook_external.yaml y config/webhook_internal.yaml tienen un campo llamado "CABundle", que contiene una cadena codificada en base64 del certificado de la CA.

  5. Actualiza este campo con el nuevo certificado.

Si estás usando helm, se deben aplicar cambios similares a los gráficos de helm.

5.1.2 Certificados legales Elimina el campo "CABundle" en config/webhook_external.yaml y config/webhook_internal.yaml, y cambia el dominio en estos archivos por el dominio que posees.