0%
ConfigMaps y Secrets

Configuración

Una imagen, varios entornos — y lo que «Secret» no significa.

15-20 min

ConfigMaps y Secrets

Una imagen de contenedor debe poder desplegarse en desarrollo, en preproducción y en producción sin reconstruirse. Es el principio que justifica toda esta lección, y no es teórico: si la URL de la base de datos está escrita en la imagen, el artefacto que prueba no es el que entrega, y la prueba ya no demuestra nada.

La otra mitad del problema es más desagradable. Una contraseña incluida en una imagen se queda ahí para siempre: las capas son inmutables, borrarla en una capa posterior no la elimina del historial, y cualquier persona con acceso al registro puede leerla. Un secreto entregado dentro de una imagen no es un secreto, es una fuga con retardo.

Por eso Kubernetes separa dos objetos para dos usos: la ConfigMap contiene la configuración no sensible y el Secret contiene los datos confidenciales. Ambos se inyectan en los contenedores del mismo modo —variables de entorno o archivos montados—, pero el clúster los trata de forma distinta.

Una advertencia que conviene hacer ya, antes de cualquier sintaxis, porque es el malentendido más extendido del ecosistema: un Secret de Kubernetes no está cifrado por defecto. Está codificado en base64, que es un formato de transporte, no una protección. Veremos exactamente qué implica esto y qué hay que añadir para que la palabra «secret» esté justificada.

La ConfigMap

Una ConfigMap es un diccionario de pares clave-valor, o un conjunto de archivos, almacenado en el clúster.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: atelier
data:
  # Valores simples: una clave, una cadena
  APP_ENV: "production"
  LOG_LEVEL: "info"
  API_TIMEOUT: "30"
  FEATURE_NOUVEAU_PANIER: "true"

  # Archivo completo: la clave pasa a ser el nombre del archivo si se monta como volumen
  application.properties: |
    server.port=8080
    spring.jpa.show-sql=false
    logging.level.root=INFO

  nginx.conf: |
    server {
      listen 80;
      location /api/ {
        proxy_pass http://api:8080/;
      }
    }

Dos detalles de sintaxis que hacen perder tiempo cuando se ignoran. En primer lugar, todos los valores de data son cadenas: API_TIMEOUT: 30 sin comillas es rechazado por el esquema, y true sin comillas se interpreta como un booleano de YAML. Acostúmbrese a poner comillas siempre. En segundo lugar, el | de YAML conserva los saltos de línea, lo que permite incluir un archivo de configuración tal cual.

Las variantes imperativas son prácticas para explorar:

# Desde valores literales
kubectl create configmap app-config \
  --from-literal=APP_ENV=production \
  --from-literal=LOG_LEVEL=info

# Desde un archivo: la clave es el nombre del archivo
kubectl create configmap nginx-config --from-file=nginx.conf

# Desde un directorio completo: una clave por archivo
kubectl create configmap tous-configs --from-file=./config/

# Desde un archivo .env: una clave por línea
kubectl create configmap app-config --from-env-file=.env
kubectl get configmaps
kubectl describe configmap app-config     # muestra los valores en claro
kubectl get configmap app-config -o yaml

El Secret

Estructuralmente idéntico, con un campo más y una codificación esperada.

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: atelier
type: Opaque
stringData:
  # stringData: usted escribe en claro y Kubernetes codifica al guardar
  DB_USER: "app_user"
  DB_PASSWORD: "S3cr3t-a-changer"
  DATABASE_URL: "postgresql://app_user:S3cr3t-a-changer@postgres:5432/appdb"

stringData es casi siempre la mejor opción para un manifiesto escrito a mano: evita tener que codificar uno mismo. El campo data, en cambio, espera valores que ya estén en base64:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  DB_USER: YXBwX3VzZXI=              # echo -n 'app_user' | base64
  DB_PASSWORD: UzNjcjN0LWEtY2hhbmdlcg==
# Creación imperativa: la codificación se hace por usted
kubectl create secret generic db-credentials \
  --from-literal=DB_USER=app_user \
  --from-literal=DB_PASSWORD='S3cr3t-a-changer'

# Desde un archivo (certificado, clave privada, archivo de credenciales)
kubectl create secret generic tls-keys --from-file=./server.key

# Secret dedicado a registros privados, para imagePullSecrets
kubectl create secret docker-registry registre-prive \
  --docker-server=registry.example.com \
  --docker-username=deploiement \
  --docker-password='jeton-du-registre'

Los tipos de Secret más utilizados:

