Kubernetes (IV): autenticación y autorización

Hoy vamos a continuar con la serie de posts relacionados con Kubernetes. Tras el contenido de los post I, II y III, ya sabemos un poco qué es Kubernetes, cómo almacenar datos en nuestro clúster y cómo acceder a nuestros servicios.

En cada post hemos visto como K8s está diseñado para ser escalable y para ello, cualquier servicio necesita ingentes cantidades de hardware (host, switchs, routers, cableado, discos duros, etc).

Si utilizamos una nube pública, nuestro proveedor se encargará de aprovisionar los recursos necesarios, pero seguirá de nuestra mano el tener que distribuir las cargas de trabajo a lo largo del clúster para lograr nuestros objetivos de servicio (alta disponibilidad, distribución en zonas o regiones, etc).

La gran necesidad de recursos nos lleva a hacernos una pregunta: ¿Cómo podemos aprovechar al máximo los recursos de nuestro clúster? La forma más completa se basa en desplegar diferentes aplicaciones que compartan hardware, pero puede no ser lo más seguro.

Así que, ¿cómo podemos aprovechar al máximo y de la manera más segura posible, los recursos de nuestro clúster?

Namespaces

Kubernetes está pensado para que podamos compartir los recursos de nuestro clúster entre diferentes aplicaciones. Esto permite generar clústers más grandes donde pueden convivir varias cargas de trabajo, en lugar de tener clústers pequeños y que dificultan la gestión del conjunto.

Aunque siempre hemos podido desplegar pods y asignarles unos límites en el consumo de recursos (trataremos esto en otros posts), eso solo distribuye los recursos entre las aplicaciones.

Para poder agrupar, aislar o etiquetar objetos en K8s, así como para aplicar cuotas tenemos que utilizar una especie de carpetas lógicas llamadas namespaces. Por el momento, sólo voy a utilizarlos para agrupar los recursos.

Gran parte de los objetos de Kubernetes están ligados al namespace en el que es creado y si no le indicamos ninguno, utiliza el namespace default. Éste es junto a kube-system (donde se despliegan las herramientas propias del clúster) uno de namespaces que suelen existir por defecto en cualquier clúster.

La creación de namespaces es muy sencilla: si quisiésemos crear uno de nombre ghost, tan sólo ejecutaríamos el siguiente comando kubectl create namespace ghost y ya podríamos empezar a utilizarlo.

kubectl get namespaces
NAME              STATUS   AGE
default           Active    1d
ghost             Active    1d
kube-node-lease   Active    1d
kube-public       Active    1d
kube-system       Active    1d

Ya podríamos desplegar objetos en él. Algunos de ellos, como los Persistent Volumes o las Storage Class son comunes al clúster, pero otros como los Deployment o los Persistent Volume Claim si lo son. Para ver que objetos son comunes al clúster y no se encuentran “namespaceados” (¡Toma palabro!), podemos ejecutar el siguiente comando kubectl api-resources. Así podemos ver los tipos de objeto que podemos crear y si se definen a nivel de namespace o no.

NAME                              SHORTNAMES   APIVERSION                             NAMESPACED   KIND
bindings                                       v1                                     true         Binding
componentstatuses                 cs           v1                                     false        ComponentStatus
configmaps                        cm           v1                                     true         ConfigMap
endpoints                         ep           v1                                     true         Endpoints
events                            ev           v1                                     true         Event
limitranges                       limits       v1                                     true         LimitRange
namespaces                        ns           v1                                     false        Namespace
nodes                             no           v1                                     false        Node
persistentvolumeclaims            pvc          v1                                     true         PersistentVolumeClaim
persistentvolumes                 pv           v1                                     false        PersistentVolume
pods                              po           v1                                     true         Pod
podtemplates                                   v1                                     true         PodTemplate
replicationcontrollers            rc           v1                                     true         ReplicationController
resourcequotas                    quota        v1                                     true         ResourceQuota
secrets                                        v1                                     true         Secret
serviceaccounts                   sa           v1                                     true         ServiceAccount
services                          svc          v1                                     true         Service
mutatingwebhookconfigurations                  admissionregistration.k8s.io/v1        false        MutatingWebhookConfiguration
validatingwebhookconfigurations                admissionregistration.k8s.io/v1        false        ValidatingWebhookConfiguration
customresourcedefinitions         crd,crds     apiextensions.k8s.io/v1                false        CustomResourceDefinition
apiservices                                    apiregistration.k8s.io/v1              false        APIService
controllerrevisions                            apps/v1                                true         ControllerRevision
daemonsets                        ds           apps/v1                                true         DaemonSet
deployments                       deploy       apps/v1                                true         Deployment
replicasets                       rs           apps/v1                                true         ReplicaSet
[...]

