Gestión de Secretos DevOps GitOps: 6 Patrones de Producción desde Sealed Secrets hasta External Secrets Operator

DevOps

¿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:

Referencias Externas:

Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →

#DevOps#GitOps#密钥管理#Sealed Secrets#External Secrets#SOPS#2026#DevOps