Introducción

En entornos Kubernetes modernos, la automatización de infraestructura es moneda corriente, pero la gestión de secretos sigue siendo un dolor de cabeza recurrente. Cuando las organizaciones escalan sus operaciones —ya sea con múltiples cuentas de AWS/Azure, clústeres separados por entorno (dev/staging/prod) o namespaces aislados— la pregunta es la misma: ¿cómo distribuir y rotar credenciales compartidas de forma consistente sin caer en el «copy-paste» manual?

El problema no es exclusivo de un proveedor cloud. Ya sea en EKS, AKS, GKE, en entornos on-premise o incluso en desarrollo local con KIND/Minikube, la complejidad persiste: cómo replicar secretos entre entornos aislados sin exponer credenciales en repositorios de código o configuraciones estáticas.

Este artículo detalla cómo resolver este desafío usando External Secrets Operator (ESO) —un proyecto CNCF— combinado con Bitwarden Secrets Manager como backend centralizado. El patrón aplica a cualquier proveedor soportado (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, etc.), pero usamos Bitwarden por alinearse con prácticas existentes en el cliente. Al finalizar, tendrás un clúster listo para sincronizar secretos de forma automática, segura y auditable.

Qué es y para qué sirve

External Secrets Operator (ESO)

ESO es un operador de Kubernetes que sincroniza secretos desde backends externos directamente al API de Kubernetes Secrets. Su valor clave es:

  • Desacoplar el almacenamiento de secretos del consumo: las aplicaciones consumen Secrets estándar de Kubernetes, mientras la fuente de verdad permanece externa (ej: Bitwarden, Vault, etc.).
  • Soporte multi-proveedor: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, Bitwarden Secrets Manager, entre otros.
  • Reconciliación declarativa: define qué secretos sincronizar (mediante ExternalSecret) y ESO se encarga de mantenerlos actualizados.
  • Alcance global o namespace: usa ClusterSecretStore para secretos compartidos entre namespaces o SecretStore para ámbito limitado.
Ejemplo práctico:

Si rotas una API key en Bitwarden, ESO la reflejará en todos los clústeres asociados en ≤15 minutos, sin tocar manualmente cada clúster.

Bitwarden Secrets Manager (opcional pero recomendado)

  • Ventaja pragmática: si tu organización ya usa Bitwarden Password Manager, escalar a Secrets Manager es natural.
  • Control de acceso centralizado: los permisos se gestionan en Bitwarden (ej: Machine Account con acceso read-only a un proyecto específico).
  • Cifrado en reposo y tránsito: Bitwarden cifra secretos con AES-256 y usa TLS 1.2+.

Prerequisitos

ComponenteVersión mínimaNotas
Kubernetes1.25+Cluster con permisos para instalar operadores y CRDs.
BLOCK241.28+Para aplicar manifiestos.
BLOCK253.12+Para instalar ESO.
BitwardenOrganz. activaNecesitas un **Machine Account Access Token** con permisos *read-only*.
Cert-Manager1.13+Requerido para generar certificados TLS auto-firmados.
AWS (opcional)Si usas EKS, asegura IAM roles con permisos para crear LoadBalancers.
Accesos requeridos:
  1. Cluster de Kubernetes: acceso cluster-admin para instalar operadores y CRDs.
  2. Bitwarden: cuenta organization con acceso a Secrets Manager y permisos para crear Machine Accounts.
  3. DNS/Ingress: si usas TLS, necesitas un dominio y acceso a configurar registros DNS (para los certificados auto-firmados).

Guía paso a paso

Paso 1: Configurar TLS seguro entre ESO y Bitwarden SDK

ESO se comunica con Bitwarden SDK mediante HTTPS. Necesitas generar un certificado TLS auto-firmado para el endpoint local del SDK.

1.1. Instalar Cert-Manager

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.13.1 \
  --set installCRDs=true
Resultado esperado:
kubectl get pods -n cert-manager
# NAME                                      READY   STATUS    RESTARTS   AGE
# cert-manager-...                          1/1     Running   0          30s

1.2. Crear un ClusterIssuer auto-firmado

# 01-cluster-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}

Aplicar:

kubectl apply -f 01-cluster-issuer.yaml

