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: 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: 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óntype 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:
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
Generar una clave privada para el servidor webhook:
openssl genrsa -des3 -out server.key 2048
openssl rsa -in server.key -out server.keyUsar 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 llamadoopenssl.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
Reemplazar el campo 'CABundle': Ambos
config/webhook_external.yaml
yconfig/webhook_internal.yaml
tienen un campo llamado "CABundle", que contiene una cadena codificada en base64 del certificado de la CA.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.