typeUso
Opaquedatos arbitrarios: el caso general
kubernetes.io/dockerconfigjsonautenticación en un registro de imágenes privado
kubernetes.io/tlscertificado y clave privada, consumidos por los Ingress
kubernetes.io/service-account-tokentoken de una cuenta de servicio

Qué protege realmente un «Secret»

Esta es la demostración que conviene hacer una vez para no olvidarla nunca:

# El contenido «protegido»
kubectl get secret db-credentials -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
# S3cr3t-a-changer

Ninguna contraseña, ninguna clave, ningún descifrado. El base64 nunca ha sido un mecanismo de seguridad: sirve para transportar bytes binarios en un campo de texto. Lo que esto implica en concreto:

  • Cualquier persona autorizada a leer los Secrets de un namespace lee todas las contraseñas de ese namespace en claro.
  • Un Secret confirmado en Git es un secreto comprometido, aunque esté codificado en base64.
  • Por defecto, los Secrets se almacenan en claro en etcd. Una copia de seguridad de etcd mal protegida lo expone todo.

Lo que hace realmente confidencial a un Secret son tres medidas añadidas:

  1. El RBAC. Es la única protección que funciona por defecto: restringir quién puede leer los Secrets. Un Secret solo es confidencial si los permisos de lectura lo son.
  2. El cifrado en reposo. Se activa en el servidor de API mediante una EncryptionConfiguration, con una clave local o un servicio de gestión de claves (KMS). No viene activado de serie en un clúster autogestionado; las ofertas gestionadas suelen ofrecerlo, pero conviene comprobarlo caso por caso.
  3. Un gestor externo. HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, con un operador que sincroniza hacia el clúster. La rotación pasa a ser posible y existe auditoría, algo que Kubernetes por sí solo no proporciona.

Frente a una variable de entorno escrita en un manifiesto de Deployment, el Secret aporta, con todo, dos ventajas reales: el valor no aparece en kubectl describe deployment y se puede administrar por separado del despliegue. Es un avance, no una garantía.

Inyectar mediante variables de entorno

El método más directo, clave por clave:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: mon-api:1.0.0
          ports:
            - containerPort: 8080
          env:
            # Una clave de ConfigMap a una variable, con posibilidad de renombrarla
            - name: ENVIRONNEMENT
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: APP_ENV

            # Una clave de Secret a una variable
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: DB_PASSWORD

            # Un valor literal, para lo que ni se comparte ni es sensible
            - name: PORT
              value: "8080"

Para importarlo todo de una vez, sin enumerar:

          envFrom:
            - configMapRef:
                name: app-config
            - secretRef:
                name: db-credentials

envFrom es conciso, pero tiene una contrapartida que hay que valorar: los nombres de las variables pasan a ser implícitos. Al leer el Deployment, ya no se sabe qué variables recibe la aplicación. Para una aplicación cuyo código no controla, es preferible la enumeración explícita.

Por último, añada optional: true cuando una clave pueda faltar legítimamente; de lo contrario, el Pod queda bloqueado en CreateContainerConfigError:

            - name: FONCTION_EXPERIMENTALE
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: FEATURE_NOUVEAU_PANIER
                  optional: true

El límite principal de las variables de entorno

Es estructural: las variables de entorno se fijan al arrancar el proceso. Modificar una ConfigMap no cambia nada en los Pods que ya están en ejecución y no desencadena ningún redespliegue. Se obtiene entonces un clúster donde la ConfigMap dice una cosa y la aplicación hace otra: una incoherencia silenciosa, muy difícil de detectar.

Hay dos respuestas posibles, según la necesidad.

Forzar un redespliegue en cada cambio, haciendo que la plantilla dependa de una huella de la configuración:

    metadata:
      labels:
        app: api
      annotations:
        # A recalcular en cada entrega: cualquier modificación de la plantilla
        # desencadena una actualización progresiva de los Pods.
        checksum/config: "9f2b7c1e4a"

O, de forma más sencilla en la operación diaria:

# Reinicio progresivo del Deployment, sin cambiar la imagen
kubectl rollout restart deployment/api

Inyectar mediante volumen

Montar una ConfigMap como un directorio genera un archivo por clave. Es el modo obligatorio para un archivo de configuración completo, y el único que permite una actualización sin reinicio.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80
          volumeMounts:
            # Todas las claves de la ConfigMap se convierten aquí en archivos
            - name: config-volume
              mountPath: /etc/app/config
              readOnly: true
            # Un Secret montado: permisos restrictivos, nunca en un log
            - name: secrets-volume
              mountPath: /etc/app/secrets
              readOnly: true
      volumes:
        - name: config-volume
          configMap:
            name: app-config
            items:
              # Sin «items», se montan todas las claves.
              # Con él, se elige con precisión y se renombra.
              - key: nginx.conf
                path: default.conf
        - name: secrets-volume
          secret:
            secretName: db-credentials
            defaultMode: 0400     # solo lectura para el propietario