1.3. Crear namespace y certificado raíz para el SDK de Bitwarden

# 02-ca-certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: bitwarden-ca
  namespace: external-secrets
spec:
  secretName: bitwarden-ca-tls
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
  commonName: "bitwarden-sdk-ca"
  isCA: true
  duration: 8760h # 1 año
  renewBefore: 720h # 30 días antes de expirar

Crear namespace y aplicar:

kubectl create namespace external-secrets
kubectl apply -f 02-ca-certificate.yaml
Verificar:
kubectl get secret bitwarden-ca-tls -n external-secrets -o jsonpath='{.data.ca\.crt}' | base64 -d | openssl x509 -text -noout | grep -A 2 "Validity"
# Debe mostrar fecha de validez de 1 año

1.4. Crear certificado TLS para el SDK de Bitwarden

# 03-bitwarden-tls.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: bitwarden-sdk-tls
  namespace: external-secrets
spec:
  secretName: bitwarden-sdk-tls
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
  commonName: "bitwarden-sdk.external-secrets.svc"
  dnsNames:
    - "bitwarden-sdk.external-secrets.svc.cluster.local"
    - "bitwarden-sdk.external-secrets.svc"
  duration: 8760h
  renewBefore: 720h
  usages:
    - server auth

Aplicar:

kubectl apply -f 03-bitwarden-tls.yaml

Paso 2: Instalar External Secrets Operator con soporte para Bitwarden

ESO requiere el Bitwarden SDK para comunicarse con la API de Bitwarden. Instálalo con Helm, habilitando el SDK.

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 \
  --version v0.9.6 \
  --set bitwarden.enabled=true \
  --set bitwarden.tls.enabled=true \
  --set bitwarden.tls.secretName=bitwarden-sdk-tls
Verificar instalación:
kubectl get pods -n external-secrets
# external-secrets-...        1/1     Running   0          30s
kubectl get crd | grep external-secrets
# clustersecretstores.external-secrets.io
# secretstores.external-secrets.io
# externalsecrets.external-secrets.io
Error común:

Si el pod de ESO no arranca, revisa logs:

kubectl logs -n external-secrets -l app.kubernetes.io/instance=external-secrets

Busca errores como «failed to create client» o «tls handshake failed» (indican problemas de certificado o token).

Paso 3: Autenticación con Bitwarden

3.1. Generar un Machine Account Access Token en Bitwarden

  1. Ve a Bitwarden Organizations → tu organización → Access ControlMachine Accounts.
  2. Crea un Machine Account con nombre k8s-eso-reader.
  3. Asigna el rol Secrets Manager Read-Only al proyecto que contiene tus secretos.
  4. Copia el Access Token generado (ej: bw_...).

3.2. Crear el Secret de Kubernetes con el token

# 04-bitwarden-token.yaml
apiVersion: v1
kind: Secret
metadata:
  name: bitwarden-access-token
  namespace: external-secrets
type: Opaque
stringData:
  token: "bw_ejemplo_token_abc123" # Reemplaza con tu token real

Aplicar:

kubectl apply -f 04-bitwarden-token.yaml
Principio de menor privilegio:
  • El token debe ser read-only y scoped al proyecto específico.
  • Si necesitas write (ej: para rotar secretos), usa el campo write en el ClusterSecretStore.

Paso 4: Configurar el ClusterSecretStore

El ClusterSecretStore define cómo ESO se conecta a Bitwarden. Usamos uno global para evitar repetir configuración en cada namespace.

# 05-cluster-secret-store.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: bitwarden-cluster-store
spec:
  provider:
    bitwarden:
      auth:
        secretRef:
          accessToken:
            name: bitwarden-access-token
            key: token
            namespace: external-secrets
      url: "https://bitwarden.example.com" # Reemplaza con tu URL de Bitwarden
      tls:
        enabled: true
        caProvider:
          type: Secret
          name: bitwarden-ca-tls
          key: ca.crt
          namespace: external-secrets

Aplicar:

kubectl apply -f 05-cluster-secret-store.yaml
Verificar conexión:
kubectl get ClusterSecretStore bitwarden-cluster-store -o yaml
# Status debe mostrar "Ready: true"
Error común:

