Gestión de Secretos DevOps GitOps: 6 Patrones de Producción desde Sealed Secrets hasta External Secrets Operator
¿Confirmar Secretos en Git? Estás Sentado sobre una Bomba de Tiempo
GitOps gira en torno a "Git como la Única Fuente de Verdad", pero los Secrets de Kubernetes están simplemente codificados en Base64 — no cifrados. Subir Secrets codificados en Base64 a un repositorio Git es equivalente a almacenar contraseñas de bases de datos, claves API y certificados TLS en texto plano. Una vez que el repositorio se filtra (configuración incorrecta interna, brecha en la cadena de suministro de terceros, limpieza olvidada en un repositorio bifurcado), todos los secretos quedan expuestos instantáneamente.
En 2026, la gestión de secretos DevOps GitOps ya no es opcional — es un requisito imprescindible para despliegues en producción. Este artículo cubre 6 patrones de gestión de secretos de nivel producción, desde el almacenamiento cifrado con Sealed Secrets hasta la inyección dinámica con External Secrets Operator, ayudándote a resolver completamente la seguridad de secretos en flujos de trabajo GitOps.
Puntos Clave
- Comprender 3 patrones arquitectónicos para la gestión de secretos GitOps: almacenamiento cifrado, inyección externa e híbrido
- Dominar el despliegue y configuración completa de Sealed Secrets, External Secrets Operator y SOPS
- Implementar rotación automática de secretos y sincronización multi-clúster
- Evitar los 5 errores más comunes en producción y 10 errores frecuentes
- Obtener una matriz de decisión comparativa para la selección de herramientas
Tabla de Contenidos
- Conceptos Fundamentales de Gestión de Secretos GitOps
- Patrón 1: Almacenamiento Cifrado con Sealed Secrets
- Patrón 2: External Secrets Operator + Vault
- Patrón 3: Cifrado SOPS + Age/GPG
- Patrón 4: Rotación Automática de Secretos
- Patrón 5: Sincronización de Secretos Multi-Clúster
- Patrón 6: Auditoría y Cumplimiento
- 5 Errores Comunes y Soluciones
- 10 Soluciones a Errores Comunes
- Consejos Avanzados de Optimización
- Análisis Comparativo
- Herramientas en Línea Recomendadas
Conceptos Fundamentales de Gestión de Secretos GitOps
Tres Patrones Arquitectónicos
┌─────────────────────────────────────────────────────────────┐
│ Gestión de Secretos GitOps: 3 Patrones │
├─────────────────┬──────────────────┬────────────────────────┤
│ Almacenamiento │ Inyección │ Híbrido │
│ Cifrado │ Externa │ (SOPS + ESO) │
│ (Sealed Secrets)│ (ESO + Vault) │ │
├─────────────────┼──────────────────┼────────────────────────┤
│ Git almacena │ Git almacena │ Git almacena valores │
│ texto cifrado │ solo referencias│ cifrados + referencias│
│ Descifrado │ El clúster │ Descifrado en clúster │
│ en el clúster │ obtiene texto │ o obtención │
│ Auditoría │ Deps externas │ Combinación flexible │
│ offline │ Soporte nativo │ Soporte parcial de │
│ Re-cifrar para │ de rotación │ rotación │
│ rotar │ │ │
└─────────────────┴──────────────────┴────────────────────────┘
El Problema Fundamental con los Secrets de K8s
# Crear un Secret estándar
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='SuperSecret123!'
# El "cifrado" es solo codificación Base64 — cualquiera puede decodificarlo
kubectl get secret db-credentials -o yaml
# Decodificar toma un solo comando
kubectl get secret db-credentials \
-o jsonpath='{.data.password}' | base64 -d
# Salida: SuperSecret123!
# Verificar con el codificador/decodificador Base64 de ToolsKu
# https://toolsku.com/es-419/encode/base64
Principios Fundamentales de la Gestión de Secretos DevOps GitOps
| Principio | Descripción | Consecuencia de su Violación |
|---|---|---|
| Cero Texto Plano | Nunca almacenar secretos en texto plano en Git | Fuga del repositorio = compromiso total |
| Mínimo Privilegio | Cada aplicación accede solo a sus propios secretos | Fuga lateral de secretos |
| Rotación Automática | Los secretos se actualizan automáticamente periódicamente | Claves de larga duración forzadas por fuerza bruta |
| Auditable | Cada acceso a secretos queda registrado | No se puede rastrear el origen de la fuga |
| Recuperable | Los secretos pueden restaurarse rápidamente tras su pérdida | Interrupción del negocio |
Patrón 1: Almacenamiento Cifrado con Sealed Secrets
Sealed Secrets es una solución de gestión de secretos GitOps de código abierto de Bitnami. La idea central: cifrar Secrets localmente con kubeseal, generar recursos SealedSecret que se confirman en Git, y el Sealed Secrets Controller en el clúster los descifra automáticamente de vuelta a Secrets de K8s.
Arquitectura
┌──────────────────────────────────────────────────────┐
│ Estación de Trabajo del Desarrollador │
│ ┌────────────┐ ┌────────────┐ │
│ │ secret.yaml │───▶│ kubeseal │ │
│ │ (texto plano,│ │ (herramienta│ │
│ │ no confirmar)│ │ de cifrado)│ │
│ └────────────┘ └─────┬──────┘ │
│ │ cifrar │
│ ┌─────▼──────┐ │
│ │sealed-secret│ │
│ │.yaml(cifrado)│ │
│ └─────┬──────┘ │
└──────────────────────────┼───────────────────────────┘
│ git push
┌──────────────────────────▼───────────────────────────┐
│ Repositorio Git │
│ (almacena solo SealedSecrets cifrados) │
└──────────────────────────┬───────────────────────────┘
│ git pull (ArgoCD/Flux)
┌──────────────────────────▼───────────────────────────┐
│ Clúster K8s │
│ ┌──────────────────────────────┐ │
│ │ Sealed Secrets Controller │ │
│ │ (descifra con clave privada) │ │
│ └──────────────┬───────────────┘ │
│ │ descifrar │
│ ┌─────▼──────┐ │
│ │ K8s Secret │ │
│ │ (texto plano,│ │
│ │ en el clúster)│ │
│ └────────────┘ │
└───────────────────────────────────────────────────────┘
Instalando Sealed Secrets
# Instalar Controller en el clúster
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.0/controller.yaml
# Esperar a que el Controller esté listo
kubectl wait --for=condition=available --timeout=120s \
deployment/sealed-secrets-controller -n kube-system
# Instalar CLI kubeseal
KUBESEAL_VERSION=0.27.0
curl -sLO "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-linux-amd64"
chmod +x kubeseal-linux-amd64
sudo mv kubeseal-linux-amd64 /usr/local/bin/kubeseal
# Verificar instalación
kubeseal --version
Cifrando Secretos
# Cifrar desde archivo Secret en texto plano
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='SuperSecret123!' \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-secret.yaml
# Crear desde archivos
kubectl create secret generic tls-cert \
--from-file=tls.crt=server.crt \
--from-file=tls.key=server.key \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-tls.yaml
# Especificar namespace y nombre (modo strict)
kubectl create secret generic api-key \
--from-literal=key=abc123xyz \
--namespace production \
--dry-run=client -o yaml | \
kubeseal --format yaml \
--scope namespace-wide > sealed-api-key.yaml
Ejemplo de Recurso SealedSecret
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: production
spec:
encryptedData:
username: AgBfj8k2mN3pQ7sT9vWxYz...
password: AgCdH5lM6nO8qR0tU2wXyZa...
template:
metadata:
name: db-credentials
namespace: production
type: Opaque
Control de Alcance de Cifrado
# strict: solo se puede descifrar con el mismo namespace y nombre (predeterminado)
kubeseal --scope strict
# namespace-wide: se puede renombrar dentro del mismo namespace
kubeseal --scope namespace-wide
# cluster-wide: utilizable en cualquier namespace del clúster
kubeseal --scope cluster-wide
| Alcance | Nivel de Seguridad | Flexibilidad | Caso de Uso |
|---|---|---|---|
| strict | Más alto | Más baja | Secretos de producción |
| namespace-wide | Medio | Medio | Multi-aplicación en el mismo namespace |
| cluster-wide | Más bajo | Más alta | Recursos compartidos como certificados |
Respaldo y Restauración de la Clave Maestra
# Respaldar clave maestra (¡todos los SealedSecrets se vuelven indescifrables si se pierde!)
kubectl get secret -n kube-system \
sealed-secrets-key -o yaml > sealed-secrets-key-backup.yaml
# Restaurar clave maestra a un nuevo clúster
kubectl apply -f sealed-secrets-key-backup.yaml
# Rotar clave maestra
kubectl delete secret -n kube-system sealed-secrets-key
# El Controller genera automáticamente una nueva clave, los SealedSecrets antiguos siguen funcionando
Patrón 2: External Secrets Operator + Vault
External Secrets Operator (ESO) integra sistemas externos de gestión de secretos (Vault, AWS Secrets Manager, GCP Secret Manager, etc.) con Kubernetes. Los repositorios Git solo almacenan referencias SecretStore, y el ESO Controller en el clúster obtiene los secretos de sistemas externos y crea Secrets de K8s.
Arquitectura
┌──────────────────────────────────────────────────────┐
│ Repositorio Git │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ SecretStore │ │ ExternalSecret│ │
│ │ (config de │ │ (mapeo de │ │
│ │ conn Vault) │ │ ref secret) │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────┬───────────────────────────────┘
│ git pull
┌──────────────────────▼───────────────────────────────┐
│ Clúster K8s │
│ ┌──────────────────────────────┐ │
│ │ External Secrets Operator │ │
│ │ (monitorea ExternalSecrets) │ │
│ └──────────────┬───────────────┘ │
│ │ obtener secretos │
│ ┌────────────▼────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ K8s │ │ HashiCorp│ │
│ │ Secret │ │ Vault │ │
│ └──────────┘ └──────────┘ │
└───────────────────────────────────────────────────────┘
Instalando ESO
# Instalar con Helm
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets \
external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace \
--set installCRDs=true \
--set replicaCount=2 \
--set leaderElect=true
# Verificar
kubectl get pods -n external-secrets
Configurando HashiCorp Vault SecretStore
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "eso-role"
serviceAccountRef:
name: "external-secrets-sa"
Configuración de Política de Vault
# Política de Vault: restringir ESO a rutas específicas
path "secret/data/production/*" {
capabilities = ["read"]
}
path "secret/data/production/database/*" {
capabilities = ["read", "list"]
}
# Denegar acceso a otros entornos
path "secret/data/staging/*" {
capabilities = ["deny"]
}
path "secret/data/development/*" {
capabilities = ["deny"]
}
Recurso ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 15m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials
creationPolicy: Owner
template:
type: Opaque
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@db.internal:5432/mydb"
data:
- secretKey: username
remoteRef:
key: secret/data/production/database
property: username
- secretKey: password
remoteRef:
key: secret/data/production/database
property: password
Multi-Fuente: ClusterSecretStore
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-aws-sa
namespace: external-secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: aws-api-keys
namespace: production
spec:
refreshInterval: 5m
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: aws-api-keys
dataFrom:
- extract:
key: production/api-keys
Patrón 3: Cifrado SOPS + Age/GPG
SOPS (Secrets OPerationS) es una herramienta de cifrado de secretos desarrollada por Mozilla, que soporta múltiples backends de cifrado incluyendo AES, PGP y Age. A diferencia de Sealed Secrets, SOPS cifra el archivo en sí — puede cifrar formatos YAML/JSON/ENV y solo cifra los valores, no las claves. Esto significa que puedes usar Git diff para ver cambios estructurales en los secretos.
Arquitectura
┌──────────────────────────────────────────────────────┐
│ Estación de Trabajo del Desarrollador │
│ ┌────────────┐ ┌────────────┐ │
│ │ secret.yaml │───▶│ SOPS │ │
│ │ (texto plano,│ │ + Age/GPG │ │
│ │ no confirmar)│ │ │ │
│ └────────────┘ └─────┬──────┘ │
│ │ cifrar │
│ ┌─────▼──────┐ │
│ │secret.enc. │ │
│ │yaml(cifrado)│ │
│ └─────┬──────┘ │
└──────────────────────────┼───────────────────────────┘
│ git push
┌──────────────────────────▼───────────────────────────┐
│ Repositorio Git │
│ (almacena archivos cifrados, claves visibles, │
│ valores cifrados) │
└──────────────────────────┬───────────────────────────┘
│ git pull
┌──────────────────────────▼───────────────────────────┐
│ Pipeline CI/CD │
│ ┌──────────────────────────────┐ │
│ │ sops --decrypt + kubectl apply│ │
│ │ o integración Flux │ │
│ │ Kustomization SOPS │ │
│ └──────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
Instalando SOPS y Age
# Instalar SOPS
curl -sLO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
chmod +x sops-v3.9.0.linux.amd64
sudo mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops
# Instalar Age (backend de cifrado recomendado, más simple que GPG)
curl -sLO 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 /usr/local/bin/
sudo mv age/age-keygen /usr/local/bin/
# Generar par de claves Age
age-keygen -o age.key
# Clave pública: age1abc123xyz...
# Verificar
sops --version
age --version
Cifrando Archivos de Secretos
# secret.yaml (antes del cifrado)
apiVersion: v1
kind: Secret
metadata:
name: app-config
namespace: production
type: Opaque
stringData:
DB_HOST: "db.internal.example.com"
DB_PASSWORD: "SuperSecret123!"
API_KEY: "sk-proj-abc123xyz456"
REDIS_URL: "redis://redis.internal:6379"
# Cifrar con clave pública Age
sops --encrypt \
--age age1abc123xyz456... \
--encrypted-regex '^(DB_PASSWORD|API_KEY)$' \
--in-place secret.yaml
# secret.yaml (después del cifrado)
apiVersion: v1
kind: Secret
metadata:
name: app-config
namespace: production
type: Opaque
stringData:
DB_HOST: "db.internal.example.com"
DB_PASSWORD: ENC[AES256_GCM,data:Wk5kPQ==,tag:abc123==,type:str]
API_KEY: ENC[AES256_GCM,data:Zm9vYmFy,tag:def456==,type:str]
REDIS_URL: "redis://redis.internal:6379"
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1abc123xyz456...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-06-16T10:00:00Z"
mac: ENC[AES256_GCM,data:abc==,tag:xyz==,type:str]
pgp: []
encrypted_regex: ^(DB_PASSWORD|API_KEY)$
version: 3.9.0
Integración SOPS con Flux CD
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app-secrets
namespace: flux-system
spec:
interval: 10m
path: ./clusters/production
prune: true
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-age-key
# Crear Secret con clave privada Age para descifrado de Flux
kubectl create secret generic sops-age-key \
--namespace flux-system \
--from-file=age.agekey=age.key \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-sops-key.yaml
Cifrado Multi-Clave (Colaboración en Equipo)
# Archivo de configuración .sops.yaml — configuración de cifrado a nivel de proyecto
cat > .sops.yaml << 'EOF'
creation_rules:
- path_regex: ^clusters/production/.*\.yaml$
key_groups:
- age:
- age1abc123xyz456 # clave de producción
- age1def789uvw012 # clave del equipo SRE
- path_regex: ^clusters/staging/.*\.yaml$
key_groups:
- age:
- age1ghi345rst678 # clave de staging
- path_regex: ^clusters/.*\.yaml$
key_groups:
- age:
- age1abc123xyz456 # clave predeterminada
EOF
Patrón 4: Rotación Automática de Secretos
La rotación de secretos es el aspecto más descuidado de la gestión de secretos DevOps GitOps. Los secretos estáticos que nunca cambian crean una ventana de ataque infinita una vez filtrados. La mejor práctica de 2026: rotar todos los secretos de producción cada 90 días, y los secretos de alta sensibilidad cada 7 días.
Arquitectura de Rotación
┌──────────────────────────────────────────────────────┐
│ Arquitectura de Auto-Rotación de Secretos │
│ │
│ ┌──────────┐ disparador ┌──────────────┐ │
│ │ CronJob │─────────────▶│ Vault Rotate │ │
│ │ (programa)│ │ (generar nuevo)│ │
│ └──────────┘ └──────┬───────┘ │
│ │ nuevo secreto │
│ ┌──────▼───────┐ │
│ │ ExternalSecret│ │
│ │ (auto refresh)│ │
│ └──────┬───────┘ │
│ │ actualizar │
│ ┌──────▼───────┐ │
│ │ K8s Secret │ │
│ │ (auto actualizar)│ │
│ └──────┬───────┘ │
│ │ reinicio rolling │
│ ┌──────▼───────┐ │
│ │ Pods │ │
│ │ (leer nueva │ │
│ │ clave) │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────┘
Configuración de Auto-Rotación ESO
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rotating-api-key
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: api-key
creationPolicy: Owner
template:
type: Opaque
metadata:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
data:
- secretKey: api-key
remoteRef:
key: secret/data/production/api
property: key
Secretos Dinámicos de Vault
# Configuración de credenciales dinámicas de base de datos Vault
resource "vault_database_secret_backend_connection" "postgresql" {
backend = "database"
name = "postgresql-production"
allowed_roles = ["app-readonly", "app-readwrite"]
postgresql {
connection_url = "postgresql://{{username}}:{{password}}@db.internal:5432/mydb?sslmode=require"
username = "vault_admin"
password = "VaultAdminPassword123!"
}
}
resource "vault_database_secret_backend_role" "app_readwrite" {
backend = "database"
name = "app-readwrite"
db_name = vault_database_secret_backend_connection.postgresql.name
default_ttl = 3600
max_ttl = 86400
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
]
}
Detección Automática de Cambios de Secretos en Aplicaciones
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
annotations:
secret.reloader.stakater.com/reload: "api-key,db-credentials"
spec:
containers:
- name: api-server
image: registry.example.com/api-server:v2.1.0
envFrom:
- secretRef:
name: api-key
- secretRef:
name: db-credentials
# Instalar Reloader — reinicio rolling automático de Pods al cambiar Secrets
helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update
helm install reloader stakater/reloader \
--namespace reloader \
--create-namespace \
--set reloader.watchGlobally=false
Patrón 5: Sincronización de Secretos Multi-Clúster
En escenarios multi-clúster, los secretos necesitan sincronizarse de forma segura entre clústeres. La gestión de secretos DevOps GitOps requiere: cada clúster tiene su propio ciclo de vida de secretos mientras mantiene la consistencia.
Arquitectura Multi-Clúster
┌──────────────────────────────────────────────────────┐
│ Arquitectura de Sincronización de Secretos │
│ Multi-Clúster │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ HashiCorp Vault (Central) │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │prod-east/│ │prod-west/│ │ │
│ │ │ secrets │ │ secrets │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └──────────┬───────────────┬──────────┘ │
│ │ │ │
│ ┌────────▼──────┐ ┌──────▼────────┐ │
│ │ Clúster East │ │ Clúster West │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ ESO │ │ │ │ ESO │ │ │
│ │ │Controller│ │ │ │Controller│ │ │
│ │ └────┬─────┘ │ │ └────┬─────┘ │ │
│ │ ▼ │ │ ▼ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ Secrets │ │ │ │ Secrets │ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │
│ └───────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────┘
SecretStore Independiente por Clúster
# Clúster East
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-east
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes/east"
role: "eso-east-role"
serviceAccountRef:
name: "external-secrets-sa"
# Clúster West
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-west
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes/west"
role: "eso-west-role"
serviceAccountRef:
name: "external-secrets-sa"
Distribución Multi-Clúster de Certificados TLS
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: wildcard-tls
namespace: ingress-nginx
spec:
refreshInterval: 24h
secretStoreRef:
name: vault-east
kind: SecretStore
target:
name: wildcard-tls
creationPolicy: Owner
template:
type: kubernetes.io/tls
data:
- secretKey: tls.crt
remoteRef:
key: secret/data/shared/tls/wildcard
property: cert
- secretKey: tls.key
remoteRef:
key: secret/data/shared/tls/wildcard
property: key
Sincronización de Clave Maestra Sealed Secrets Multi-Clúster
# Exportar clave maestra
kubectl get secret -n kube-system sealed-secrets-key \
-o yaml > sealed-secrets-master-key.yaml
# Importar al clúster destino
kubectl apply -f sealed-secrets-master-key.yaml -n kube-system
# Reiniciar Controller para cargar la clave
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system
Patrón 6: Auditoría y Cumplimiento
La gestión de secretos DevOps GitOps debe satisfacer los requisitos de auditoría: quién accedió a qué secreto y cuándo. Los estándares de cumplimiento de 2026 (SOC2, ISO 27001, MLPS 2.0) requieren acceso rastreable a secretos.
Registros de Auditoría de Vault
# Habilitar registro de auditoría de Vault
audit {
type = "file"
options = {
file_path = "/vault/audit/audit.log"
mode = "0600"
}
}
# Habilitar auditoría Syslog
audit {
type = "syslog"
options = {
facility = "AUTH"
tag = "vault"
address = "syslog.internal:514"
}
}
Registro de Acceso ESO
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: audited-vault
annotations:
audit.external-secrets.io/enabled: "true"
audit.external-secrets.io/log-access: "true"
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "eso-audited-role"
serviceAccountRef:
name: "external-secrets-sa"
Política de Auditoría de Kubernetes
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
namespaces: ["production", "staging"]
omitStages:
- RequestReceived
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
namespaces: ["default"]
omitStages:
- RequestReceived
- level: RequestResponse
resources:
- group: "external-secrets.io"
resources: ["externalsecrets", "secretstores"]
omitStages:
- RequestReceived
Monitoreo de Acceso a Secretos
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-secret-access
spec:
validationFailureAction: Audit
background: true
rules:
- name: block-default-sa-secret-access
match:
resources:
kinds: [Pod]
validate:
message: "Acceder a secrets con el ServiceAccount predeterminado está prohibido"
pattern:
spec:
serviceAccountName: "!default"
- name: require-secret-annotations
match:
resources:
kinds: [Secret]
names: ["db-*", "api-*", "tls-*"]
validate:
message: "Los secrets de producción deben tener anotaciones de propietario y vencimiento"
pattern:
metadata:
annotations:
owner: "?*"
expiry: "?*"
5 Errores Comunes y Soluciones
Error 1: Pérdida de la Clave Maestra de Sealed Secrets
Síntoma: Después de reconstruir el clúster, todos los SealedSecrets no pueden descifrarse. El Controller reporta failed to unseal.
Causa Raíz: Sealed Secrets usa cifrado asimétrico. La clave privada solo existe en el clúster. Destruir el clúster pierde la clave privada.
Solución:
# 1. Respaldo regular de la clave maestra (automatizado)
kubectl get secret -n kube-system \
sealed-secrets-key -o yaml | \
sops --encrypt --age age1abc123xyz456... \
/dev/stdin > sealed-secrets-key.enc.yaml
# 2. Almacenar clave cifrada en otro repositorio Git
git add sealed-secrets-key.enc.yaml
git commit -m "backup: sealed secrets master key $(date +%Y%m%d)"
# 3. Proceso de recuperación
sops --decrypt sealed-secrets-key.enc.yaml | \
kubectl apply -f -
Error 2: Retraso en la Actualización de ExternalSecret Causa Fallo en el Inicio de Pods
Síntoma: Los Pods recién desplegados fallan al iniciar porque el Secret aún no existe — ESO aún no lo ha obtenido de Vault.
Solución:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 5m
target:
creationPolicy: Owner
template:
type: Opaque
dataFrom:
- extract:
key: production/app-secrets
# Asegurar que ESO ha sincronizado antes de desplegar la aplicación
# Método 1: Usar ArgoCD Sync Hooks
# Método 2: Esperar a que el Secret exista en CI
kubectl wait --for=condition=Ready \
externalsecret/app-secrets -n production --timeout=120s
Error 3: La Rotación de Claves SOPS Rompe el Descifrado de Textos Cifrados Antiguos
Síntoma: Después de que el equipo rota las claves Age, los archivos cifrados antiguos no pueden descifrarse.
Solución:
# Usar cifrado multi-clave — coexistencia de claves antiguas y nuevas
sops --encrypt \
--age age1NEWKEY...,age1OLDKEY... \
--in-place secret.yaml
# O gestionar grupos de claves vía .sops.yaml
# Después de actualizar la clave, descifrar con clave antigua y luego re-cifrar con clave nueva
sops --decrypt --age age1OLDKEY... secret.yaml | \
sops --encrypt --age age1NEWKEY... \
--filename-override secret.yaml /dev/stdin > secret_new.yaml
Error 4: No se Pueden Revocar Rápidamente Secretos Filtrados
Síntoma: Se filtra una API Key, pero SealedSecret requiere re-cifrado y commit — la ventana de revocación es demasiado larga.
Solución:
# Enfoque ESO: deshabilitar directamente la clave antigua en Vault
vault kv metadata put -delete-version-after=0s \
secret/data/production/api-key
# Enfoque Sealed Secrets: eliminación de emergencia vía Controller
kubectl delete secret api-key -n production
# Re-cifrar inmediatamente y confirmar nuevo SealedSecret
# Enfoque genérico: bloqueo a nivel de red
kubectl apply -f - <<EOF
apiVersion: networkpolicies.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-compromised
namespace: production
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- compromised-api.example.com/32
EOF
Error 5: Secretos Compartidos Entre Equipos Llevan al Caos de Control de Acceso
Síntoma: Múltiples equipos usan el mismo Token de Vault para acceder a secretos, haciendo imposible distinguir quién hizo qué.
Solución:
# Crear políticas de Vault por equipo
path "secret/data/team-a/*" {
capabilities = ["read", "list"]
}
path "secret/data/team-b/*" {
capabilities = ["read", "list"]
}
# Denegación explícita para acceso cruzado
path "secret/data/team-a/*" {
capabilities = ["deny"]
}
# Vincular al rol team-b
# Usar namespaces de Vault para aislamiento
namespace "team-a" {
path "secret/*" {
capabilities = ["read", "list", "create", "update"]
}
}
10 Soluciones a Errores Comunes
1. failed to unseal: no private key found
# Verificar si el Controller tiene la clave maestra
kubectl get secrets -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key
# Restaurar clave maestra
kubectl apply -f sealed-secrets-key-backup.yaml -n kube-system
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system
2. ExternalSecret "not ready": could not get secret data
# Verificar conexión SecretStore
kubectl describe secretstore vault-backend -n production
# Verificar autenticación Vault
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets
# Verificar permisos del ServiceAccount
kubectl auth can-i get secret -n production \
--as=system:serviceaccount:production:external-secrets-sa
3. sops error decrypting: could not find a matching key
# Verificar si la clave Age es correcta
age-keygen -y age.key
# Verificar configuración de claves en .sops.yaml
cat .sops.yaml
# Intentar especificar clave para descifrado
sops --decrypt --age age1YOURKEY... secret.yaml
4. SealedSecret "sealed-secrets" is invalid: metadata.name
# Verificar namespace y nombre especificados durante el cifrado
# El modo strict requiere coincidencia exacta
kubeseal --scope strict --namespace production \
--name db-credentials < secret.yaml > sealed.yaml
5. Vault seal status: sealed
# Verificar estado de Vault
vault status
# Desellar Vault (requiere 2 de 3 claves de unseal)
vault operator unseal <key1>
vault operator unseal <key2>
# Configuración de auto-unseal (recomendado para producción)
# Usar AWS KMS / GCP KMS / Azure Key Vault para auto-unseal
6. refreshInterval: cannot unmarshal
# Asegurar que el formato de refreshInterval es correcto
# Correcto: "15m", "1h", "24h"
# Incorrecto: 15, "15", "15minutes"
spec:
refreshInterval: 15m
7. kubeseal: error: failed to get certificate
# Verificar si el Controller está ejecutándose
kubectl get deployment sealed-secrets-controller -n kube-system
# Verificar Secret del certificado
kubectl get secret -n kube-system sealed-secrets-key
# Obtener clave pública del Controller
kubeseal --fetch-cert > sealed-secrets-cert.pem
kubeseal --cert sealed-secrets-cert.pem < secret.yaml
8. ExternalSecret status: SecretSyncedError
# Ver error detallado
kubectl describe externalsecret db-credentials -n production
# Causa común: la ruta de Vault no existe
# Verificar rutas en Vault
vault kv list secret/production/
# Causa común: permisos insuficientes
vault policy read eso-role
9. SOPS: mac mismatch: file has been modified
# El archivo fue modificado manualmente después del cifrado, la verificación MAC falló
# Enfoque seguro: re-cifrar
sops --decrypt secret.yaml > secret_plain.yaml
# Modificar y luego re-cifrar
sops --encrypt --age age1YOURKEY... secret_plain.yaml > secret.yaml
10. Kubernetes: secret "xxx" not found
# ESO aún no ha terminado de sincronizar
kubectl get externalsecret -n production
# Verificar logs del Controller ESO
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets \
--tail=100
# Activar sincronización manual
kubectl annotate externalsecret db-credentials \
force-sync=$(date +%s) -n production \
--overwrite
Consejos Avanzados de Optimización
1. Hook Pre-commit de Git para Prevenir Commits de Secretos en Texto Plano
#!/bin/bash
# .git/hooks/pre-commit
# Escanear archivos preparados en busca de secretos sospechosos
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.yaml$|\.yml$|\.env$')
for FILE in $STAGED_FILES; do
# Detectar Secret de K8s codificado en Base64
if grep -qE 'kind:\s*Secret' "$FILE" 2>/dev/null; then
if ! grep -qE 'kind:\s*SealedSecret|encryptedData|sops:' "$FILE" 2>/dev/null; then
echo "ERROR: Archivo Secret sin cifrar encontrado: $FILE"
echo "Por favor cifrar con kubeseal o sops antes de confirmar"
exit 1
fi
fi
# Detectar patrones de secretos comunes
if grep -qiE '(password|secret|api.key|token)\s*[:=]\s*["\x27]?[A-Za-z0-9+/=]{16,}' "$FILE" 2>/dev/null; then
if ! grep -qE 'ENC\[|sops:|encryptedData' "$FILE" 2>/dev/null; then
echo "ADVERTENCIA: Secreto en texto plano sospechoso en: $FILE"
echo "Por favor confirmar cifrado o usar gestión de secretos externa"
fi
fi
done
exit 0
2. Integración de Gestión de Secretos con ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-with-secrets
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-manifests.git
targetRevision: main
path: overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
3. Motor de Plantillas de Secretos
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-config
namespace: production
spec:
refreshInterval: 15m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: app-config
template:
type: Opaque
engineVersion: v2
data:
DATABASE_URL: "postgresql://{{ .db_user }}:{{ .db_pass }}@{{ .db_host }}:5432/{{ .db_name }}?sslmode=require"
REDIS_URL: "redis://:{{ .redis_pass }}@{{ .redis_host }}:6379/0"
JWT_SECRET: "{{ .jwt_secret }}"
CONFIG_JSON: |
{
"database": {
"host": "{{ .db_host }}",
"port": 5432,
"name": "{{ .db_name }}"
},
"redis": {
"host": "{{ .redis_host }}"
}
}
data:
- secretKey: db_user
remoteRef:
key: secret/data/production/database
property: username
- secretKey: db_pass
remoteRef:
key: secret/data/production/database
property: password
- secretKey: db_host
remoteRef:
key: secret/data/production/database
property: host
- secretKey: db_name
remoteRef:
key: secret/data/production/database
property: dbname
- secretKey: redis_pass
remoteRef:
key: secret/data/production/redis
property: password
- secretKey: redis_host
remoteRef:
key: secret/data/production/redis
property: host
- secretKey: jwt_secret
remoteRef:
key: secret/data/production/auth
property: jwt_secret
4. Verificación de Salud de Secretos
apiVersion: batch/v1
kind: CronJob
metadata:
name: secret-health-check
namespace: security
spec:
schedule: "0 8 * * 1"
jobTemplate:
spec:
template:
spec:
serviceAccountName: secret-checker
containers:
- name: checker
image: bitnami/kubectl:1.30
command:
- /bin/bash
- -c
- |
echo "=== Verificación de Salud de Secretos $(date) ==="
echo ""
echo "--- Verificando certificados TLS por vencer ---"
kubectl get secrets --all-namespaces \
-o json | jq -r '.items[] |
select(.type=="kubernetes.io/tls") |
"\(.metadata.namespace)/\(.metadata.name)"' | \
while read secret; do
ns=$(echo $secret | cut -d/ -f1)
name=$(echo $secret | cut -d/ -f2)
cert=$(kubectl get secret $name -n $ns \
-o jsonpath='{.data.tls\.crt}' | base64 -d)
expiry=$(echo "$cert" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -n "$expiry" ]; then
days=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
if [ $days -lt 30 ]; then
echo "ADVERTENCIA: el certificado $secret vence en ${days} días"
fi
fi
done
echo ""
echo "--- Verificando estado de sincronización de ExternalSecret ---"
kubectl get externalsecrets --all-namespaces \
-o json | jq -r '.items[] |
select(.status.conditions[]?.type=="Ready" and
.status.conditions[]?.status!="True") |
"\(.metadata.namespace)/\(.metadata.name): NO LISTO"'
restartPolicy: OnFailure
Comparación: Sealed Secrets vs External Secrets vs SOPS
| Dimensión | Sealed Secrets | External Secrets Operator | SOPS |
|---|---|---|---|
| Cifrado | Asimétrico (clave pública del clúster) | Sin cifrado de contenido en Git | Simétrico/Asimétrico |
| Contenido en Git | Texto cifrado (SealedSecret) | Referencias (ExternalSecret) | Texto cifrado (valores cifrados) |
| Deps Externas | Ninguna (autocontenido) | Vault/AWS/GCP etc. | Ninguna (autocontenido) |
| Rotación de Secretos | Re-cifrado requerido | Soporte nativo | Re-cifrado requerido |
| Multi-Clúster | Sincronizar clave maestra | SecretStore por clúster | Compartir clave de cifrado |
| Compatible con Git Diff | No (texto cifrado ilegible) | Sí (referencias legibles) | Sí (claves legibles, valores cifrados) |
| Descifrado Offline | No (necesita clave privada del clúster) | No (necesita servicio externo) | Sí (clave local suficiente) |
| Curva de Aprendizaje | Baja | Media | Media |
| Complejidad Operativa | Baja | Alta (necesita Vault) | Baja |
| Capacidad de Auditoría | Historial de Git | Registros de auditoría de Vault | Historial de Git |
| Secretos Dinámicos | No soportado | Soportado (credenciales dinámicas de Vault) | No soportado |
| Escala Adecuada | Equipos pequeños-medianos | Empresas medianas-grandes | Cualquier escala |
| Mejor Para | GitOps simple | Gestión de secretos empresarial | Cifrado multi-formato |
Árbol de Decisión de Selección
¿Ya tienes Vault/AWS Secrets Manager?
├── Sí → External Secrets Operator
│ └── ¿Necesitas secretos dinámicos? → ESO + credenciales dinámicas de Vault
└── No → ¿Necesitas almacenar texto cifrado en Git?
├── Sí → ¿Necesitas compatibilidad con Git Diff?
│ ├── Sí → SOPS + Age
│ └── No → Sealed Secrets
└── No → ¿Necesitas secretos dinámicos?
├── Sí → Desplegar Vault + ESO
└── No → Sealed Secrets
Herramientas en Línea Recomendadas
- Codificador/Decodificador Base64: /es-419/encode/base64 — Codificar/decodificar datos de K8s Secret y SealedSecret
- Generador de Claves RSA: /es-419/encode/rsa — Generar pares de claves RSA para cifrado GPG de SOPS
- Calculadora de Hash: /es-419/encode/hash — Calcular huellas digitales de secretos y sumas de verificación
Resumen: No hay una bala de plata para la gestión de secretos DevOps GitOps, pero existe un camino de mejores prácticas. Los equipos pequeños comienzan con Sealed Secrets — cero dependencias externas para seguridad de secretos GitOps. Las empresas medianas y grandes eligen External Secrets Operator + Vault, con rotación nativa de secretos y credenciales dinámicas. Cuando necesitas cifrado multi-formato y compatibilidad con Git Diff, SOPS + Age es la mejor opción. Los principios fundamentales de 2026: cero almacenamiento en texto plano, rotación automática, auditable y rastreable. Recuerda — las filtraciones de secretos no son cuestión de "si" sino de "cuándo."
Artículos Relacionados:
- GitOps con ArgoCD en Producción — Guía completa de automatización de despliegue con ArgoCD
- GitOps con Flux CD en Producción — Entrega continua con Flux CD en la práctica
- Endurecimiento de Seguridad de Contenedores Docker — Sistema de defensa de contenedores de 8 capas
Referencias Externas:
Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →