Usuarios y Service Accounts en Kubernetes
En Kubernetes existen dos tipos de identidades:
- Usuarios: personas (administradores, desarrolladores...). Kubernetes no tiene un objeto "User" en su API: los usuarios se gestionan fuera del cluster.
- Service Accounts: identidades para procesos. Sí son objetos de la API y se usan para que los pods (u otras herramientas) se autentiquen contra el API server.
En este capítulo veremos cómo crear ambos tipos. En el siguiente veremos cómo darles permisos con RBAC.
Usuarios
Normalmente los usuarios se definen en un sistema de autenticación externo, como un proveedor OIDC (OpenID Connect), LDAP o Active Directory. Kubernetes no tiene un sistema de usuarios propio, pero puede integrarse con estos sistemas mediante estrategias de autenticación.
También se pueden definir usuarios de forma manual mediante certificados x509: para Kubernetes, cualquier certificado firmado por la CA del cluster identifica a un usuario, donde el CN (Common Name) es el nombre de usuario y la O (Organization) su grupo.
Crear un usuario mediante certificado x509 (firma manual)
Estos pasos se realizan en un nodo del control plane, que es donde reside la CA del cluster (/etc/kubernetes/pki/).
Primero creamos una clave privada para el usuario:
openssl genrsa -out user.key 2048
A continuación, generamos la petición de firma (CSR), donde CN es el nombre de usuario y O el grupo:
openssl req -new -key user.key -out user.csr -subj "/CN=user/O=group"
Finalmente, firmamos la petición con la CA del cluster:
openssl x509 -req -in user.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out user.crt -days 365
Crear un usuario mediante la API de CertificateSigningRequest
La forma "nativa" de hacer lo mismo, y la que suele caer en el examen CKA, es delegar la firma en el propio Kubernetes mediante un objeto CertificateSigningRequest:
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: user-csr
spec:
request: <contenido de user.csr en base64 y en una sola línea>
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 86400 # 1 día, opcional
usages:
- client auth
El campo request se rellena con cat user.csr | base64 | tr -d "\n". Una vez aplicado, un administrador aprueba la petición y descarga el certificado firmado:
kubectl get csr
kubectl certificate approve user-csr
kubectl get csr user-csr -o jsonpath='{.status.certificate}' | base64 -d > user.crt
Añadir el usuario a nuestro kubeconfig
Con el certificado firmado (por cualquiera de los dos métodos), añadimos las credenciales y un contexto a nuestro kubeconfig:
kubectl config set-credentials user --client-certificate=user.crt --client-key=user.key --embed-certs=true
kubectl config set-context user-context --cluster=kubernetes --user=user
kubectl config use-context user-context
Por defecto, este usuario carecerá de permisos para realizar ninguna acción en el cluster. Para asignárselos, tendremos que vincularlo a un Role (a nivel de namespace) o a un ClusterRole (a nivel de cluster) mediante un RoleBinding o ClusterRoleBinding, como veremos en el capítulo de RBAC.
Importante: Kubernetes no permite "revocar" certificados x509. Si una clave se ve comprometida, la única solución práctica es rotar la CA o apoyarse en RBAC para retirar los permisos de ese usuario. Por eso, en producción, se recomienda autenticación vía OIDC con tokens de corta duración.
Service Accounts
Las Service Accounts son las identidades de los procesos. Cada namespace tiene una llamada default y todo pod, salvo que se indique lo contrario, la utiliza.
Crear una service account es trivial:
kubectl create serviceaccount mi-app-sa
O en yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: mi-app-sa
namespace: default
Para que un pod la utilice, se indica en el spec:
apiVersion: v1
kind: Pod
metadata:
name: mi-app
spec:
serviceAccountName: mi-app-sa
containers:
- name: app
image: nginx
El token de la service account se monta automáticamente en el pod, en /var/run/secrets/kubernetes.io/serviceaccount/token, y es el que usan las aplicaciones para hablar con la API.
Tokens de Service Account
Desde Kubernetes 1.24, al crear una service account ya no se genera un Secret con un token de larga duración. Los tokens son efímeros y se solicitan bajo demanda:
kubectl create token mi-app-sa --duration=1h
Si nos encontramos tutoriales que buscan el token en un Secret asociado a la service account, están desactualizados.
Buenas prácticas con Service Accounts
- No usemos la service account
defaultpara nuestras aplicaciones: creemos una por aplicación, con los permisos mínimos. - Si el pod no necesita hablar con la API de Kubernetes (la mayoría no lo necesita), desactivemos el montaje del token:
spec:
serviceAccountName: mi-app-sa
automountServiceAccountToken: false
- Asignemos permisos siempre vía RBAC y de forma granular, como veremos en el siguiente capítulo.
Ejemplo: una Service Account con su propio kubeconfig
Un caso muy habitual: una herramienta externa (una pipeline de CI/CD, un panel de monitorización...) necesita leer el estado de un namespace. No queremos darle nuestro kubeconfig de administrador, así que le creamos su propia identidad.
1. El namespace, la Service Account y sus permisos. Los permisos se dan con RBAC, que veremos a fondo en el siguiente capítulo. Aquí basta con saber que un Role dice qué se puede hacer y un RoleBinding dice quién:
kubectl create namespace equipo
kubectl -n equipo create deployment web --image=nginx:alpine
kubectl -n equipo create serviceaccount visor
kubectl -n equipo create role lector-pods --verb=get,list,watch --resource=pods,pods/log
kubectl -n equipo create rolebinding visor-lee-pods --role=lector-pods --serviceaccount=equipo:visor
2. Un token para la Service Account. Le damos una duración razonable para el caso de uso:
TOKEN=$(kubectl -n equipo create token visor --duration=24h)
3. Los datos del clúster. Un kubeconfig necesita la dirección del API server y su certificado de autoridad (CA). Los sacamos de nuestro propio kubeconfig:
SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
kubectl config view --raw --minify \
-o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d > ca.crt
4. Montar el kubeconfig en un fichero aparte. Con --kubeconfig le decimos a kubectl config que escriba en visor.kubeconfig en lugar de en el nuestro:
kubectl config --kubeconfig=visor.kubeconfig set-cluster curso \
--server=$SERVER --certificate-authority=ca.crt --embed-certs=true
kubectl config --kubeconfig=visor.kubeconfig set-credentials visor --token=$TOKEN
kubectl config --kubeconfig=visor.kubeconfig set-context visor@curso \
--cluster=curso --user=visor --namespace=equipo
kubectl config --kubeconfig=visor.kubeconfig use-context visor@curso
rm ca.crt
--embed-certs=true mete el certificado dentro del fichero, así que visor.kubeconfig es autosuficiente: se puede entregar tal cual.
5. Probarlo. ¿Quién soy con este kubeconfig?
kubectl --kubeconfig=visor.kubeconfig auth whoami
Username system:serviceaccount:equipo:visor
Puede leer pods de su namespace:
kubectl --kubeconfig=visor.kubeconfig get pods
Pero nada más:
kubectl --kubeconfig=visor.kubeconfig delete deployment web
kubectl --kubeconfig=visor.kubeconfig get secrets
kubectl --kubeconfig=visor.kubeconfig get pods -n kube-system
Error from server (Forbidden): deployments.apps "web" is forbidden: User "system:serviceaccount:equipo:visor" cannot delete resource "deployments" ...
Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:equipo:visor" cannot list resource "secrets" ...
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:equipo:visor" cannot list resource "pods" ... in the namespace "kube-system"
Cuando caduque el token, basta con generar otro y actualizar el fichero:
kubectl config --kubeconfig=visor.kubeconfig set-credentials visor --token=$(kubectl -n equipo create token visor --duration=24h).Si se usa desde fuera del clúster, la dirección del servidor (
$SERVER) tiene que ser alcanzable desde allí, y figurar en el certificado del API server.
Ejemplo: usar la Service Account desde dentro de un pod
Las aplicaciones que corren dentro del clúster no necesitan kubeconfig: Kubernetes les monta el token en el propio pod. Veámoslo con un pod que usa la Service Account default:
kubectl -n equipo run curioso --image=curlimages/curl --command -- sleep 3600
kubectl -n equipo exec curioso -- ls /var/run/secrets/kubernetes.io/serviceaccount
ca.crt
namespace
token
Tres ficheros: el certificado de la CA del clúster, el namespace del pod y el token. Con ellos, cualquier proceso del pod puede llamar a la API. Por ejemplo, para listar los pods de su namespace:
kubectl -n equipo exec curioso -- sh -c '
cd /var/run/secrets/kubernetes.io/serviceaccount
curl -s --cacert ca.crt -H "Authorization: Bearer $(cat token)" \
https://kubernetes.default.svc/api/v1/namespaces/$(cat namespace)/pods'
{
"kind": "Status",
"status": "Failure",
"message": "pods is forbidden: User \"system:serviceaccount:equipo:default\" cannot list resource \"pods\" ...",
"reason": "Forbidden",
"code": 403
}
Llega a la API, pero no tiene permisos. La identidad funciona; lo que falta es autorización. ¿Y si alguien, por comodidad, le da permisos a la default?
kubectl -n equipo create rolebinding default-lee-pods --role=lector-pods --serviceaccount=equipo:default
Repite la llamada: ahora devuelve la lista completa de pods del namespace.
El problema es que todos los pods del namespace usan la default salvo que se diga otra cosa. Ese permiso lo acaba de recibir cualquier pod que se cree en equipo, también uno vulnerable o malicioso.
Un detalle sobre la caducidad: el token montado por defecto se renueva solo, pero si lo decodificas verás que dura un año. El API server alarga la caducidad de estos tokens automáticos por compatibilidad con clientes antiguos. Un motivo más para montarlo solo donde haga falta.
La versión bastionada
Vamos a dejarlo como debería estar. Primero, retiramos el permiso de la default:
kubectl -n equipo delete rolebinding default-lee-pods
1. Que la default no monte el token. Así, ningún pod lo recibe salvo que lo pida expresamente:
kubectl -n equipo patch serviceaccount default -p '{"automountServiceAccountToken": false}'
kubectl -n equipo run sin-token --image=curlimages/curl --command -- sleep 3600
kubectl -n equipo exec sin-token -- ls /var/run/secrets/kubernetes.io/serviceaccount
ls: /var/run/secrets/kubernetes.io/serviceaccount: No such file or directory
2. Una Service Account propia para el pod que sí necesita la API, con los permisos justos y también sin montaje automático:
kubectl -n equipo create serviceaccount lector-api
kubectl -n equipo patch serviceaccount lector-api -p '{"automountServiceAccountToken": false}'
kubectl -n equipo create rolebinding lector-api-lee-pods --role=lector-pods --serviceaccount=equipo:lector-api
3. Un token de vida corta, montado a mano. En lugar del montaje automático, pedimos el token con un volumen projected, donde sí podemos elegir cuánto dura y dónde se monta:
apiVersion: v1
kind: Pod
metadata:
name: bastionado
namespace: equipo
spec:
serviceAccountName: lector-api
automountServiceAccountToken: false # nada de montaje automático
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: curlimages/curl
command: ["sleep", "3600"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: token-api
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token-api
projected:
sources:
- serviceAccountToken: # el token, que caduca a los 10 minutos
path: api-token
expirationSeconds: 600
- configMap: # el certificado de la CA del clúster
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
El kubelet renueva el token antes de que caduque, así que la aplicación siempre tiene uno válido. Solo tiene que volver a leer el fichero, en vez de guardarlo en memoria al arrancar.
4. Comprobarlo. Puede listar pods:
kubectl -n equipo exec bastionado -- sh -c '
cd /var/run/secrets/tokens
curl -s --cacert ca.crt -H "Authorization: Bearer $(cat api-token)" \
https://kubernetes.default.svc/api/v1/namespaces/equipo/pods'
Pero si cambias pods por secrets en la URL, o intentas borrar un pod con curl -X DELETE, la respuesta es un 403. Y puedes confirmar sus permisos desde fuera:
kubectl -n equipo auth can-i --list --as=system:serviceaccount:equipo:lector-api
| Versión por defecto | Versión bastionada | |
|---|---|---|
| Identidad | default, compartida por todo el namespace | lector-api, solo para este pod |
| Token en el pod | Siempre, en todos los pods | Solo donde se monta expresamente |
| Caducidad del token | Un año | 10 minutos, renovado solo |
| Permisos | Los que alguien le haya dado a la default | Solo leer pods de su namespace |
| Contenedor | Root, con todas sus capacidades | Sin root, sin capacidades, solo lectura |
Al terminar, limpia el laboratorio:
kubectl delete namespace equipo
rm visor.kubeconfig
Resumen
- Los usuarios humanos no existen como objeto en Kubernetes: se autentican con certificados x509 (CN=usuario, O=grupo) o sistemas externos (OIDC).
- La API
CertificateSigningRequestpermite firmar certificados de usuario desde el propio cluster (kubectl certificate approve). - Las Service Accounts son las identidades de los procesos y sus tokens son efímeros desde la 1.24 (
kubectl create token). - Con
kubectl create tokenykubectl config --kubeconfigse monta un kubeconfig para una Service Account, sin compartir el del administrador. - Dentro de un pod, el token está en
/var/run/secrets/kubernetes.io/serviceaccount. Bastionado:automountServiceAccountToken: false, una Service Account por aplicación y un tokenprojectedde vida corta solo donde haga falta. - Ninguna identidad tiene permisos por sí sola: estos se asignan con RBAC, que vemos en el siguiente capítulo.
- Lista de vídeos en Youtube: Curso Kubernetes