Si ves Invalid URL o TLS handshake failed, verifica:

  • La URL de Bitwarden (debe incluir https://).
  • Que el certificado CA (bitwarden-ca-tls) esté correctamente configurado en el ClusterSecretStore.

Paso 5: Sincronizar un secreto desde Bitwarden a Kubernetes

5.1. Crear un secreto en Bitwarden

  1. Ve a Secrets Manager → tu proyecto → New Secret.
  2. Nombre: stripe-api-key.
  3. Valor: sk_test_ejemplo123.
  4. Guarda.

5.2. Crear el ExternalSecret

# 06-external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: stripe-api-key
  namespace: payment-service # Namespace donde se usará el secreto
spec:
  refreshInterval: "1h" # Intervalo de sincronización
  secretStoreRef:
    name: bitwarden-cluster-store
    kind: ClusterSecretStore
  target:
    name: stripe-creds
    creationPolicy: Owner
  data:
    - secretKey: stripe_api_key
      remoteRef:
        key: stripe-api-key

Aplicar:

kubectl apply -f 06-external-secret.yaml
Resultado esperado:
kubectl get externalsecret -n payment-service
# NAME              STORE                       REFRESH INTERVAL   STATUS
# stripe-api-key    bitwarden-cluster-store     1h               Ready

kubectl get secret stripe-creds -n payment-service -o jsonpath='{.data.stripe_api_key}' | base64 -d
# sk_test_ejemplo123
Rotación automática:

Si actualizas el valor de stripe-api-key en Bitwarden, ESO lo sincronizará en ≤15 minutos (depende de refreshInterval).

Consideraciones y buenas prácticas

Seguridad

  1. Tokens con alcance mínimo:
– Usa Machine Accounts con permisos read-only por defecto. Solo otorga write si es necesario (ej: para PushSecret).

– Renueva tokens periódicamente y revoca acceso a Machine Accounts inactivos.

  1. TLS en tránsito:
– Siempre usa HTTPS entre ESO y el backend de secretos. Los certificados auto-firmados son válidos para entornos internos, pero en producción considera usar CA interna o certificados firmados por una CA confiable.
  1. Almacenamiento local de secretos:
– ESO no almacena secretos en etcd. Los sincroniza directamente a Secrets de Kubernetes, que sí se cifran en etcd (con AES-256 si usas KMS en cloud).

Rendimiento y escalabilidad

  1. Carga en el backend:
– Cada ExternalSecret genera una petición al backend por refreshInterval. Para miles de secretos, considera:

– Agrupar secretos en projects de Bitwarden para reducir peticiones.

– Usar ClusterSecretStore en lugar de SecretStore para evitar redundancia en configuración.

  1. Latencia:
– La sincronización depende de la API del backend. Bitwarden suele responder en <1s, pero en entornos con alta latencia (ej: multi-cloud), ajusta refreshInterval a 5-10m para reducir carga.

Alternativas a Bitwarden

ProveedorVentajasConsideraciones
**HashiCorp Vault**Soporte avanzado de políticas, HSMRequiere configurar autenticación (ej: JWT, AppRole)
**AWS Secrets Manager**Integración nativa con EKSCoste por secreto almacenado (~$0.40/mes)
**Azure Key Vault**Integración con AKS y RBACRequiere permisos de Azure AD
**Google Secret Manager**Cifrado por defecto con CMEKLimitado a entornos GCP
## Conclusión

External Secrets Operator resuelve el desastre de secretos (secret sprawl) en entornos Kubernetes multi-cuenta al centralizar el almacenamiento y automatizar la sincronización. Con este enfoque:

Elimina el «copy-paste» de credenciales entre clústeres.

Reduce el riesgo al evitar secretos estáticos en repositorios de código.

Simplifica rotaciones (actualiza una vez en Bitwarden y propaga automáticamente).

Es portable (el patrón aplica a Vault, AWS Secrets Manager, etc.).

Próximos pasos:
  1. Replica este patrón en todos tus clústeres (dev/staging/prod).
  2. Automatiza la instalación con Terraform (ej: módulo de Helm para ESO).
  3. Implementa auditoría: usa kubectl get events --field-selector involvedObject.kind=ExternalSecret para monitorear sincronizaciones fallidas.

Fuentes

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *