Almacenar secretos de Kubernetes en repositorios Git sin cifrado es uno de los errores de seguridad más comunes en despliegues cloud-native. Aunque los Secrets de Kubernetes usan codificación base64, esto no proporciona protección criptográfica alguna — cualquier persona con acceso al repositorio puede decodificarlos al instante. SOPS (Secrets OPerationS) combinado con cifrado Age resuelve este problema de forma elegante, permitiendo almacenar secretos cifrados directamente en Git manteniendo compatibilidad total con flujos de trabajo GitOps como ArgoCD y FluxCD.
Requisitos Previos
- Un clúster de Kubernetes en funcionamiento (v1.24+) con
kubectlconfigurado sopsv3.8+ instaladoagev1.1+ instalado- Familiaridad básica con Kubernetes Secrets y manifiestos YAML
- Repositorio Git para tus manifiestos de Kubernetes
- Opcional: ArgoCD o FluxCD para integración GitOps
Entendiendo los Riesgos de los Secrets de Kubernetes
Los Secrets de Kubernetes frecuentemente se malinterpretan como un mecanismo de almacenamiento seguro. En realidad, tienen limitaciones significativas que debes entender antes de diseñar tu estrategia de secretos.
Base64 No Es Cifrado
Un Secret estándar de Kubernetes almacena valores como cadenas codificadas en base64:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4=
password: cDRzc3cwcmQxMjM=
Decodificar estos valores es trivial:
echo "cDRzc3cwcmQxMjM=" | base64 -d
# Salida: p4ssw0rd123
Cualquier persona que clone tu repositorio u obtenga acceso de lectura a etcd puede extraer cada secreto de tu clúster.
Preocupaciones de Almacenamiento en etcd
Por defecto, Kubernetes almacena los Secrets sin cifrar en etcd. Aunque puedes habilitar cifrado en reposo con EncryptionConfiguration, esto solo protege los archivos de datos de etcd — no las respuestas del servidor API ni los manifiestos almacenados en Git. Necesitas una solución que cifre los secretos antes de que entren en tu sistema de control de versiones.
El Problema de Git
GitOps requiere que el estado deseado de tu clúster viva en Git. Pero hacer commit de secretos en texto plano a Git significa:
- Cada desarrollador con acceso al repositorio ve las credenciales de producción
- El historial de secretos persiste en Git para siempre, incluso después de eliminarlos
- Repositorios filtrados exponen cada secreto jamás comiteado
- Los marcos de cumplimiento (SOC 2, PCI-DSS) prohíben credenciales en texto plano en el control de código fuente
Instalación de SOPS y Age
Linux
# Instalar Age
sudo apt-get install age
# O desde las releases de GitHub
curl -LO https://github.com/FiloSottile/age/releases/download/v1.2.0/age-v1.2.0-linux-amd64.tar.gz
tar xzf age-v1.2.0-linux-amd64.tar.gz
sudo mv age/age age/age-keygen /usr/local/bin/
# Instalar SOPS
curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
sudo mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
macOS
brew install sops age
Verifica ambas instalaciones:
sops --version
# sops 3.9.4
age --version
# v1.2.0
Cifrado de Secretos con SOPS y Age
Generar un Par de Claves Age
age-keygen -o age-key.txt
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Almacena la clave privada de forma segura. La clave pública es segura para compartir y commitear en tu repositorio:
# Almacenar la clave donde SOPS pueda encontrarla
mkdir -p ~/.config/sops/age
mv age-key.txt ~/.config/sops/age/keys.txt
# O establecer la variable de entorno
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
Crear Configuración .sops.yaml
Crea un archivo .sops.yaml en la raíz de tu repositorio para definir reglas de cifrado:
creation_rules:
# Cifrar todos los archivos en el directorio secrets/
- path_regex: secrets/.*\.yaml$
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Cifrar secretos de staging con una clave diferente
- path_regex: envs/staging/secrets/.*\.yaml$
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
age1lzd99uca0lqtmgahfmxj4gvr2fcswcaxmxnz30fwcmm22hjvrzrqsnqxsl
# Clave diferente para producción
- path_regex: envs/production/secrets/.*\.yaml$
age: >-
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
age1an5quvgyl8uny5mkmgkzpnpe9wuufrl2vvmqsht4xp3k96s608q0eamcl
Múltiples destinatarios (separados por coma) habilitan acceso de equipo — cualquier clave listada puede descifrar el archivo.
Cifrar un Secret de Kubernetes
Comienza con tu manifiesto de secreto en texto plano:
# secrets/db-credentials.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
stringData:
username: admin
password: "s3cur3-pr0d-p@ssw0rd"
connection-string: "postgresql://admin:s3cur3-pr0d-p@ssw0rd@db.internal:5432/myapp"
Cífralo con SOPS:
sops --encrypt --in-place secrets/db-credentials.yaml
El archivo cifrado preserva la estructura YAML pero cifra los valores:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
stringData:
username: ENC[AES256_GCM,data:k8mN3w==,iv:abc...,tag:def...,type:str]
password: ENC[AES256_GCM,data:dGhpcyBpcyBl...,iv:ghi...,tag:jkl...,type:str]
connection-string: ENC[AES256_GCM,data:bG9uZ2VyIHN0cmluZw==...,iv:mno...,tag:pqr...,type:str]
sops:
age:
- recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-17T10:00:00Z"
version: 3.9.4
Las claves (apiVersion, kind, metadata) permanecen legibles mientras los valores están cifrados. Esto significa que git diff muestra qué secretos cambiaron sin revelar los valores reales.
Descifrar y Aplicar
# Descifrar y aplicar en un solo comando
sops --decrypt secrets/db-credentials.yaml | kubectl apply -f -
# O descifrar a un archivo temporal
sops --decrypt secrets/db-credentials.yaml > /tmp/secret.yaml
kubectl apply -f /tmp/secret.yaml
rm -f /tmp/secret.yaml
Integración con Flujos de Trabajo GitOps
ArgoCD con KSOPS
KSOPS es un plugin de Kustomize que descifra archivos cifrados con SOPS durante las operaciones de sincronización de ArgoCD.
Instala KSOPS en tu servidor de repositorio de ArgoCD:
# Parche de argocd-repo-server
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-repo-server
namespace: argocd
spec:
template:
spec:
containers:
- name: argocd-repo-server
env:
- name: SOPS_AGE_KEY
valueFrom:
secretKeyRef:
name: sops-age-key
key: age-key.txt
- name: XDG_CONFIG_HOME
value: /.config
volumeMounts:
- mountPath: /.config/kustomize/plugin/viaduct.ai/v1/ksops
name: custom-tools
initContainers:
- name: install-ksops
image: viaductoss/ksops:v4.3.2
command: ["/bin/sh", "-c"]
args:
- cp /usr/local/bin/ksops /.config/kustomize/plugin/viaduct.ai/v1/ksops/ksops
volumeMounts:
- mountPath: /.config/kustomize/plugin/viaduct.ai/v1/ksops
name: custom-tools
volumes:
- name: custom-tools
emptyDir: {}
Crea un generador KSOPS en tu overlay de Kustomize:
# secret-generator.yaml
apiVersion: viaduct.ai/v1
kind: ksops
metadata:
name: secret-generator
files:
- secrets/db-credentials.yaml
- secrets/api-keys.yaml
Integración Nativa de FluxCD
FluxCD tiene soporte nativo para SOPS. Crea un secreto de descifrado y configura tu Kustomization:
# Crear el secreto de clave Age en el namespace flux-system
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=~/.config/sops/age/keys.txt
# flux-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
namespace: flux-system
spec:
interval: 10m
path: ./envs/production
prune: true
sourceRef:
kind: GitRepository
name: my-app
decryption:
provider: sops
secretRef:
name: sops-age
FluxCD descifra automáticamente cualquier archivo cifrado con SOPS en la ruta especificada durante la reconciliación.
SOPS+Age vs Sealed Secrets vs Vault vs External Secrets
| Característica | SOPS + Age | Sealed Secrets | HashiCorp Vault | External Secrets |
|---|---|---|---|---|
| Ubicación del cifrado | Lado cliente | Lado controlador | Lado servidor | Lado servidor |
| Compatible con Git | Sí (YAML cifrado) | Sí (recurso personalizado) | No (solo referencias) | No (solo referencias) |
| Infraestructura necesaria | Ninguna | Controlador en clúster | Servidor Vault | Operador + backend |
| Gestión de claves | Archivos de clave Age | Certificado del clúster | Políticas de Vault | Varía según backend |
| Multi-entorno | Reglas .sops.yaml | Certificados por clúster | Namespaces/políticas | Múltiples stores |
| Integración GitOps | Plugins ArgoCD/Flux | K8s nativo | Driver CSI/injector | Sincronización operador |
| Rotación | Re-cifrar con SOPS | Re-sellar requerido | Secretos dinámicos | Depende del backend |
| Complejidad | Baja | Baja | Alta | Media |
| Funciona offline | Sí | No | No | No |
| Ideal para | Equipos pequeños-medianos | Clústeres individuales | Empresa/cumplimiento | Multi-cloud |
SOPS+Age destaca cuando deseas simplicidad, capacidad offline y GitOps verdadero sin infraestructura adicional.
Escenario del Mundo Real
Gestionas un despliegue multi-entorno de Kubernetes en clústeres de desarrollo, staging y producción. Tu equipo de ocho ingenieros usa ArgoCD para GitOps, y necesitas una solución de secretos que:
- Permita a los desarrolladores crear y actualizar secretos sin acceso al clúster
- Mantenga secretos cifrados en Git para auditabilidad
- Use diferentes claves de cifrado por entorno
- Permita rotación de claves sin re-desplegar cada secreto
Así es como estructurarías tu repositorio:
k8s-manifests/
├── .sops.yaml
├── base/
│ ├── deployment.yaml
│ └── service.yaml
├── envs/
│ ├── dev/
│ │ ├── kustomization.yaml
│ │ ├── secret-generator.yaml
│ │ └── secrets/
│ │ └── app-secrets.yaml # cifrado con clave dev
│ ├── staging/
│ │ ├── kustomization.yaml
│ │ ├── secret-generator.yaml
│ │ └── secrets/
│ │ └── app-secrets.yaml # cifrado con clave staging
│ └── production/
│ ├── kustomization.yaml
│ ├── secret-generator.yaml
│ └── secrets/
│ └── app-secrets.yaml # cifrado con clave prod
Cada entorno tiene su propio par de claves Age. Los desarrolladores tienen la clave de dev, los líderes de equipo tienen dev+staging, y solo el pipeline CI/CD tiene la clave de producción. El archivo .sops.yaml dirige el cifrado a la clave correcta basándose en la ruta del archivo.
Errores Comunes y Casos Límite
La rotación de claves requiere re-cifrado. Cuando rotas una clave Age, debes descifrar cada archivo con la clave antigua y re-cifrar con la nueva. SOPS proporciona sops updatekeys para esto, pero pruébalo primero:
# Actualizar claves para un solo archivo (usa reglas de .sops.yaml)
sops updatekeys secrets/db-credentials.yaml
# Re-cifrar todos los secretos en lote
find . -name "*.yaml" -path "*/secrets/*" -exec sops updatekeys {} \;
Cifrado parcial con encrypted_regex. Por defecto, SOPS cifra todos los valores. Usa encrypted_regex en .sops.yaml para cifrar solo claves específicas:
creation_rules:
- path_regex: secrets/.*\.yaml$
encrypted_regex: "^(data|stringData)$"
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Los secretos binarios necesitan codificación base64 primero. SOPS opera sobre archivos de texto. Para secretos binarios (certificados TLS, keystores), codifícalos en base64 antes de cifrar:
# Codificar el certificado, luego dejar que SOPS cifre la cadena base64
cat tls.crt | base64 -w0 > tls.crt.b64
Nunca commitees la clave privada de Age. Agrégala a .gitignore inmediatamente:
echo "age-key.txt" >> .gitignore
echo "*.agekey" >> .gitignore
La sección de metadatos de SOPS crece con los destinatarios. Cada destinatario Age adicional agrega ~300 bytes de metadatos. Con muchos destinatarios, considera usar una clave de equipo compartida distribuida a través de un canal seguro en lugar de claves individuales.
Solución de Problemas
Error: could not find common decryption key
La clave privada de Age no está disponible. Verifica la ubicación del archivo de clave:
# Verificar si el archivo de clave existe
ls -la ~/.config/sops/age/keys.txt
# O verificar la variable de entorno
echo $SOPS_AGE_KEY_FILE
# Verificar que la clave coincida con el destinatario
grep "public key" ~/.config/sops/age/keys.txt
Error: failed to decrypt
El archivo fue cifrado con una clave Age diferente. Verifica qué clave se usó:
sops --decrypt --verbose secrets/db-credentials.yaml 2>&1 | grep "recipient"
La sincronización de ArgoCD falla con errores de KSOPS Asegúrate de que el contenedor init de KSOPS se completó exitosamente y que el secreto de clave Age existe:
kubectl logs deployment/argocd-repo-server -n argocd -c install-ksops
kubectl get secret sops-age-key -n argocd
FluxCD Kustomization atascado en Not Ready
Revisa los logs del controlador de Kustomize:
kubectl logs deployment/kustomize-controller -n flux-system | grep -i sops
Causa común: el secreto sops-age está en el namespace incorrecto o tiene el nombre de clave equivocado (debe ser age.agekey).
Archivo cifrado muestra error mac mismatch
El archivo fue modificado después del cifrado sin usar sops --set o sops edit. Re-cifra desde la fuente en texto plano:
sops --decrypt secrets/db-credentials.yaml > /tmp/plain.yaml
sops --encrypt /tmp/plain.yaml > secrets/db-credentials.yaml
rm -f /tmp/plain.yaml
Resumen
- Los Secrets de Kubernetes usan codificación base64, no cifrado — no son seguros por defecto
- SOPS cifra valores YAML manteniendo las claves legibles, habilitando diffs significativos en Git
- Age proporciona gestión de claves más simple que PGP sin sobrecarga de configuración
- El archivo
.sops.yamldefine reglas de cifrado por ruta para configuraciones multi-entorno - ArgoCD se integra mediante el plugin KSOPS; FluxCD tiene soporte nativo de descifrado SOPS
- La rotación de claves requiere re-cifrar todos los archivos afectados con
sops updatekeys - Almacena siempre las claves privadas de Age fuera del repositorio y en
.gitignore - Para entornos empresariales que requieren secretos dinámicos, considera HashiCorp Vault