Autenticación en K8s

Aunque hayamos creado un namespace nuevo, sigue sin hacer separación lógica. Nuestro usuario puede ver los objetos del clúster independientemente del namespace en el que se encuentren. Esto es debido a que podemos hacerlo: nuestra autenticación y autorización (somos administradores) lo permite. La configuración por defecto de Microk8s es ésta.

Para comenzar a limitar lo que se puede hacer en Kubernetes, primero debemos entender que cada usuario tiene primero que autenticarse (quien es) y autorizarse (que puede hacer dentro del clúster). Cualquier operador, cuenta robot o servicio necesita credenciales para interactuar contra el clúster, o mejor dicho, contra las APIs del clúster.

Podemos autenticarnos frente a Kubernetes de diferentes maneras: podemos utilizar certificados, cuentas de servicio o tokens JWT (a través de OpenID). Para simplificar el post, voy a utilizar service account, pero si alguien desea más información al respecto, puede revisar cómo funcionan el resto de sistemas en la documentación adjunta.

Cuentas de servicio

Tras crear nuestro namespace vamos a crear una service account o cuenta de servicio de Kubernetes. Es un tipo de objeto que creamos a nivel de namespace y que nos permite autenticar nuestras aplicaciones dentro del clúster. Para generar una cuenta, podemos aplicar el siguiente código:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: ghost-sa
  namespace: ghost

Si aplicamos ese fichero, se habrá generado una cuenta de servicio llamada ghost-sa en el namespace ghost. También habrá aparecido un nuevo secreto que contiene el token que podemos utilizar para loguearnos en el clúster.

kubectl get sa,secrets -n ghost
NAME                      SECRETS   AGE
serviceaccount/default    1          1d
serviceaccount/ghost-sa   1         26s

NAME                          TYPE                                  DATA   AGE
secret/default-token-nstql    kubernetes.io/service-account-token   3       1d
secret/ghost-sa-token-9qvks   kubernetes.io/service-account-token   3      26s

Cuando utilizamos las cuentas de servicio dentro del clúster, éstas se autentican automáticamente, pero para probar nosotros vamos a obtener el token y a añadirlo a nuestro kubeconfig. Para obtener el token ejecutamos el siguiente comando:

kubectl get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='ghost-sa')].data.token}" -n ghost | base64 --decode

Utilizando dicho token vamos a añadir una entrada en nuestro kubeconfig (suele estar en ~/.kube/config):

kubeconfig-sa

  • La parte azul es la configuración del nuevo usuario que utiliza directamente el token que hemos obtenido en el paso anterior.

  • La parte roja se corresponde con un nuevo contexto que utilizaremos para conectarnos con esta cuenta de servicio.

Autorización en K8s

Ya tenemos nuestro namespace y nuestra cuenta de servicio, pero antes de explicar cómo funciona la autorización, es necesario comentar cómo interactuamos contra Kubernetes:

  • Kubernetes tiene diferentes APIs.

  • Cada una posee diversos recursos sobre los que podemos realizar acciones.

  • Cada API tiene una serie de verbs o acciones, que podemos realizar sobre uno o más recursos.

Imaginemos que hemos generado una cuenta de servicio y nos hemos autenticado contra el clúster. Ahora cada operación que hagamos,

Una forma de interactuar es utilizar kubectl apply -f $fichero. Si utilizaramos este fichero de ejemplo estaríamos haciendo lo siguiente:

  • Interactuaríamos contra la API de apps/v1.

  • El resource creado sería un Deployment.

  • Sería necesario tener una serie de verbs para que el recurso pueda ser creado. Podrían ser get, delete, create, etc.

Para poder configurar qué puede hacer cada usuario del clúster, tenemos que utilizar alguna de las estrategias de autorización de Kubernetes.

Autorización mediante RBAC en K8s

RBAC significa Role-based access control es un sistema que nos permite definir qué puede hacer qué y sobre qué dentro de nuestro clúster. Es la política más extendida y la recomendada por Kubernetes.

RBAC no es la única política disponible. ABAC es otra política de permisos en Kubernetes pero es más antigua y compleja, por lo que no se recomienda. No voy a explicarla, pero si alguien desea más información al respecto, podemos hacer click aquí.

La mejor forma de entender cómo funciona RBAC es mediante un ejemplo. Imaginemos que hemos creado una cuenta de servicio y hemos asignado unos permisos que le permiten hacer las siguientes acciones:

rbac-example

  • En el API Group 1, puede ejecutar cualquier acción en el recurso 1 y un par de acciones en el tipo de recurso 2.

  • En el API Group 2, sólo puede ejecutar una acción en un tipo de recursos concreto.

  • En el API Group 3, puede ejecutar algunas acciones en un tipo de recurso concreto.

Para utilizar RBAC, antes de nada tenemos que ver si éste se encuentra habilitado o no. Si nuestro clúster es muy antiguo, podemos ejecutar el comando kubectl api-versions | grep rbac.authorization.k8s.io/v1 y si nos devuelve algún resultado es que está habilitado.

Si no estuviese habilitado, podemos habilitarlo en en clúster ya existente, relanzando el API Server de K8s con la flag –authorization-mode=RBAC. Si utilizamos algún proveedor de nube, nos encontramos con este estado:

  • En Microsoft Azure, a día de hoy todavía no podemos habilitar RBAC en un clúster ya creado y debemos hacerlo en la creación del clúster. Si utilizamos la CLI de Azure, lo haríamos añadiendo la flag –enable-rbac al comando.

  • En AWS el servicio se lanzó habilitado por defecto.

  • En Google Cloud está habilitado por defecto desde la versión de Kubernetes 1.6. Si deseamos actualizar un clúster ya existente para que utilice RBAC debemos actualizar el clúster con la opción –no-enable-legacy-authorization.

En MicroK8s tenemos que habilitarlo con el comando microk8s.enable rbac.

microk8s.kubectl api-versions | grep rbac.authorization.k8s.io/v1

rbac.authorization.k8s.io/v1

Roles y scopes

En estos momentos ya tenemos RBAC habilitado y una cuenta de servicio asociada en nuestro kubeconfig. Vamos a activarla con kubectl config use-context ghost

Si ahora intentamos ejecutar cualquier comando, recibiremos un error:

~ kubectl get pods
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:ghost:ghost-sa" cannot list resource "pods" in API group "" in the namespace "default"

~ kubectl get pods -n ghost
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:ghost:ghost-sa" cannot list resource "pods" in API group "" in the namespace "ghost"

Estos errores demuestran que no tenemos permisos en nuestra cuenta de servicio. Para ilustrar como funciona RBAC vamos a añadirle dos permisos:

  • Un permiso global a todo el clúster que nos permita ver que pods y los logs de los mismos en cualquier namespace del clúster. Dicho permiso no va a permitir borrar, desplegar nuevos pods o ver el contenido de los secretos.

  • Un permiso específico para poder gestionar cualquier tipo de carga de trabajo en nuestro namespace, pero sin permitir que podamos borrarlos. Tampoco queremos que pueda tener permisos para modificar permisos en dicho namespace.

Antes de nada, cambiamos nuestro contexto con kubectl config use-context microk8s para volver a tener permisos.

Cada grupo de permisos que definamos en Kubernetes, tiene un alcance o scope. Si éste aplica a un único namespace, estamos generando un objeto de tipo Role y si aplica a todo el clúster, un objeto de tipo ClusterRole.

Para el permiso general, vamos a crear un fichero con el siguiente contenido:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-reader
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "list"]

De esta forma le indicamos a Kubernetes (a través de la API de autorización rbac.authorization.k8s.io/v1), que el ClusteRole cluster-reader puede realizar las acciones (verbs) en los siguientes objetos (resources). Así definimos “el qué” puede hacer de la autorización.

NOTA: apiGroups está vacío debido a que nos estamos refiriendo a la API por defecto.

Ya hemos generado unos permisos para nuestras cuentas, pero todavía no se los hemos asignado. Para asignar un permiso general a todo el clúster debemos crear otro objeto llamado ClusterRoleBinding. Este sería el código de ejemplo:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ghost-cluster-reader-binding
subjects:
- kind: ServiceAccount
  name: ghost-sa
  namespace: ghost
  apiGroup: ""
roleRef:
  kind: ClusterRole
  name: cluster-reader
  apiGroup: ""

En este ejemplo, la cuenta de servicio ghost-sa es asignado al ClusterRole de nombre cluster-reader. Así definimos “el quién” de la autorización.

Ahora procedemos a aplicar los dos ficheros para generar el permiso y asignarlo a nuestra cuenta de servicio.

kubectl apply -f clusterrole.yaml 
clusterrole.rbac.authorization.k8s.io/cluster-reader created

kubectl apply -f clusterrolebinding.yaml 
clusterrolebinding.rbac.authorization.k8s.io/ghost-cluster-reader-binding created

El siguiente permiso es más específico y restringido en scope. En lugar de dar permisos sobre todo el clúster vamos a hacerlo sobre un único namespace. Este tipo de objetos son similares a los anteriores, pero sin el “clúster”: Role y RoleBinding respectivamente. Éste sería su código:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ghost-operator
  namespace: ghost
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "watch", "list", "create", "update", "patch"]
- apiGroups: ["extensions"]
  resources: ["deployments"]
  verbs: ["get", "watch", "list", "create", "update", "patch"]
