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 kubectl configurado
  • sops v3.8+ instalado
  • age v1.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ísticaSOPS + AgeSealed SecretsHashiCorp VaultExternal Secrets
Ubicación del cifradoLado clienteLado controladorLado servidorLado servidor
Compatible con GitSí (YAML cifrado)Sí (recurso personalizado)No (solo referencias)No (solo referencias)
Infraestructura necesariaNingunaControlador en clústerServidor VaultOperador + backend
Gestión de clavesArchivos de clave AgeCertificado del clústerPolíticas de VaultVaría según backend
Multi-entornoReglas .sops.yamlCertificados por clústerNamespaces/políticasMúltiples stores
Integración GitOpsPlugins ArgoCD/FluxK8s nativoDriver CSI/injectorSincronización operador
RotaciónRe-cifrar con SOPSRe-sellar requeridoSecretos dinámicosDepende del backend
ComplejidadBajaBajaAltaMedia
Funciona offlineNoNoNo
Ideal paraEquipos pequeños-medianosClústeres individualesEmpresa/cumplimientoMulti-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.yaml define 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

Artículos Relacionados