Módulo 3: Configuration Secrets Health And Autoscaling
3. Secrets: por qué una credencial nunca vive en la imagen
Descripción
cloud-security-and-guardrails-guide (Módulo 3, lección 2) ya construyó, con evidencia real, el caso completo de por qué un archivo .secrets en disco —aunque esté perfectamente gitignoreado desde el primer commit— sigue sin ser aceptable el día que las credenciales dejan de ser dummies: sin control de acceso propio, sin registro de quién lo leyó, sin ningún mecanismo asistido de rotación. Esta lección retoma exactamente ese mismo argumento, desde el ángulo de Kubernetes: un ConfigMap —el objeto de la lección anterior— tiene el mismo problema de fondo que ese archivo .secrets, y Kubernetes ofrece un objeto distinto para el caso sensible. Pero esta lección también incluye la honestidad que el diseño de esta guía promete: un Secret de Kubernetes no es cifrado por defecto, y quien lo trate como si lo fuera se equivoca de una forma que puede costarle caro.
Conexión con el módulo
Esta lección completa la segunda de las tres piezas de este módulo. La lección 4 monta, de verdad, tanto el ConfigMap de la lección 2 como el Secret de esta lección en andes-cargo-status-api, cerrando el ciclo de configuración externalizada que la lección 1 prometió.
El mismo antipatrón, visto desde Kubernetes
Retoma el archivo .secrets que cicd-and-gitops-on-aws-guide construyó y que cloud-security-and-guardrails-guide analizó a fondo:
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
Dos líneas de texto plano, sobre un disco físico. Esa guía identificó tres razones exactas por las que ese diseño —incluso perfectamente gitignoreado— dejaría de ser aceptable el día que esas credenciales fueran reales:
- Ningún control de acceso propio. Cualquier proceso con acceso de lectura al filesystem puede abrir el archivo con
cat, sin ninguna condición adicional. - Ningún registro de quién lo leyó, ni cuándo. Abrir un archivo de texto no genera ningún evento observable.
- Ningún mecanismo asistido de rotación. Cambiar el valor significa editar el archivo a mano y confiar en que todo lo que lo usa se entere del cambio a tiempo.
Un ConfigMap de Kubernetes —el objeto de la lección 2— hereda exactamente el mismo problema: sus valores viven en texto plano, visibles con un simple kubectl get configmap -o yaml, sin ninguna marca que distinga "esto es un endpoint" de "esto es una credencial". Si metieras AWS_ACCESS_KEY_ID en el ConfigMap de la lección anterior, tendrías, dentro de etcd (la base de datos del clúster, Módulo 1, lección 6), exactamente el mismo antipatrón que ese archivo .secrets — solo que ahora vive dentro de Kubernetes en vez de en un disco local.
Qué SÍ cambia con un Secret, y qué NO
Un objeto Secret de Kubernetes tiene la misma forma que un ConfigMap —pares clave-valor, alcance de namespace, montable vía envFrom o volumeMounts— pero con dos diferencias reales:
- Codificación en base64, no en texto plano directo (ver la demostración más abajo — y la advertencia importante que sigue).
- RBAC lo trata como un recurso distinto. Un rol de Kubernetes puede otorgar permiso de lectura sobre
configmapssin otorgarlo sobresecrets, y viceversa — la primera vez, en esta guía, que la palabra "secreto" tiene una consecuencia de control de acceso real y separada. (ElNetworkPolicydel Módulo 4 y elConstraintTemplatede Gatekeeper del Módulo 6 van a poder aplicar reglas específicas a objetosSecretque nunca aplicarían a unConfigMap.)
Lo que NO cambia, y hay que decirlo sin rodeos: un Secret de Kubernetes no está cifrado por defecto. Base64 es una codificación, no un cifrado — es reversible sin ninguna clave, con una sola línea de comando. Cualquiera con permiso de lectura sobre el objeto (kubectl get secret -o yaml, o acceso directo a etcd) puede recuperar el valor original en un segundo. La única forma de que un Secret esté realmente cifrado en reposo es habilitar cifrado de etcd a nivel de clúster (EncryptionConfiguration, una configuración del kube-apiserver que esta guía no activa en kind por simplicidad de laboratorio, y que en EKS real se resuelve de forma distinta, con AWS KMS — un tema que el Módulo 7 retoma). Sin esa capa adicional, un Secret es, en el fondo, un ConfigMap con una codificación reversible y un control de acceso más estricto — no una caja fuerte.
Demo: base64 no es cifrado, con evidencia real
Antes de crear el Secret real de andes-cargo-status-api (eso es la lección 4), confirma con tus propias manos que base64 se revierte sin ninguna clave:
echo -n "test" | base64
Qué esperar (literal, determinista — base64 es una función pura, el mismo texto produce siempre la misma salida):
dGVzdA==
Ahora revierte esa misma codificación, sin ninguna clave ni contraseña:
echo -n "dGVzdA==" | base64 -d
Qué esperar:
test
Confirma el mismo mecanismo con un Secret real de Kubernetes, creado de forma imperativa como demostración desechable:
kubectl create secret generic demo-secret -n andes-cargo \
--from-literal=AWS_ACCESS_KEY_ID=test \
--from-literal=AWS_SECRET_ACCESS_KEY=test
Qué esperar:
secret/demo-secret created
kubectl get secret demo-secret -n andes-cargo -o yaml
Qué esperar (literal — creationTimestamp/resourceVersion/uid son tus valores variables; los valores de data son literales, porque test en base64 siempre produce el mismo resultado):
apiVersion: v1
data:
AWS_ACCESS_KEY_ID: dGVzdA==
AWS_SECRET_ACCESS_KEY: dGVzdA==
kind: Secret
metadata:
creationTimestamp: "2026-08-14T19:55:26Z"
name: demo-secret
namespace: andes-cargo
resourceVersion: "4043"
uid: d4fa33b2-98f2-4a34-89e3-68e1aa2b64db
type: Opaque
Ahí está: kubectl create secret codificó test en dGVzdA== automáticamente —no tuviste que hacerlo tú—, pero cualquiera que tenga permiso de leer este objeto puede revertirlo sin esfuerzo:
kubectl get secret demo-secret -n andes-cargo -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d
Qué esperar:
test
Ninguna clave, ninguna contraseña, ningún paso adicional — la misma tubería de comandos que decodificó dGVzdA== manualmente unos párrafos atrás. Esto no es un defecto de Kubernetes: es una decisión de diseño documentada, y la razón por la que RBAC (quién puede correr kubectl get secret) es la protección real, no la codificación en sí misma.
Limpia el objeto de demostración:
kubectl delete secret demo-secret -n andes-cargo
Qué esperar:
secret "demo-secret" deleted from andes-cargo namespace
Por qué esta guía sigue usando test/test, con los ojos abiertos
Esta guía va a crear, en la lección 4, un Secret real con exactamente los mismos valores dummy que aws-serverless-and-containers-guide ya usó: AWS_ACCESS_KEY_ID=test, AWS_SECRET_ACCESS_KEY=test. Es la misma decisión, documentada de la misma forma que esa guía la documentó: LocalStack no valida que estas credenciales sean reales, solo que estén presentes — no son un secreto que proteger, son un marcador de posición. Usar el objeto Secret de Kubernetes para guardarlas, aun así, no es teatro: es la práctica correcta, ejercitada con el objeto correcto, sobre datos que no importa que se filtren. El día que este mismo patrón se aplicara contra una cuenta AWS real, el objeto sería el mismo Secret — lo que cambiaría es qué tan grave sería un error de RBAC, no la mecánica.
Analogía: la receta en la pared, la clave de la caja fuerte
El menú de servicio a la habitación de un hotel se puede pegar en la pared de la recepción, a la vista de cualquiera —es información útil, sin ningún riesgo si alguien la lee sin permiso—. La combinación de la caja fuerte del hotel es harina de otro costal: no se pega en ninguna pared, vive en un lugar distinto, con acceso restringido a quien realmente lo necesita. Un ConfigMap es la receta en la pared —el endpoint de LocalStack, el nombre de la tabla, cualquier dato que no importa que alguien vea—. Un Secret es la clave de la caja fuerte —pero, y esta es la honestidad de esta lección, una caja fuerte con la combinación escrita en un papel dentro de un sobre transparente: cualquiera que tenga acceso al sobre (permiso de kubectl get secret) puede leer la combinación sin ningún esfuerzo. El sobre transparente sigue siendo mejor que dejar la combinación pegada en la misma pared que el menú —pero no confundas "separado" con "cifrado".
Errores comunes
Creer que un Secret de Kubernetes está cifrado, y bajar la guardia sobre quién tiene acceso de lectura (conceptual, el más importante de esta lección). Qué pasa: alguien ve la palabra "Secret" y el hecho de que los valores aparecen codificados en kubectl get -o yaml, y asume que están protegidos de la misma forma que una contraseña con hash. Por qué pasa: el nombre del objeto y el hecho de que no se vea el texto plano a simple vista invitan a esa conclusión — pero la demostración de esta lección prueba lo contrario con un solo comando. Cómo detectarlo: si nunca revisaste qué RBAC tiene permiso de get/list sobre secrets en tu clúster, asumiendo que "ya están protegidos". Cómo corregirlo: trata cualquier permiso de lectura sobre secrets con la misma seriedad que le darías a un permiso de lectura sobre un archivo de credenciales en disco — porque, en la práctica, con base64 de por medio, es exactamente eso.
Confundir "base64" con "ofuscado lo suficiente" para pegarlo en un chat o un ticket (de disciplina). Qué pasa: alguien copia el valor de data.AWS_SECRET_ACCESS_KEY de un kubectl get secret -o yaml y lo pega en un canal de Slack o un ticket, razonando que "está codificado, no se puede leer directamente". Cómo detectarlo: si buscas, en el historial de mensajes de tu equipo, una cadena que termine en == o = (el relleno característico de base64) cerca de la palabra "secret". Cómo corregirlo: cualquier valor de data de un Secret debe tratarse como texto plano a todos los efectos prácticos de manejo — la demostración de esta lección tomó menos de un segundo en revertirlo.
Usar kubectl create secret sin -n andes-cargo y romper la referencia del Deployment (de configuración, el mismo patrón de la lección 2). Qué pasa: el Secret se crea en default en vez de andes-cargo, y el Deployment que lo referencia —en el namespace correcto— nunca lo encuentra, produciendo un error al intentar montarlo. Cómo detectarlo: kubectl describe pod muestra un evento como Error: secret "andes-cargo-status-api-secrets" not found si el Pod intenta arrancar sin encontrar el Secret en su propio namespace. Cómo corregirlo: la lección 4 usa -n andes-cargo en cada comando, exactamente por esta razón — un Secret, igual que un ConfigMap, solo es visible para Pods de su mismo namespace.
Ejercicios
Ejercicio 1 — Explica la diferencia entre ConfigMap y Secret sin decir "uno es más seguro". En dos o tres frases, sin usar la frase "más seguro" ni "cifrado", explica a un colega la diferencia real entre un ConfigMap y un Secret de Kubernetes.
Ver solución
Una respuesta razonable: "Ambos guardan pares clave-valor de la misma forma, con la misma facilidad de lectura si tienes el permiso correcto — la diferencia real es que RBAC puede otorgar acceso a uno sin otorgarlo al otro, así que un Secret te permite separar quién puede leer configuración general de quién puede leer credenciales. La codificación base64 no agrega protección por sí misma, es reversible con un comando."
Ejercicio 2 — Reconstruye las tres razones de cloud-security-and-guardrails-guide aplicadas a un ConfigMap. Sin volver a la sección correspondiente, explica por qué meter una credencial en un ConfigMap de Kubernetes tiene los mismos tres problemas que el archivo .secrets de esa guía hermana.
Ver solución
- Ningún control de acceso propio adicional — un
ConfigMapno tiene ninguna separación de RBAC frente a otros recursos del mismo tipo; cualquiera con permiso general de lectura deconfigmapsvería la credencial. - Ningún registro de quién lo leyó — leer un
ConfigMapvíakubectl get -o yamlno genera, por sí mismo, ningún evento de auditoría distinto al de leer cualquier otroConfigMap. - Ningún mecanismo asistido de rotación — cambiar el valor de un
ConfigMapes editar texto plano a mano, sin ninguna coordinación automática con quien lo consume.
Ejercicio 3 — Decodifica un Secret sin usar kubectl -o jsonpath. Si tuvieras la salida completa de kubectl get secret <nombre> -o yaml copiada en un archivo de texto, ¿qué dos comandos de Unix encadenarías para obtener el valor real de un campo específico de data, sin usar ningún flag especial de kubectl?
Ver solución
grep <clave>: archivo.yaml | awk '{print $2}' | base64 -d — o cualquier combinación equivalente que extraiga el valor de la línea correspondiente y lo pase a base64 -d. El punto del ejercicio es confirmar que decodificar un Secret no depende de ninguna herramienta especial de Kubernetes: cualquier texto que termine con el relleno de base64 se revierte con el mismo comando estándar de Unix usado en esta lección.
Resumen y siguiente paso
Esta lección retomó, desde Kubernetes, el mismo argumento que cloud-security-and-guardrails-guide ya construyó contra un archivo .secrets en disco: una credencial en texto plano, sin control de acceso propio ni registro de lectura, es un antipatrón sin importar dónde viva. Un objeto Secret de Kubernetes agrega una separación real de RBAC frente a un ConfigMap — pero, con la honestidad completa que esta guía promete, no agrega cifrado por defecto: base64 es una codificación reversible en un segundo, confirmado con evidencia real en esta lección. La protección real de un Secret es quién tiene permiso de leerlo, no cómo se ve codificado.
Antes de avanzar deberías poder: explicar por qué base64 no es cifrado, con una demostración de un solo comando; nombrar la diferencia real de RBAC entre ConfigMap y Secret; y justificar por qué esta guía sigue usando credenciales dummy (test/test) incluso dentro de un objeto pensado para datos sensibles.
Siguiente lección: manos a la obra, ConfigMap y Secret para andes-cargo-status-api. Ahí construyes los objetos reales —no de demostración— y los montas en el Deployment, sin reconstruir la imagen ni una sola vez.
Recursos
- Kubernetes — Secrets — referencia oficial completa; la sección "Uses for Secrets" y la nota explícita de que un
Secretno está cifrado por defecto son la base técnica de esta lección. - Kubernetes — Encrypting Confidential Data at Rest — la documentación oficial del mecanismo real de cifrado (
EncryptionConfigurationdeetcd), nombrado pero no activado en esta guía. cloud-security-and-guardrails-guide(NIEVA), Módulo 3, lección 2 — el antipatrón original sobre el archivo.secrets, que esta lección retoma desde Kubernetes.aws-serverless-and-containers-guide(NIEVA), Módulo 6, lección 5 — el origen de las credenciales dummytest/test, reutilizadas sin cambios en elSecretde la lección 4.