Con este manifiesto, el contenedor ve /etc/app/config/default.conf con el valor de la clave nginx.conf. El renombrado mediante items permite colocar un archivo donde la aplicación lo espera, sin contorsiones.

Para reemplazar un único archivo en un directorio existente sin ocultar el resto de su contenido, la clave es subPath:

          volumeMounts:
            - name: config-volume
              mountPath: /etc/nginx/conf.d/default.conf
              subPath: default.conf     # reemplaza UN archivo, no el directorio

Sin subPath, montar sobre /etc/nginx/conf.d/ ocultaría todo el contenido original del directorio. Es una causa clásica de contenedor que se niega a arrancar tras añadir una configuración.

La actualización en caliente y sus condiciones

Las ConfigMaps montadas como volumen sí se actualizan en el contenedor en ejecución, sin reinicio. Hay tres reservas que conviene conocer:

  • La propagación tarda hasta un minuto aproximadamente, según el período de sincronización del kubelet.
  • La aplicación debe saber recargar su archivo. nginx necesita un nginx -s reload, y una aplicación Spring Boot no vuelve a leer sus propiedades de forma espontánea. El archivo cambia, el comportamiento no.
  • Un montaje con subPath nunca se actualiza. Es exactamente el compromiso de esta opción: precisión del montaje a cambio de perder la recarga.
# Modificar una ConfigMap in situ y luego observar el archivo en el Pod
kubectl edit configmap app-config
kubectl exec -it deploy/web -- cat /etc/app/config/default.conf

Variables de entorno o volumen: cuál elegir

CriterioVariables de entornoVolumen
Valores simples y cortosadecuadoverboso
Archivo de configuración completoinadecuadoúnica opción
Actualización sin reinicioimposibleposible, con reservas
Visibilidad en /proc y en los logs de falloexpuestalimitada a quienes pueden leer el archivo
Salida de kubectl describe podnombres de las variables visiblessolo la ruta del montaje

Para los secretos, el volumen es más seguro, y por una razón concreta que se puede comprobar uno mismo:

# El entorno del proceso principal se puede leer en /proc.
kubectl exec -it deploy/api -- sh -c 'tr "\0" "\n" < /proc/1/environ | grep DB_'
# DB_PASSWORD=S3cr3t-a-changer

Las variables de entorno también aparecen en las trazas de excepciones y en los informes de fallo, y las heredan todos los procesos hijos. Un archivo montado en modo 0400 tiene una superficie de exposición notablemente más reducida.

Las ConfigMaps inmutables

Marcar una ConfigMap o un Secret como inmutable prohíbe cualquier modificación posterior y, de paso, reduce la carga de vigilancia sobre el servidor de API en los clústeres grandes.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v3
immutable: true
data:
  APP_ENV: "production"
  LOG_LEVEL: "warn"

Esto impone una disciplina útil: en lugar de modificar app-config, se crea app-config-v4 y se hace que el Deployment apunte a ella. El cambio de configuración pasa a ser una modificación versionada del Deployment, por tanto visible en kubectl rollout history y reversible con kubectl rollout undo. Una ConfigMap modificada in situ, en cambio, no deja ningún rastro aprovechable.

A recordar

  • Una imagen debe ser independiente de su entorno. La ConfigMap lleva la configuración, el Secret los datos sensibles, y ambos se inyectan mediante variables de entorno o mediante un volumen.
  • Un Secret no está cifrado: está codificado en base64, se lee con un simple base64 -d y, por defecto, se almacena en claro en etcd. Lo que realmente lo protege es el RBAC, el cifrado en reposo configurado en el servidor de API y un gestor externo.
  • Las variables de entorno se fijan al arrancar: modificar una ConfigMap no cambia nada en los Pods en ejecución. Fuerce el cambio mediante una anotación de checksum o un kubectl rollout restart.
  • Los volúmenes se propagan en caliente (con un margen de aproximadamente un minuto), siempre que la aplicación recargue su archivo, y nunca con subPath, que congela el montaje.
  • Prefiera el volumen para los secretos: las variables de entorno acaban en /proc, en las trazas de error y en todos los procesos hijos.

Consejo: versione sus ConfigMaps (app-config-v3) y hágalas inmutables. Así, un cambio de configuración se convierte en una revisión del Deployment, y por tanto es trazable y reversible como cualquier otro despliegue.

Comentarios

Los comentarios funcionan con GitHub Discussions

Inicia sesión con GitHub para participar en la discusión

¡Enlace copiado!