La lección anterior sacó la configuración de tienda-web de la imagen y la puso en un ConfigMap, pero terminaba con una advertencia: un ConfigMap no protege nada. Cualquiera que pueda listar ConfigMaps en rutas-norte-pro lee su contenido íntegro en texto plano. Y la deuda que arrastramos desde el módulo 2 es precisamente de las que no admiten eso: la contraseña de postgres-reservas está escrita en claro en un manifiesto versionado en Git, y esa base de datos guarda el nombre, el DNI, el teléfono y el correo de cada cliente de Rutas Norte. Esta lección la salda con el objeto Secret, pero sobre todo te enseña a no fiarte de él más de lo que merece: verás por qué base64 no es cifrado, por qué el cifrado en reposo de etcd no viene activado por defecto, quién puede leer un secreto sin que te enteres, y qué usa el ecosistema cuando el Secret nativo se queda corto.
Advertencia importante. Esta lección explica los mecanismos que Kubernetes ofrece para gestionar credenciales y te da un punto de partida sólido, pero no sustituye a un diseño de seguridad profesional.
postgres-reservasalmacena datos personales de clientes reales (nombre, DNI, teléfono, correo), lo que en la Unión Europea entra de lleno en el ámbito del RGPD y, en España, de la LOPDGDD. El diseño concreto de la gestión de credenciales, el cifrado en reposo, la política de rotación, los registros de auditoría y las medidas de seguridad aplicables debe revisarlo y aprobarlo un profesional de seguridad o el responsable de cumplimiento normativo de tu organización. Trata todo lo que sigue como conocimiento técnico necesario, no como una recomendación de cumplimiento.
Contenido
- La deuda del módulo 2 y por qué duele
- Qué es un Secret y en qué se diferencia de un ConfigMap
- Base64 no es cifrado: la demostración
- Los tipos de Secret
- Crear Secrets: imperativo y declarativo con
stringData - Consumir un Secret como volumen
- El secreto
postgres-reservas-credencialesen Rutas Norte imagePullSecretspara el registro privado- Cifrado en reposo en etcd
- Quién puede leer un secreto
- Rotación de credenciales
- Qué no se debe hacer nunca
- Las soluciones reales del ecosistema
- La deuda del módulo 2 y por qué duele
Este es el manifiesto que tenemos hoy en el repositorio de Rutas Norte:
# k8s/base/postgres-reservas/deployment.yaml <-- ESTO ESTA MAL
spec:
containers:
- name: postgres
image: postgres:16.4
env:
- name: POSTGRES_PASSWORD
value: "R3s3rv4s2026!" # en claro, en Git, para siempre
- name: POSTGRES_USER
value: "rutasnorte"
- name: POSTGRES_DB
value: "reservas"El problema no es solo estético. Enumeremos el daño real:
- Git no olvida. Aunque borres la línea hoy y hagas commit, la contraseña sigue en el historial. Sacarla de ahí exige reescribir el historial del repositorio (
git filter-repo) y forzar el push a todos los clones. En la práctica, una credencial que ha estado en Git se considera comprometida y hay que rotarla, no borrarla. - Se propaga sin control. Cada desarrollador que clonó el repositorio tiene la contraseña de producción en su portátil. También la tienen el CI, el sistema de copias del repositorio, el buscador de código interno y cualquier análisis estático que guarde caché.
- No hay trazabilidad. No existe forma de saber quién ha leído esa contraseña ni cuándo.
- Rotarla es un despliegue de código. Cambiar la contraseña exige un commit, una revisión, un merge y un despliegue. Eso empuja a no rotarla nunca.
- Bots automatizados la encontrarán. Si el repositorio llega a ser público un solo minuto, hay rastreadores que localizan credenciales en segundos.
El objetivo de esta lección es que ese manifiesto quede así:
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: passwordEl manifiesto se puede publicar sin filtrar nada. Ese es el listón de los doce factores que veíamos en la lección anterior.
- Qué es un Secret y en qué se diferencia de un ConfigMap
Un Secret es, estructuralmente, casi idéntico a un ConfigMap: metadata más datos, sin spec ni status, con namespace, y con el mismo límite de 1 MiB. La diferencia está en el tratamiento que Kubernetes le da y en las expectativas que crea.
| Aspecto | ConfigMap | Secret |
|---|---|---|
apiVersion |
v1 |
v1 |
| Campo de datos | data (texto), binaryData |
data (base64), stringData (solo escritura) |
Codificación de data |
Texto plano UTF-8 | Base64 obligatorio |
Campo type |
No existe | Sí, y determina validaciones |
| Al montarse como volumen | Escrito en disco del nodo | Escrito en tmpfs (memoria RAM) |
Aparece en kubectl describe |
El contenido completo | Solo el tamaño en bytes de cada clave |
| Cifrado en reposo en etcd | No | Solo si se configura (no por defecto) |
| Puede ser inmutable | Sí | Sí |
| Límite de tamaño | 1 MiB | 1 MiB |
Puede llevar imagePullSecrets |
No | Sí |
| Se puede restringir con RBAC | Sí | Sí, y es imprescindible |
Las tres diferencias que de verdad importan:
Primera: stringData. Es un campo solo de escritura. Escribes en él texto plano y el apiserver lo codifica en base64 y lo mueve a data antes de guardarlo. Es lo que hace tolerable escribir Secrets a mano.
Segunda: el montaje en tmpfs. Cuando un Secret se monta como volumen, el kubelet crea un sistema de ficheros en memoria RAM, no en el disco del nodo. Consecuencia práctica: si el nodo se apaga bruscamente, la credencial no queda escrita en ningún disco recuperable. Es una protección real, aunque limitada.
Tercera: describe no muestra el contenido.
Name: postgres-reservas-credenciales
Namespace: rutas-norte-dev
Labels: app=postgres-reservas
app.kubernetes.io/part-of=rutas-norte
entorno=dev
Annotations: <none>
Type: Opaque
Data
====
password: 13 bytes
username: 10 bytes
database: 8 bytesSolo los tamaños. Esto evita el accidente clásico de mostrar la contraseña en una sesión compartida o dejarla en el log de una consola. Pero es un detalle de presentación, no un control de seguridad, como demuestra el apartado siguiente.
- Base64 no es cifrado: la demostración
Esta es la idea más importante de la lección y hay que interiorizarla haciéndola.
Creamos el Secret:
kubectl create secret generic postgres-reservas-credenciales \
--from-literal=username=rutasnorte \
--from-literal=password='R3s3rv4s2026!' \
--from-literal=database=reservas \
-n rutas-norte-devLo miramos en YAML:
apiVersion: v1
data:
database: cmVzZXJ2YXM=
password: UjNzM3J2NHMyMDI2IQ==
username: cnV0YXNub3J0ZQ==
kind: Secret
metadata:
creationTimestamp: "2026-08-05T11:02:47Z"
name: postgres-reservas-credenciales
namespace: rutas-norte-dev
resourceVersion: "191455"
uid: 8a3f7d21-5c6b-4e09-b1d7-4f2a9c8e3105
type: OpaqueA primera vista parece cifrado. No lo es. Base64 es una codificación reversible sin clave, diseñada en los años 80 para transportar binarios por canales de texto. Cualquiera puede deshacerla:
kubectl get secret postgres-reservas-credenciales -n rutas-norte-dev \
-o jsonpath='{.data.password}' | base64 -d; echoTres segundos y un comando. Ni contraseña, ni clave, ni permisos especiales más allá de poder leer el Secret.
Podemos sacar todo el contenido de golpe:
kubectl get secret postgres-reservas-credenciales -n rutas-norte-dev \
-o go-template='{{range $k, $v := .data}}{{$k}}={{$v | base64decode}}{{"\n"}}{{end}}'O, desde Kubernetes 1.30, con el visor incorporado:
kubectl get secret postgres-reservas-credenciales -n rutas-norte-dev -o jsonpath='{.data}' \
| jq -r 'to_entries[] | "\(.key)=\(.value|@base64d)"'Qué implica esto exactamente:
| Creencia falsa | Realidad |
|---|---|
| "Está codificado, es seguro" | Base64 se deshace sin clave, en un comando |
| "Como está en un Secret, puedo subirlo a Git" | No. Es equivalente a subirlo en claro |
| "Solo los administradores lo ven" | Lo ve quien tenga permiso get secrets en el namespace |
| "En etcd está cifrado" | Por defecto no (apartado 9) |
| "Los logs no lo muestran" | Un kubectl get secret -o yaml en un pipeline sí |
Entonces, ¿para qué sirve base64? Para dos cosas legítimas y ninguna de ellas es seguridad:
- Permitir contenido binario. Un certificado en formato DER o una clave privada con bytes no imprimibles no cabe en un campo de texto YAML. Base64 lo hace posible.
- Evitar problemas de escapado. Una contraseña con comillas, saltos de línea o
$rompería el YAML. Codificada, no.
La regla que debes memorizar: un Secret de Kubernetes no protege el dato; lo que hace es darle un lugar donde aplicar controles (RBAC, cifrado en reposo, auditoría, montaje en memoria) y separarlo del manifiesto de la aplicación. La protección la aportan esos controles, no el objeto.
- Los tipos de Secret
El campo type no es decorativo: determina qué claves exige Kubernetes y qué componentes saben usar el Secret.
type |
Claves obligatorias | Para qué sirve | Comando de creación |
|---|---|---|---|
Opaque |
Ninguna | Uso general: contraseñas, tokens de API, claves de cifrado | kubectl create secret generic |
kubernetes.io/dockerconfigjson |
.dockerconfigjson |
Credenciales de un registro privado de imágenes | kubectl create secret docker-registry |
kubernetes.io/tls |
tls.crt, tls.key |
Certificado y clave privada para TLS (Ingress) | kubectl create secret tls |
kubernetes.io/basic-auth |
username, password |
Autenticación básica HTTP | generic con --type |
kubernetes.io/ssh-auth |
ssh-privatekey |
Clave SSH (clonar repositorios privados) | generic con --type |
kubernetes.io/service-account-token |
token, ca.crt, namespace |
Token permanente de una ServiceAccount | Manifiesto con anotación |
bootstrap.kubernetes.io/token |
token-id, token-secret |
Unir nodos nuevos al clúster (kubeadm) | Manifiesto |
Notas de uso en Rutas Norte:
Opaquees el que usaremos parapostgres-reservas-credenciales, para la clave de la pasarela de pago y para el token del proveedor de correo deworker-notificaciones. Es el tipo por defecto si no pones nada.kubernetes.io/dockerconfigjsonserá el que permita descargar imágenes deregistry.rutasnorte.example(apartado 8).kubernetes.io/tlsserá el que sostenga el certificado dewww.rutasnorte.example. Lo verás en TLS y Certificados, donde cert-manager lo generará y renovará solo.kubernetes.io/service-account-token: cuidado con este. Antes de Kubernetes 1.24, cada ServiceAccount tenía asociado un Secret de este tipo con un token sin caducidad. Ya no se crean automáticamente y crearlos a mano es una mala idea salvo casos muy concretos: un token eterno es un token que nunca se puede revocar de forma limpia. El mecanismo actual lo verás en ServiceAccounts.
El tipo también se valida. Esto falla:
kubectl create secret generic mal-tipo --type=kubernetes.io/basic-auth \
--from-literal=usuario=rutasnorte -n rutas-norte-devEl tipo basic-auth exige exactamente la clave username, no usuario. Esa validación es útil: evita que un Ingress o un controlador reciba un Secret con la forma equivocada.
- Crear Secrets: imperativo y declarativo con
stringData
stringData5.1. kubectl create secret generic
kubectl create secret generic postgres-reservas-credenciales \
--from-literal=username=rutasnorte \
--from-literal=password='R3s3rv4s2026!' \
-n rutas-norte-devCuidado con el historial del shell. Ese comando queda escrito en ~/.bash_history con la contraseña dentro. Dos formas de evitarlo:
# Opcion A: un espacio delante (con HISTCONTROL=ignorespace)
kubectl create secret generic ... --from-literal=password='R3s3rv4s2026!'
# Opcion B (mejor): desde fichero, y borrar el fichero despues
printf '%s' 'R3s3rv4s2026!' > /tmp/pw
kubectl create secret generic postgres-reservas-credenciales \
--from-file=password=/tmp/pw -n rutas-norte-dev
shred -u /tmp/pwEl printf '%s' en lugar de echo es deliberado: echo añade un salto de línea final que acabaría dentro de la contraseña. Es un error clásico y desconcertante: la contraseña "parece correcta" pero PostgreSQL la rechaza. Si usas echo, pon -n.
5.2. kubectl create secret docker-registry
kubectl create secret docker-registry registry-rutasnorte \
--docker-server=registry.rutasnorte.example \
--docker-username=despliegues \
--docker-password='<token-de-despliegue>' \
[email protected] \
-n rutas-norte-pro5.3. kubectl create secret tls
kubectl create secret tls tienda-web-tls \
--cert=certs/www.rutasnorte.example.crt \
--key=certs/www.rutasnorte.example.key \
-n rutas-norte-pro5.4. Manifiesto con stringData
Si escribes el manifiesto a mano, usa siempre stringData, nunca data:
apiVersion: v1
kind: Secret
metadata:
name: postgres-reservas-credenciales
namespace: rutas-norte-dev
labels:
app: postgres-reservas
app.kubernetes.io/name: postgres-reservas
app.kubernetes.io/component: base-de-datos
app.kubernetes.io/part-of: rutas-norte
entorno: dev
type: Opaque
stringData: # texto plano; el apiserver lo codifica al guardarlo
username: rutasnorte
password: "R3s3rv4s2026!"
database: reservasVentajas de stringData frente a data:
- Se escribe y se lee sin codificar ni descodificar nada.
- Elimina el error de codificar con
echosin-n, que mete un\nen el valor. - Un
diffen una pull request es legible... lo cual, ojo, también es el motivo por el que este fichero no puede ir a Git tal cual.
Si una clave aparece en stringData y en data, gana stringData.
Y aquí llega la pregunta obvia: si este fichero no puede ir a Git, ¿dónde vive? Tres respuestas legítimas, en orden de madurez:
- Solo en el clúster. El fichero se aplica una vez desde un puesto seguro y se destruye. Simple, pero se pierde la trazabilidad y hay que documentar aparte qué claves existen.
- En Git, cifrado. Sealed Secrets o SOPS permiten versionar un fichero que solo el clúster puede descifrar (apartado 13).
- Fuera de Kubernetes. Un gestor de secretos externo (Vault, AWS Secrets Manager) es la fuente de verdad y un operador lo sincroniza (apartado 13).
Mientras tanto, lo mínimo imprescindible en el repositorio de Rutas Norte:
Y un secret-postgres.yaml.ejemplo con valores ficticios, versionado, para que el equipo sepa qué claves hacen falta.
- Consumir un Secret como volumen
El mecanismo es idéntico al de los ConfigMaps de la lección anterior, cambiando configMap por secret y name por secretName:
spec:
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0
volumeMounts:
- name: credenciales-bd
mountPath: /etc/secretos/postgres # ruta propia, no un directorio del sistema
readOnly: true
volumes:
- name: credenciales-bd
secret:
secretName: postgres-reservas-credenciales
defaultMode: 0400 # solo el propietario lee
items: # opcional: solo estas claves
- key: password
path: passwordDentro del contenedor:
kubectl exec -n rutas-norte-dev deploy/api-reservas -- ls -la /etc/secretos/postgres
kubectl exec -n rutas-norte-dev deploy/api-reservas -- mount | grep secretostotal 0
drwxrwxrwt 3 root root 100 Aug 5 11:31 .
drwxr-xr-x 3 root root 4096 Aug 5 11:31 ..
lrwxrwxrwx 1 root root 15 Aug 5 11:31 password -> ..data/password
tmpfs on /etc/secretos/postgres type tmpfs (ro,relatime,size=...)Dos cosas a destacar:
type tmpfs: el volumen está en memoria RAM. Nunca toca el disco del nodo. Al morir el pod, desaparece.- La misma cadena de enlaces a
..dataque en los ConfigMaps, con la misma consecuencia: si el Secret cambia, el fichero montado se actualiza (con el mismo retardo del kubelet, y con la misma excepción desubPath, que no propaga).
Esa capacidad de refresco es la razón principal para preferir el volumen frente a la variable de entorno cuando se trata de credenciales. Una aplicación bien escrita puede releer el fichero y adoptar una contraseña rotada sin reiniciarse. Volveremos a ello en el apartado 11.
El consumo como variable de entorno (secretKeyRef, envFrom con secretRef) es igual de común y a menudo más práctico, sobre todo con imágenes de terceros como postgres:16, que esperan POSTGRES_PASSWORD en el entorno. Lo verás con todo el detalle en Variables de Entorno. Adelanto la comparación, porque condiciona el diseño:
| Volumen | Variable de entorno | |
|---|---|---|
| Se refresca al rotar | Sí | No: exige recrear el pod |
Visible en kubectl describe pod |
No | La referencia sí, el valor no |
Visible en /proc/<pid>/environ |
No | Sí, para cualquier proceso del contenedor |
| Puede filtrarse en un volcado de error | Poco probable | Sí: muchos frameworks vuelcan el entorno |
| Compatible con imágenes de terceros | A veces (*_FILE) |
Casi siempre |
Muchas imágenes serias admiten la variante _FILE: POSTGRES_PASSWORD_FILE=/etc/secretos/postgres/password. Cuando exista, prefiérela: combina la compatibilidad de la variable con la seguridad del fichero.
- El secreto
postgres-reservas-credenciales en Rutas Norte
postgres-reservas-credenciales en Rutas NorteVamos a saldar la deuda de verdad. Diseño completo:
flowchart LR
S["Secret<br/>postgres-reservas-credenciales<br/>type: Opaque"]
S -->|"env: POSTGRES_PASSWORD"| PG["postgres-reservas<br/>(crea el usuario al inicializarse)"]
S -->|"volumen /etc/secretos/postgres"| API["api-reservas<br/>(se conecta a la BD)"]
S -->|"volumen /etc/secretos/postgres"| W["worker-notificaciones<br/>(lee reservas pendientes)"]
S2["Secret<br/>smtp-notificaciones"] -->|"env: SMTP_PASSWORD"| W
RBAC["RBAC (08-01)<br/>solo estas SA leen el Secret"] -.controla.-> S
El Secret, en k8s/entornos/dev/secret-postgres.yaml (fuera de Git):
apiVersion: v1
kind: Secret
metadata:
name: postgres-reservas-credenciales
namespace: rutas-norte-dev
labels:
app: postgres-reservas
app.kubernetes.io/name: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
type: Opaque
stringData:
username: rutasnorte
password: "d3v-C4mbi4m3-2026"
database: reservas
# URL completa: comoda para la app, pero duplica la contrasenya.
# Si la app puede componerla, mejor no tenerla aqui.
url: "postgresql://rutasnorte:d3v-C4mbi4m3-2026@postgres-reservas:5432/reservas"El Deployment de postgres-reservas, ya sin contraseñas en claro:
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-reservas
namespace: rutas-norte-dev
labels:
app: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
replicas: 1
selector:
matchLabels:
app: postgres-reservas
entorno: dev
strategy:
type: Recreate # una BD no admite dos pods sobre el mismo disco
template:
metadata:
labels:
app: postgres-reservas
app.kubernetes.io/name: postgres-reservas
app.kubernetes.io/component: base-de-datos
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: postgres
image: postgres:16.4
ports:
- name: postgres
containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-reservas-credenciales
key: database
- name: PGDATA
value: /var/lib/postgresql/data/pgdataY api-reservas, que la consume como fichero:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0
env:
- name: DB_HOST
value: postgres-reservas # el Service del modulo 2
- name: DB_PORT
value: "5432"
- name: DB_PASSWORD_FILE
value: /etc/secretos/postgres/password
volumeMounts:
- name: credenciales-bd
mountPath: /etc/secretos/postgres
readOnly: true
volumes:
- name: credenciales-bd
secret:
secretName: postgres-reservas-credenciales
defaultMode: 0400
items:
- key: password
path: password
- key: username
path: usernameFíjate en el detalle de diseño: api-reservas recibe solo username y password gracias a items. No recibe la clave url, que contiene la contraseña duplicada, ni ninguna otra clave que pueda añadirse al Secret en el futuro. Es el principio de mínimo privilegio aplicado al contenido de un objeto.
Verificación de que la deuda está saldada:
grep -r "R3s3rv4s2026" k8s/ ; echo "salida: $?"
kubectl exec -n rutas-norte-dev deploy/api-reservas -- cat /etc/secretos/postgres/password; echogrep no encuentra nada en el repositorio y la aplicación tiene su credencial. Ese es el objetivo.
imagePullSecrets para el registro privado
imagePullSecrets para el registro privadoLas imágenes de Rutas Norte viven en registry.rutasnorte.example, que es privado. Sin credenciales, el pod falla así:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulling 45s kubelet Pulling image "registry.rutasnorte.example/api-reservas:2.5.0"
Warning Failed 43s kubelet Failed to pull image: rpc error: code = Unknown
desc = failed to resolve reference: unexpected status from HEAD request: 401 Unauthorized
Warning Failed 43s kubelet Error: ErrImagePull
Normal BackOff 18s (x3 over 42s) kubelet Back-off pulling image
Warning Failed 18s kubelet Error: ImagePullBackOffImagePullBackOff con un 401 es siempre lo mismo: faltan credenciales de registro.
Se crea el Secret del tipo adecuado:
kubectl create secret docker-registry registry-rutasnorte \
--docker-server=registry.rutasnorte.example \
--docker-username=despliegues \
--docker-password='<token-de-despliegue-de-solo-lectura>' \
-n rutas-norte-proLo que guarda dentro es un fichero .dockerconfigjson:
kubectl get secret registry-rutasnorte -n rutas-norte-pro \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq .{
"auths": {
"registry.rutasnorte.example": {
"username": "despliegues",
"password": "<token-de-despliegue-de-solo-lectura>",
"auth": "ZGVzcGxpZWd1ZXM6PHRva2VuLi4uPg=="
}
}
}Otra demostración de que base64 no protege nada: el campo auth es simplemente usuario:contraseña codificado.
Y se referencia en el pod:
spec:
imagePullSecrets:
- name: registry-rutasnorte # a nivel de POD, no de contenedor
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0Tres detalles importantes:
imagePullSecretsva enspecdel pod, no dentro decontainers. Aplica a todas las imágenes del pod, incluidos los init containers.- Se pueden listar varios, uno por registro. El kubelet elige el que casa con el servidor de la imagen.
- Hay que repetirlo en cada pod del namespace, lo cual es tedioso y fácil de olvidar. La solución elegante es declararlo en la ServiceAccount: todos los pods que la usen lo heredarán sin escribirlo. Lo verás en ServiceAccounts.
Buena práctica de seguridad: el token del registro debe ser de solo lectura de imágenes (pull), nunca uno con permiso de subida. Si el clúster se ve comprometido, un token de pull limita el daño a poder descargar imágenes; uno de push permitiría sustituirlas por versiones maliciosas. La lección de Seguridad de Imágenes profundiza en esto.
- Cifrado en reposo en etcd
Llegamos al punto que más gente desconoce.
Por defecto, los Secrets se guardan en etcd sin cifrar. Codificados en base64, sí; cifrados, no. Quien tenga acceso al disco de un nodo del plano de control, a una copia de seguridad de etcd o a un volcado del sistema de ficheros, tiene todas las credenciales del clúster.
Demostración (en un clúster donde tengas acceso al plano de control):
sudo ETCDCTL_API=3 etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
get /registry/secrets/rutas-norte-dev/postgres-reservas-credenciales | hexdump -C | head -800000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret|
00000010 73 2f 72 75 74 61 73 2d 6e 6f 72 74 65 2d 64 65 |s/rutas-norte-de|
00000020 76 2f 70 6f 73 74 67 72 65 73 0a 6b 38 73 00 0a |v/postgres.k8s..|
...
000000a0 64 33 76 2d 43 34 6d 62 69 34 6d 33 2d 32 30 32 |d3v-C4mbi4m3-202|
000000b0 36 12 0a 72 75 74 61 73 6e 6f 72 74 65 |6..rutasnorte|La contraseña se lee directamente en el volcado. Ni siquiera hace falta descodificar base64: etcd guarda el objeto serializado con los valores ya en binario.
La solución es una EncryptionConfiguration en el apiserver:
# /etc/kubernetes/enc/encryption-config.yaml (solo en los nodos del plano de control)
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets # que objetos se cifran
- configmaps # opcional, tiene coste
providers:
- aescbc: # el PRIMERO se usa para ESCRIBIR
keys:
- name: clave-2026-08
secret: <32 bytes aleatorios en base64>
- identity: {} # sin cifrar: necesario para LEER lo antiguoY se activa arrancando el apiserver con --encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml.
Los proveedores disponibles:
| Proveedor | Cifrado | Velocidad | Fortaleza | Comentario |
|---|---|---|---|---|
identity |
Ninguno | — | Nula | El comportamiento por defecto |
secretbox |
XSalsa20 + Poly1305 | Muy rápida | Fuerte | Buena opción si no hay KMS |
aescbc |
AES-CBC 32 bytes | Rápida | Aceptable | Vulnerable a padding oracle en teoría |
aesgcm |
AES-GCM | Muy rápida | Fuerte | Exige rotar la clave con frecuencia |
kms v2 |
Delegado en un KMS externo | Rápida | La mejor | La clave nunca está en el disco del nodo |
Cuatro reglas críticas:
- El orden importa. El primer proveedor de la lista es el que cifra al escribir. Todos los de la lista se prueban al leer. Por eso
identitydebe ir al final durante la migración: permite leer los Secrets que ya existían sin cifrar. - Activarlo no cifra lo que ya existe. Solo se cifra lo que se escribe a partir de ese momento. Hay que forzar una reescritura de todo:
- La clave del fichero es la joya de la corona. Si usas
aescbcosecretbox, la clave está en texto plano en el disco del nodo del plano de control. Quien tenga ese fichero y una copia de etcd lo tiene todo. Por esokmsv2 es la única opción realmente sólida en producción: la clave de cifrado de datos se envuelve con una clave que vive en un HSM o en el KMS de la nube y nunca se escribe en el nodo. - En Kubernetes gestionado (EKS, AKS, GKE) esto lo activas con una casilla, delegando en el KMS del proveedor. Es una de las razones de peso para usar un clúster gestionado, como verás en 10-06.
Para Rutas Norte, que guarda DNI y datos de contacto de clientes, el cifrado en reposo con KMS no es opcional: es una medida técnica de las que el RGPD espera ver documentada. Y de nuevo: la decisión concreta debe validarla el responsable de cumplimiento.
- Quién puede leer un secreto
Un Secret no tiene ninguna protección propia. Todo depende de RBAC, que se estudia a fondo en Control de Acceso Basado en Roles. Aquí basta con entender por qué es inseparable.
Quién puede leer postgres-reservas-credenciales hoy:
| Quién | Cómo | Cómo se restringe |
|---|---|---|
Cualquiera con get secrets en el namespace |
kubectl get secret -o yaml |
RBAC: no conceder ese verbo |
Cualquiera con list secrets en el namespace |
kubectl get secrets -o yaml (¡lista los contenidos!) |
RBAC: list es tan peligroso como get |
| Cualquiera que pueda crear un pod en el namespace | Monta el Secret en un pod suyo y lo lee | RBAC sobre create pods |
Cualquiera con exec en un pod que lo monte |
kubectl exec ... -- cat /etc/secretos/... |
RBAC sobre pods/exec |
| El kubelet de los nodos donde corren esos pods | Necesita el Secret para montarlo | NodeRestriction limita a los de sus pods |
| Quien tenga acceso a etcd o a sus copias | Volcado directo | Cifrado en reposo + seguridad del nodo |
| Un administrador del clúster | Todo | Auditoría |
Las dos entradas que sorprenden y hay que subrayar:
list filtra los contenidos. Mucha gente concede list secrets pensando que solo permite ver los nombres. No es así: kubectl get secrets -o yaml con permiso de list devuelve los objetos completos, con sus datos. En RBAC, list sobre secretos es equivalente a get.
Poder crear pods equivale a poder leer todos los secretos del namespace. Quien pueda desplegar un pod puede montar cualquier Secret del namespace y volcarlo. No hay forma de evitarlo con RBAC sobre secretos: hay que restringir la creación de pods. Es la razón principal por la que el namespace es la unidad real de aislamiento de credenciales y por la que Rutas Norte separa dev, pre y pro: nadie con acceso de desarrollo debe poder crear pods en rutas-norte-pro.
Un buen ejercicio de auditoría, que anticipa el módulo 8:
kubectl auth can-i get secrets -n rutas-norte-pro
kubectl auth can-i list secrets -n rutas-norte-pro --as=system:serviceaccount:rutas-norte-pro:default
- Rotación de credenciales
Rotar una credencial es cambiarla de forma periódica y, obligatoriamente, cuando se sospecha que se ha filtrado. Y aquí aparece un problema que Kubernetes no resuelve solo.
El problema: un pod que arrancó ayer tiene la contraseña vieja. Si la cambias en el Secret:
- Si la consume como volumen: el fichero se actualiza en un par de minutos. Pero la aplicación solo se enterará si relee el fichero. La mayoría lee al arrancar y guarda la conexión abierta.
- Si la consume como variable de entorno: no se actualiza nunca. Las variables de entorno de un proceso se fijan al ejecutarlo y no hay forma de cambiarlas desde fuera.
La secuencia correcta de rotación, con doble credencial válida, que es lo que evita el corte de servicio:
sequenceDiagram
participant OP as Operador
participant PG as postgres-reservas
participant K8S as Secret
participant API as pods de api-reservas
OP->>PG: 1. Crear la contrasenya NUEVA (la vieja sigue valida)
OP->>K8S: 2. Actualizar el Secret con la nueva
Note over API: 3. Los pods viejos siguen con la vieja: SIGUEN FUNCIONANDO
OP->>API: 4. kubectl rollout restart (progresivo, sin corte)
Note over API: 5. Los pods nuevos arrancan con la nueva
OP->>PG: 6. Revocar la contrasenya VIEJA
OP->>PG: 7. Verificar en los logs que nadie la usa
En comandos:
# 1. En PostgreSQL, crear la nueva credencial sin quitar la vieja
kubectl exec -n rutas-norte-pro deploy/postgres-reservas -- \
psql -U postgres -c "ALTER USER rutasnorte PASSWORD 'nueva-2026-08';"
# 2. Actualizar el Secret
kubectl create secret generic postgres-reservas-credenciales \
--from-literal=username=rutasnorte \
--from-literal=password='nueva-2026-08' \
--from-literal=database=reservas \
-n rutas-norte-pro --dry-run=client -o yaml | kubectl apply -f -
# 3. Reiniciar de forma progresiva los consumidores
kubectl rollout restart deploy/api-reservas -n rutas-norte-pro
kubectl rollout restart deploy/worker-notificaciones -n rutas-norte-pro
kubectl rollout status deploy/api-reservas -n rutas-norte-prosecret/postgres-reservas-credenciales configured
deployment.apps/api-reservas restarted
deployment.apps/worker-notificaciones restarted
deployment "api-reservas" successfully rolled outUn aviso sobre ALTER USER: en PostgreSQL un usuario tiene una sola contraseña, así que el paso 1 la sustituye y los pods viejos se romperán en cuanto abran una conexión nueva. En un sistema con rotación seria se usan dos usuarios (rutasnorte_a y rutasnorte_b) y se alterna entre ellos, que es lo que hacen los motores de secretos dinámicos de Vault. Diséñalo antes de necesitarlo.
Puntos que conviene fijar:
- Automatiza la detección del cambio. La técnica del hash del Secret en una anotación del
template, que verás en 03-03, hace que actualizar el Secret dispare el despliegue solo, sinrollout restartmanual. - Rota siempre tras una exposición. Si la contraseña estuvo en Git, rotarla es obligatorio aunque hayas reescrito el historial.
- Vigila los pods rezagados. Un CronJob o un pod que no forma parte de un Deployment no se reinicia con
rollout restart. Hazte una lista de consumidores antes de rotar.
- Qué no se debe hacer nunca
| Práctica | Por qué es grave |
|---|---|
| Subir el Secret a Git, aunque esté en base64 | Base64 no es cifrado. Equivale a subirlo en claro y queda en el historial para siempre |
| Poner una credencial en una anotación | Las anotaciones se copian a objetos derivados, salen en describe y en los logs de los controladores |
Pasarla en args o command |
Aparece en kubectl describe pod, en ps aux dentro del contenedor y en los logs del kubelet |
| Hornearla en la imagen | Queda en una capa del registro; docker history la muestra aunque la borres en una capa posterior |
| Escribirla en un ConfigMap | Sin describe protegido, sin tmpfs, sin cifrado en reposo, y RBAC de ConfigMaps suele ser laxo |
| Registrarla en un log | Los logs se centralizan, se copian y se conservan meses (07-05) |
| Reutilizar la misma credencial en los tres entornos | Un incidente en dev compromete pro |
Usar el Secret default o compartir uno entre componentes |
Impide revocar el acceso de un solo componente |
Conceder list secrets "porque solo lista nombres" |
list devuelve los contenidos completos |
| Crear tokens de ServiceAccount permanentes | Un token sin caducidad no se puede revocar limpiamente |
Depender solo de .gitignore |
Un git add -f o un fichero con otro nombre lo saltan. Añade un pre-commit hook que detecte credenciales |
Sobre el punto de args, una demostración de lo expuesto que queda:
# NUNCA HAGAS ESTO
- name: worker
image: registry.rutasnorte.example/worker-notificaciones:1.8.0
args: ["--smtp-password=Env10-2026!"]Cualquiera con permiso de lectura sobre pods —un permiso que se concede a mucha gente, incluidos los sistemas de monitorización— acaba de leer la contraseña del correo.
- Las soluciones reales del ecosistema
El Secret nativo resuelve el dónde vive la credencial dentro del clúster. No resuelve cómo llega ahí de forma segura y auditable, ni cómo se versiona, ni cómo se rota sola. Para eso existen estas cuatro familias. No vamos a hacer un tutorial de ninguna —cada una da para un curso—, pero debes saber cuándo mirar hacia cada lado.
| Solución | Idea central | Cuándo elegirla |
|---|---|---|
| Sealed Secrets (Bitnami) | Un controlador en el clúster tiene una clave privada; tú cifras con la pública y subes el fichero cifrado a Git. Solo ese clúster puede descifrarlo | Equipos pequeños, GitOps puro, sin infraestructura externa |
| SOPS (+ age/KMS) | Cifra solo los valores del YAML, dejando las claves legibles. Se integra con Flux, Helm y Kustomize | Cuando quieres diffs legibles y ya usas GitOps |
| External Secrets Operator | Un CRD ExternalSecret declara de dónde sacar la credencial; el operador la lee de Vault/AWS/Azure/GCP y mantiene sincronizado un Secret nativo |
El estándar de hecho en empresas con un gestor de secretos ya montado |
| HashiCorp Vault | Gestor de secretos completo: credenciales dinámicas de vida corta, motor de PKI, auditoría detallada, políticas propias | Organizaciones grandes, requisitos de auditoría fuertes |
| KMS de las nubes (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager) | El gestor de la nube es la fuente de verdad; se accede con identidad federada, sin credenciales estáticas | Ya estás en esa nube |
Un ejemplo de cómo se ve un ExternalSecret, solo para que reconozcas el patrón cuando te lo encuentres:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: postgres-reservas-credenciales
namespace: rutas-norte-pro
spec:
refreshInterval: 1h # resincroniza cada hora: rotacion automatica
secretStoreRef:
name: vault-rutasnorte
kind: SecretStore
target:
name: postgres-reservas-credenciales # el Secret NATIVO que se creara
creationPolicy: Owner
data:
- secretKey: password # clave en el Secret de Kubernetes
remoteRef:
key: rutasnorte/pro/postgres # ruta en Vault
property: passwordLo elegante del patrón: este fichero sí puede ir a Git. No contiene la credencial, solo dice dónde encontrarla. Y refreshInterval: 1h significa que si alguien rota la contraseña en Vault, el Secret del clúster se actualiza solo en menos de una hora.
Dónde encaja cada cosa en la madurez de una plataforma como Rutas Norte:
- Hoy (donde estamos): Secrets nativos creados a mano, fuera de Git, con
.gitignorey un fichero de ejemplo. Aceptable endev. - Siguiente paso: cifrado en reposo con KMS en el clúster y RBAC estricto sobre secretos en
rutas-norte-pro. - Madurez: External Secrets Operator apuntando al gestor de secretos corporativo, con rotación automática y auditoría de cada lectura.
Errores Comunes y Consejos
| Error | Síntoma | Solución |
|---|---|---|
| Creer que base64 protege | Se sube el Secret a Git | Es codificación, no cifrado. Nunca a Git sin cifrar |
echo sin -n al codificar |
La app rechaza la contraseña "correcta" | Usa printf '%s' o echo -n, o mejor stringData |
| Tipo incorrecto de Secret | data[username]: Required value |
Revisa la tabla de tipos del apartado 4 |
| Secret en otro namespace | Pod en ContainerCreating, FailedMount |
Los Secrets no cruzan namespaces |
| Esperar refresco con variables de entorno | La rotación no llega a los pods | Las variables son inmutables: rollout restart |
subPath con un Secret |
La rotación no se propaga nunca | Igual que con ConfigMaps: evita subPath |
defaultMode: 0400 con usuario no root |
Permission denied |
Usa 0440 con fsGroup, o 0444 |
Olvidar imagePullSecrets |
ImagePullBackOff con 401 |
Añádelo al pod o, mejor, a la ServiceAccount |
imagePullSecrets dentro de containers |
Error de validación del YAML | Va en spec del pod |
Conceder list secrets |
Fuga silenciosa de todas las credenciales | list devuelve contenidos. Trátalo como get |
| No activar cifrado en reposo | Credenciales legibles en las copias de etcd | EncryptionConfiguration con kms v2 |
| Rotar sin doble credencial | Corte de servicio durante la rotación | Nueva credencial válida antes de revocar la vieja |
Contraseña en args o en un log |
Visible en describe pod y en el sistema de logs |
Volumen o variable de entorno, nunca argumento |
Consejos finales:
- Trata todo Secret como si fuera a filtrarse. Diseña asumiendo que alguien lo leerá: credenciales distintas por entorno y por componente, con el mínimo privilegio en la base de datos.
- Instala un detector de credenciales en el
pre-commit.gitleaksodetect-secretscuestan diez minutos de configuración y evitan el 90 % de los accidentes. - Ten un inventario de secretos. Qué existe, quién lo consume, cuándo se rotó por última vez, quién es el responsable.
- Documenta las claves, nunca los valores. Un
secret-postgres.yaml.ejemploconpassword: CAMBIAMEversionado es útil y seguro. - Y lo dicho al principio: que el diseño lo revise un profesional de seguridad o el responsable de cumplimiento. Con datos personales de clientes, esto no es negociable.
Ejercicios
Ejercicio 1: Demostrar que base64 no es cifrado
- Crea un Secret
pasarela-pagoenrutas-norte-devcon las clavesapi_key(valorsk_test_4eC39HqLyjWDarjtT1zdp7dc) yentorno(valorsandbox). - Muestra el objeto en YAML y comprueba que no se lee el valor.
- Descodifica
api_keycon un solo comando encadenado. - Comprueba qué muestra
kubectl describede ese Secret y explica por qué eso no es una medida de seguridad. - Responde: si un compañero tiene el verbo
listsobre secretos en ese namespace pero noget, ¿puede leer la clave? Demuéstralo conkubectl auth can-i.
Ejercicio 2: Saldar la deuda de postgres-reservas
- Escribe el manifiesto del Secret
postgres-reservas-credencialespararutas-norte-devusandostringData, conusername,passwordydatabase, y con el esquema de etiquetas de Rutas Norte. - Modifica el Deployment de
postgres-reservaspara que tome las tres variables del Secret. - Modifica
api-reservaspara que monte solousernameypasswordcomo ficheros en/etc/secretos/postgrescon permisos de solo lectura. - Verifica que el volumen está en tmpfs.
- Demuestra con
grepque enk8s/no queda ninguna contraseña en claro, y añade las reglas necesarias al.gitignore.
Ejercicio 3: Rotación sin corte de servicio
api-reservas corre con 4 réplicas en rutas-norte-pro y hay que rotar la contraseña de la base de datos porque un antiguo miembro del equipo la conocía.
- Escribe la secuencia completa de pasos, indicando en cuál de ellos hay riesgo de corte y cómo lo evitas.
- Ejecuta la actualización del Secret con un solo comando que funcione tanto si el Secret existe como si no.
- Haz que los pods adopten la credencial nueva sin quedarte sin servicio en ningún momento.
- Verifica que los 4 pods nuevos están arriba y que ninguno conserva la credencial anterior.
- Identifica qué consumidores del Secret no se reinician con
kubectl rollout restarty qué harías con ellos.
Soluciones
Solución 1
kubectl create secret generic pasarela-pago \
--from-literal=api_key=sk_test_4eC39HqLyjWDarjtT1zdp7dc \
--from-literal=entorno=sandbox \
-n rutas-norte-dev
kubectl get secret pasarela-pago -n rutas-norte-dev -o yamlsecret/pasarela-pago created
apiVersion: v1
data:
api_key: c2tfdGVzdF80ZUMzOUhxTHlqV0Rhcmp0VDF6ZHA3ZGM=
entorno: c2FuZGJveA==
kind: Secret
metadata:
name: pasarela-pago
namespace: rutas-norte-dev
type: OpaqueLa descodificación:
kubectl get secret pasarela-pago -n rutas-norte-dev \
-o jsonpath='{.data.api_key}' | base64 -d; echoName: pasarela-pago
Namespace: rutas-norte-dev
Type: Opaque
Data
====
api_key: 32 bytes
entorno: 7 bytesdescribe solo muestra el tamaño. No es una medida de seguridad, sino de higiene visual: evita mostrar la credencial por accidente en una pantalla compartida o en la salida de un script. Quien tiene permiso para hacer describe tiene el verbo get, y con get -o yaml obtiene el valor. La protección real la dan RBAC y el cifrado en reposo, no el formato de salida.
Sobre list:
kubectl auth can-i list secrets -n rutas-norte-dev [email protected]
kubectl get secrets -n rutas-norte-dev -o yaml [email protected] | grep api_keySí puede leerla. list devuelve los objetos completos, no solo sus nombres. Es un error de concesión de permisos muy extendido.
Solución 2
# k8s/entornos/dev/secret-postgres.yaml (NO se versiona en Git)
apiVersion: v1
kind: Secret
metadata:
name: postgres-reservas-credenciales
namespace: rutas-norte-dev
labels:
app: postgres-reservas
app.kubernetes.io/name: postgres-reservas
app.kubernetes.io/component: base-de-datos
app.kubernetes.io/part-of: rutas-norte
entorno: dev
type: Opaque
stringData:
username: rutasnorte
password: "d3v-C4mbi4m3-2026"
database: reservasEl Deployment de postgres-reservas es el del apartado 7. El fragmento de api-reservas:
volumeMounts:
- name: credenciales-bd
mountPath: /etc/secretos/postgres
readOnly: true
volumes:
- name: credenciales-bd
secret:
secretName: postgres-reservas-credenciales
defaultMode: 0400
items:
- key: username
path: username
- key: password
path: passwordVerificación del tmpfs y de que solo hay dos ficheros:
kubectl exec -n rutas-norte-dev deploy/api-reservas -- sh -c \
'ls /etc/secretos/postgres && mount | grep secretos'Dos ficheros, no tres: database no se ha proyectado porque no está en items. Y el volumen es tmpfs, es decir, RAM.
Sin resultados. El .gitignore:
# Secretos: nunca en el repositorio
k8s/**/secret-*.yaml
k8s/**/*-secret.yaml
!k8s/**/*.yaml.ejemplo
*.key
*.pem
*.p12La línea !k8s/**/*.yaml.ejemplo permite versionar las plantillas con valores ficticios, que son las que documentan qué claves hacen falta.
Solución 3
La secuencia, con el riesgo señalado:
- Crear la contraseña nueva en PostgreSQL. Riesgo alto si el motor solo admite una contraseña por usuario: los pods vivos se romperán en la siguiente conexión. Se evita creando un usuario nuevo (
rutasnorte_b) en lugar de cambiar el del actual, dejando ambos válidos. - Actualizar el Secret. Sin riesgo: los pods vivos ya tienen la credencial cargada en memoria.
rollout restart. Sin riesgo simaxUnavailableestá bien puesto: el módulo 2 nos garantiza sustitución progresiva.- Revocar la credencial vieja. Riesgo si queda algún consumidor sin reiniciar; por eso va después de verificar.
kubectl create secret generic postgres-reservas-credenciales \
--from-literal=username=rutasnorte_b \
--from-literal=password='R0t4d4-2026-08!' \
--from-literal=database=reservas \
-n rutas-norte-pro --dry-run=client -o yaml | kubectl apply -f -El truco --dry-run=client -o yaml | kubectl apply -f - es el idiomático: genera el objeto y lo aplica, funcionando tanto para crear como para actualizar, cosa que kubectl create solo no hace (fallaría con AlreadyExists).
kubectl rollout restart deploy/api-reservas -n rutas-norte-pro
kubectl rollout status deploy/api-reservas -n rutas-norte-pro
kubectl get pods -l app=api-reservas -n rutas-norte-prodeployment.apps/api-reservas restarted
Waiting for deployment "api-reservas" rollout to finish: 2 of 4 updated replicas are available...
deployment "api-reservas" successfully rolled out
NAME READY STATUS RESTARTS AGE
api-reservas-7f4b8c9d6-2xkqp 1/1 Running 0 48s
api-reservas-7f4b8c9d6-8mvtr 1/1 Running 0 62s
api-reservas-7f4b8c9d6-j9wzl 1/1 Running 0 35s
api-reservas-7f4b8c9d6-qh4nc 1/1 Running 0 21sLos cuatro pods son nuevos (mismo pod-template-hash, AGE menor de un minuto y RESTARTS 0). Verificación de la credencial efectiva:
Consumidores que no se reinician con rollout restart de los Deployments:
- CronJobs como
informes-ocupacion(módulo 6): los pods se crean en cada ejecución, así que tomarán la credencial nueva solos; pero si hay una ejecución en curso durante la rotación, fallará. Hay que mirarkubectl get jobsantes de revocar. - Pods sueltos creados a mano para depurar. Se localizan con
kubectl get pods -n rutas-norte-pro -o json | jq '.items[] | select(.metadata.ownerReferences == null) | .metadata.name'. - StatefulSets (módulo 6):
rollout restartfunciona, pero es ordinal y más lento; hay que esperar a que termine. - Clientes fuera del clúster: herramientas de informes, copias de seguridad, cuadros de mando. No los ve Kubernetes y son los que provocan el incidente al revocar. Por eso el inventario de consumidores del apartado 11 no es burocracia.
Conclusión
Has saldado la deuda más grave del módulo 2. La contraseña de postgres-reservas ya no está en ningún manifiesto de Git: vive en un Secret, api-reservas la recibe como fichero montado en memoria con solo las claves que necesita, y postgres-reservas la toma de secretKeyRef para inicializarse. Un grep sobre k8s/ no devuelve nada.
Pero lo importante de esta lección no es el objeto, sino el criterio. Sabes que un Secret se diferencia de un ConfigMap en stringData, en el montaje en tmpfs, en que describe oculta el contenido y en que existe un campo type que valida la forma. Y sabes, porque lo has demostrado con un comando, que base64 no es cifrado: la protección real la aportan RBAC, el cifrado en reposo y el aislamiento por namespaces, no el objeto en sí. Conoces los siete tipos de Secret y para qué sirve cada uno, las tres formas de crearlos, y por qué stringData es siempre preferible a data al escribir a mano.
Sabes que el cifrado en reposo en etcd no viene activado, has visto la contraseña legible en un volcado de etcd, conoces los cinco proveedores de EncryptionConfiguration y por qué kms v2 es el único realmente sólido, y que activarlo no cifra retroactivamente lo existente. Tienes claro quién puede leer un secreto —con las dos sorpresas: list filtra contenidos y poder crear pods equivale a poder leerlo todo—, cómo se rota una credencial sin cortar el servicio y por qué las variables de entorno no se refrescan nunca. Tienes la lista de lo que jamás debe hacerse y un mapa del ecosistema real —Sealed Secrets, SOPS, External Secrets Operator, Vault, los KMS de las nubes— para cuando el Secret nativo se quede corto. Y no olvides la advertencia del inicio: con los datos personales que guarda Rutas Norte, el diseño definitivo debe validarlo un profesional de seguridad o el responsable de cumplimiento.
Las dos lecciones han dejado un cabo suelto común. Hemos visto los objetos que guardan la configuración y los secretos, y una de las formas de llevarlos al contenedor: el fichero montado. Falta la otra, la que aparece en el 80 % de los manifiestos del mundo real y que ambas lecciones han mencionado sin desarrollar. La siguiente, Variables de Entorno, la cubre entera: env, envFrom con prefix, valueFrom con configMapKeyRef y secretKeyRef, el flag optional, la Downward API para que api-reservas registre en sus trazas qué pod y qué nodo atendió cada petición, la expansión $(VAR) y sus reglas, las colisiones cuando la misma clave llega por varias vías, y la técnica del hash de la configuración en una anotación que resuelve de una vez el problema de que las variables de entorno no se refresquen nunca.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