- apiGroups: ["apps"]
  resources: ["statefulsets"]
  verbs: ["get", "watch", "list", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["secrets", "configmaps", "persistentvolumeclaims"]
  verbs: ["get", "watch", "list", "create", "update", "patch"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ghost-operator-binding
  namespace: ghost
subjects:
- kind: ServiceAccount
  name: ghost-sa
  namespace: ghost
  apiGroup: ""
roleRef:
  kind: Role
  name: ghost-operator
  apiGroup: ""

Una vez hayamos aplicado estos ficheros, podemos cambiar de contexto a la cuenta de ghost con kubectl config use-context ghost y verificar qué acciones podemos y cuales no podemos hacer.

NOTA: Estoy utilizando los ficheros de mi repositorio de ejemplo y el acortador de URLs Opensource Kutt para que el post quede más pequeño.

# Desplegamos un Secret y un Configmap
kubectl apply -f https://kutt.it/SPtVom -n ghost

kubectl apply -f https://kutt.it/gs7T79 -n ghost

# Desplegamos un Deployment que utiliza dichos objetos
kubectl apply -f https://kutt.it/w2VfXM -n ghost

Como podemos ver, los permisos funcionan correctamente. ¿Pero y si intentamos desplegar un StatefulSet?

kubectl apply -f https://kutt.it/GbZiMq -n ghost
statefulset.apps/nginx-with-state created

kubectl get pvc -n ghost
NAME                     STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS    AGE
www-nginx-with-state-0   Pending                                      local-storage   9s

El PVC está en Pending porque nuestro StorageClass no permite el autoaprovisionamiento de discos. Si intentamos crear el PersistentVolume veremos que no tenemos permisos y que debe crearlo alguien que si pueda.

Si intentamos crear otros objetos como Daemonsets, un Service o intentamos modificar nuestros permisos, también recibiremos un error.

# Intentamos crear un Daemonset
kubectl apply -f https://kutt.it/cJeX5g -n ghost

Error from server (Forbidden): error when retrieving current configuration of:
Resource: "apps/v1, Resource=daemonsets", GroupVersionKind: "apps/v1, Kind=DaemonSet"
Name: "fluentd", Namespace: "ghost"
from server for: "basic-fluentd-daemonset.yml": daemonsets.apps "fluentd" is forbidden: User "system:serviceaccount:ghost:ghost-sa" cannot get resource "daemonsets" in API group "apps" in the namespace "ghost"

# Intentamos crear un Service
kubectl apply -f https://kutt.it/9QAO3m -n ghost

Error from server (Forbidden): error when retrieving current configuration of:
Resource: "/v1, Resource=services", GroupVersionKind: "/v1, Kind=Service"
Name: "nginx", Namespace: "ghost"
from server for: "basic-nginx-service-nodeport.yml": services "nginx" is forbidden: User "system:serviceaccount:ghost:ghost-sa" cannot get resource "services" in API group "" in the namespace "ghost"

# Intentamos añadirnos al grupo de cluster-admins de RBAC
kubectl create clusterrolebinding ghost-cluster-admin-binding --clusterrole=cluster-admin --user=ghost-sa

Error from server (Forbidden): clusterrolebindings.rbac.authorization.k8s.io is forbidden: User "system:serviceaccount:ghost:ghost-sa" cannot create resource "clusterrolebindings" in API group "rbac.authorization.k8s.io" at the cluster scope

Por último, podemos ver que los permisos generales funcionan al listar los pods de otros namespaces como default

# Aunque no hay nada
kubectl get pods -n default

No resources found in default namespace

Conclusiones

El uso de RBAC nos abre un abanico infinito de posibilidades: podemos utilizarlo para dividir las responsabilidades en un grupo amplio de trabajo (un equipo de seguridad puede gestionar los Roles y sus asignaciones, un equipo de almacenamiento los volúmenes y otro equipo los aplicativos y los Services) o simplemente como en mis ejemplos, podemos utilizarlo para dividir las responsabilidades por aplicación y que diferentes equipos multidisciplinares trabajen sobre la misma infraestructura.

Sin embargo, sólo soluciona parte de los problemas de compartir infraestructura: nada limita que un pod de un namespace se comunique con otro pod de otro namespace y esto puede provocar problemas de seguridad, pero eso dará para otro post…

Espero que os haya gustado y… ¡os veo en el pŕoximo post!

Documentación

Revisado a 01-05-2023