Hemos protegido el borde con esmero: red segmentada, balanceador, caché, permisos mínimos y un WAF calibrado con paciencia. Y mientras tanto, la contraseña del usuario app_catalogo de la base de datos alpinashop-pedidos sigue exactamente donde la dejamos en 02-01: en una variable de entorno definida dentro del startup-script de la plantilla de instancia del MIG.

Merece la pena enumerar por qué eso es deuda de seguridad, y no una simple falta de elegancia:

  • Es legible por cualquiera con compute.instances.get. Los metadatos de una instancia, incluido el script de arranque, se leen con un gcloud compute instances describe. Dani, que solo tiene rol de lectura en producción, puede ver la contraseña de producción.
  • Está en el historial de Git, porque la plantilla se genera desde un script versionado. Borrarla del fichero no la borra del repositorio.
  • No se puede rotar sin volver a desplegar. Cambiar la contraseña exige una plantilla nueva y una actualización progresiva del MIG. En la práctica, eso significa que no se rota nunca.
  • Está duplicada. En la plantilla, en el .env del portátil de Dani, en el entorno de desarrollo, probablemente en un mensaje de chat de hace ocho meses.
  • No deja rastro. No hay forma de saber quién la ha leído ni cuándo.

Esta lección resuelve ese problema y responde además a la pregunta que viene justo detrás: si Google guarda mis datos, ¿quién los cifra y con qué clave? Verás Secret Manager para los secretos de la aplicación y Cloud KMS para el material criptográfico, y entenderás cuándo tiene sentido asumir la responsabilidad de gestionar tus propias claves y cuándo es una complicación que no compra seguridad real.

Aviso. Cifrado y gestión de secretos tocan directamente obligaciones normativas: RGPD, PCI DSS si se procesan pagos, y requisitos sectoriales de residencia del dato. Lo que sigue es un modelo docente técnicamente correcto, pero cualquier diseño destinado a producción con datos personales o de pago debe revisarlo un profesional de seguridad y cumplimiento antes de desplegarse. Un fallo aquí no se mide en minutos de caída.

Contenido

  1. El problema concreto: la contraseña de app_catalogo
  2. Secret Manager: modelo de secretos y versiones
  3. Crear los secretos de AlpinaShop
  4. Acceder desde la aplicación Flask
  5. Permisos por secreto: el mínimo privilegio aplicado
  6. Rotación: desactivar en lugar de destruir
  7. Replicación automática frente a regional: residencia del dato
  8. Integración con Cloud Run, GKE y Cloud Build
  9. Cifrado en Google Cloud: qué ocurre por defecto
  10. La jerarquía de claves: DEK y KEK
  11. Google-managed, CMEK y CSEK: tabla de decisión
  12. Cloud KMS: llavero, claves y rotación
  13. HSM y External Key Manager
  14. Aplicar CMEK al bucket y a Cloud SQL
  15. Cifrado en tránsito
  16. Qué NO hacer nunca

  1. El problema concreto: la contraseña de app_catalogo

Así está hoy la plantilla del MIG, y así no debe quedarse:

# ANTIPATRON: la contrasena en los metadatos de la instancia
gcloud compute instance-templates create tpl-catalogo-v3 \
  --metadata=startup-script='#!/bin/bash
export DB_PASSWORD="Tr3kking!2026"     # <-- visible para todo el mundo
gunicorn --bind 0.0.0.0:8080 app:app'

Comprueba tú mismo lo expuesta que está:

gcloud compute instances describe alpinashop-web-1 --zone=europe-west1-b \
  --format="value(metadata.items[startup-script])"

Ese comando solo necesita compute.instances.get, un permiso que forma parte de roles/compute.viewer y que hemos concedido al grupo gcp-desarrollo@ en producción (03-04). Es decir: nuestro propio diseño de permisos, cuidadosamente construido, se salta por completo por culpa de dónde está guardada la contraseña.

El destino es este, y el resto de la lección explica cada pieza:

flowchart LR
    APP["App Flask en el MIG<br/>identidad: sa-catalogo-web"]
    SM["Secret Manager<br/>db-password-catalogo"]
    KMS["Cloud KMS<br/>cifra el secreto en reposo"]
    SQL[("Cloud SQL<br/>alpinashop-pedidos")]
    AUD["Registros de auditoría<br/>quién accedió y cuándo"]

    APP -->|"1. accessSecretVersion<br/>con su identidad"| SM
    SM -->|2. descifra| KMS
    SM -->|3. devuelve el valor| APP
    APP -->|4. conecta| SQL
    SM -.->|registra cada acceso| AUD

  1. Secret Manager: modelo de secretos y versiones

Secret Manager guarda cadenas o binarios pequeños —contraseñas, claves de API, certificados, tokens— con control de acceso por IAM, cifrado en reposo, versionado y auditoría.

El modelo tiene dos niveles, y confundirlos es la fuente de casi todos los errores:

Nivel Qué es Contiene
Secreto El contenedor con nombre y su política IAM Metadatos, etiquetas, política de replicación, ningún valor
Versión Un valor concreto, inmutable, numerado desde 1 El dato real

Propiedades que se derivan de ese modelo:

  • Las versiones son inmutables. No se "actualiza" un secreto: se añade una versión nueva. La 1 sigue existiendo.
  • latest es un alias móvil que apunta siempre a la versión habilitada más reciente.
  • Una versión puede estar en tres estados: habilitada (se puede leer), deshabilitada (existe pero falla el acceso, y es reversible) y destruida (el material se borra de forma irreversible; solo quedan los metadatos).
  • Los permisos se dan sobre el secreto, no sobre la versión.

  1. Crear los secretos de AlpinaShop

gcloud config set project alpinashop-prod
gcloud services enable secretmanager.googleapis.com

# 1) Contrasena de la base de datos
gcloud secrets create db-password-catalogo \
  --replication-policy=automatic \
  --labels=entorno=produccion,equipo=plataforma,aplicacion=catalogo,centro-coste=tienda

# 2) Anadir la primera version SIN que quede en el historial del shell.
#    El guion final significa "leer de la entrada estandar".
printf '%s' 'Tr3kking!2026' | gcloud secrets versions add db-password-catalogo --data-file=-

# 3) Clave de la pasarela de pago
gcloud secrets create api-key-pasarela-pago \
  --replication-policy=automatic \
  --labels=entorno=produccion,equipo=plataforma,aplicacion=pagos,centro-coste=tienda

printf '%s' 'sk_live_9f2c...' | gcloud secrets versions add api-key-pasarela-pago --data-file=-

Detalles que importan de estos comandos:

  • --data-file=- con printf y sin salto de línea final. Si usas echo, añades un \n al valor y la contraseña que lee la aplicación no es la que crees. Es un fallo clásico que cuesta media tarde de depuración. Si el secreto está en un fichero, --data-file=fichero y borra el fichero después con shred -u.
  • Nunca pases el valor por la línea de comandos. Existe --data-file, pero no un --data: es deliberado. Un valor en la línea de comandos queda en ~/.bash_history y en la lista de procesos.
  • Etiqueta los secretos con el mismo convenio que el resto de recursos (entorno, equipo, centro-coste, aplicacion). Sirve para el inventario y para la revisión periódica.

Operaciones habituales:

gcloud secrets list --format="table(name, createTime, labels.aplicacion)"

gcloud secrets versions list db-password-catalogo \
  --format="table(name, state, createTime)"

# Leer el valor (esto queda registrado en auditoria)
gcloud secrets versions access latest --secret=db-password-catalogo

  1. Acceder desde la aplicación Flask

La aplicación se autentica con la identidad de la VM —la cuenta de servicio sa-catalogo-web— sin ninguna credencial en el código. Es el patrón que ya usaste con Cloud Storage en 02-02, aplicado ahora a los secretos.

# secretos.py — acceso a Secret Manager con caché en memoria
import os
from functools import lru_cache
from google.cloud import secretmanager

PROYECTO = os.environ.get("GOOGLE_CLOUD_PROJECT", "alpinashop-prod")

# El cliente es caro de crear: uno por proceso.
_cliente = secretmanager.SecretManagerServiceClient()


@lru_cache(maxsize=32)
def leer_secreto(nombre: str, version: str = "latest") -> str:
    """Devuelve el valor de un secreto.

    lru_cache evita ir a la API en cada peticion HTTP: sin ella,
    una tienda con 500 req/s haria 500 llamadas por segundo a
    Secret Manager. Eso es lento, caro y ademas topa con la cuota.

    El precio de la cache es que un cambio de secreto no se ve hasta
    que se reinicia el proceso. Es un compromiso ACEPTABLE y hay que
    tomarlo conscientemente: la rotacion se acompana de un reinicio
    progresivo del MIG.
    """
    ruta = f"projects/{PROYECTO}/secrets/{nombre}/versions/{version}"
    respuesta = _cliente.access_secret_version(request={"name": ruta})
    return respuesta.payload.data.decode("UTF-8")
# app.py — construccion de la cadena de conexion en el arranque
from secretos import leer_secreto

def crear_pool_conexiones():
    password = leer_secreto("db-password-catalogo")
    return crear_pool(
        usuario="app_catalogo",
        password=password,
        host="10.10.1.5",          # IP privada, 03-01
        base_datos="tienda",
    )

Tres decisiones de diseño explícitas en ese código:

  1. Se lee en el arranque, no en cada petición. El coste y la latencia de una llamada por petición son inaceptables.
  2. Se usa latest, lo que hace que un reinicio recoja automáticamente la versión nueva tras una rotación. La alternativa —fijar la versión 3— es más determinista pero obliga a desplegar código para rotar. Para AlpinaShop, latest es la elección correcta; en un sistema donde un secreto equivocado sea catastrófico, la versión fija tiene su argumento.
  3. El valor nunca se registra. Ni en un print, ni en un log de depuración, ni en un mensaje de excepción. Si el valor llega a Cloud Logging (06-06), lo has vuelto a exponer, ahora en un sitio que además se conserva y se exporta.

  1. Permisos por secreto: el mínimo privilegio aplicado

Aquí se aplica de forma directa lo aprendido en 03-04. Los roles relevantes:

Rol Permite Para quién
roles/secretmanager.secretAccessor Leer el valor de las versiones La aplicación. Nada más
roles/secretmanager.viewer Ver que el secreto existe y sus metadatos, sin leerlo Auditoría, inventario
roles/secretmanager.secretVersionAdder Añadir versiones nuevas, sin poder leerlas El proceso de rotación
roles/secretmanager.secretVersionManager Habilitar, deshabilitar y destruir versiones Operación
roles/secretmanager.admin Todo, incluido crear y borrar secretos Solo gcp-infra@, y con moderación
SA_WEB="[email protected]"

# La app solo puede LEER, y solo ESTE secreto
gcloud secrets add-iam-policy-binding db-password-catalogo \
  --member="serviceAccount:$SA_WEB" \
  --role="roles/secretmanager.secretAccessor"

gcloud secrets add-iam-policy-binding api-key-pasarela-pago \
  --member="serviceAccount:$SA_WEB" \
  --role="roles/secretmanager.secretAccessor"

# Auditoria: ve que existen, no los lee
gcloud secrets add-iam-policy-binding db-password-catalogo \
  --member="group:[email protected]" \
  --role="roles/secretmanager.viewer"

# Verificar quien puede leer cada secreto
gcloud secrets get-iam-policy db-password-catalogo --format=yaml

La regla que no hay que romper: los permisos se conceden a nivel de secreto, nunca de proyecto. Un roles/secretmanager.secretAccessor sobre alpinashop-prod daría acceso a todos los secretos presentes y futuros, incluida la clave de la pasarela de pago que se cree el mes que viene. Es exactamente el mismo razonamiento que aplicamos con el bucket y con las condiciones de IAM.

Un detalle que sorprende y conviene saber: el acceso a un secreto queda registrado en los logs de auditoría de acceso a datos, pero esos registros hay que activarlos explícitamente porque no vienen habilitados por defecto. Sin ellos no sabrás quién leyó qué. La configuración de los registros de auditoría se trata en 07-07; para un secreto de producción, actívalos.

  1. Rotación: desactivar en lugar de destruir

Rotar es sustituir el valor por otro nuevo. El procedimiento correcto tiene un paso que casi todo el mundo se salta, y es el que evita el corte de servicio.

# PASO 1 — Crear la credencial nueva en el sistema de destino,
#          conviviendo con la vieja
gcloud sql users set-password app_catalogo \
  --instance=alpinashop-pedidos --password="$NUEVA"

# PASO 2 — Anadir la version nueva al secreto (la vieja sigue viva)
printf '%s' "$NUEVA" | gcloud secrets versions add db-password-catalogo --data-file=-

# PASO 3 — Desplegar: reinicio progresivo del MIG para que los procesos
#          relean 'latest'. Sin corte, gracias al drenaje de conexiones (03-02).
gcloud compute instance-groups managed rolling-action restart alpinashop-web-mig \
  --region=europe-west1 --max-unavailable=1

# PASO 4 — Verificar que nadie usa ya la version anterior, y DESACTIVARLA
gcloud secrets versions disable 1 --secret=db-password-catalogo

# PASO 5 — Solo despues de un periodo de gracia (dias), destruir
gcloud secrets versions destroy 1 --secret=db-password-catalogo

El paso 4 es la clave de toda la lección en materia de operación. disable es reversible: si algo se rompe, un gcloud secrets versions enable 1 devuelve el servicio en segundos. destroy es irreversible. La regla: deshabilitar siempre primero, destruir mucho después, y nunca los dos el mismo día.

Secret Manager puede además recordarte que toca rotar:

gcloud secrets update db-password-catalogo \
  --next-rotation-time="2026-11-01T03:00:00Z" \
  --rotation-period="90d" \
  --add-topics="projects/alpinashop-prod/topics/rotacion-secretos"

Aquí hay un matiz que se malinterpreta muy a menudo: Secret Manager no rota nada. Publica un mensaje en un tema de Pub/Sub cuando llega la fecha. Quien rota es tu automatismo —una Cloud Function suscrita a ese tema (06-03) que genera la contraseña, la aplica en Cloud SQL, añade la versión y lanza el reinicio del MIG—. La rotación completamente automática es un proyecto en sí mismo; el recordatorio con Pub/Sub y un procedimiento escrito es lo mínimo exigible.

También se puede poner caducidad a un secreto, útil para credenciales temporales:

gcloud secrets update credencial-consultora-agosto --expire-time="2026-09-16T00:00:00Z"

  1. Replicación automática frente a regional: residencia del dato

Al crear un secreto se elige dónde se almacena, y la decisión es irreversible.

Política Dónde vive Ventajas Cuándo
automatic Google decide, replicado globalmente Máxima disponibilidad, sin decisiones Por defecto, salvo requisito contrario
user-managed En las regiones que tú indiques Cumple requisitos de residencia Cuando el dato no puede salir de una zona geográfica
# Secreto restringido a Europa, con dos regiones para tolerar el fallo de una
gcloud secrets create api-key-pasarela-pago-eu \
  --replication-policy=user-managed \
  --locations=europe-west1,europe-west4

Existe además la variante de secretos regionales, creados con --location=europe-west1, que viven íntegramente en el plano de control regional y ofrecen el aislamiento más estricto. Se acceden con un punto final regional (secretmanager.europe-west1.rep.googleapis.com), lo cual hay que tener en cuenta en el código.

Para AlpinaShop, cuya facturación y clientela son europeas, la elección razonable es user-managed con regiones europeas para los secretos ligados a datos personales o de pago, y automatic para el resto. Cuál de las dos exige el marco normativo aplicable es una pregunta para el responsable de cumplimiento, no para el equipo de plataforma.

  1. Integración con Cloud Run, GKE y Cloud Build

Fuera de las VM, la integración es aún más limpia: la plataforma inyecta el secreto y tu código ni siquiera llama a la API.

Cloud Run (que será el destino del catálogo según DA-001, en 07-02) ofrece las dos formas:

# Como variable de entorno: comodo
gcloud run deploy catalogo \
  --image=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:1.4.0 \
  --region=europe-west1 \
  --service-account=sa-catalogo-web@alpinashop-prod.iam.gserviceaccount.com \
  --set-secrets=DB_PASSWORD=db-password-catalogo:latest

# Como fichero montado: mas seguro
gcloud run deploy catalogo \
  --image=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:1.4.0 \
  --region=europe-west1 \
  --service-account=sa-catalogo-web@alpinashop-prod.iam.gserviceaccount.com \
  --set-secrets=/secretos/db-password=db-password-catalogo:latest
Variable de entorno Fichero montado
Comodidad Alta: os.environ["DB_PASSWORD"] Requiere leer un fichero
Visibilidad accidental Alta: aparece en volcados de entorno, en trazas de excepción de muchos frameworks y en herramientas de depuración Baja
Actualización sin redesplegar No, con :latest se fija al desplegar : usando :latest, el fichero se actualiza
Recomendación Aceptable para secretos poco críticos Preferible para contraseñas y claves de pago

GKE Autopilot (el clúster alpinashop-cluster, namespace tienda) usa el complemento de Secret Manager con el controlador CSI, que monta el secreto como volumen:

# secret-provider.yaml — el secreto se monta, no se copia a un Secret de Kubernetes
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: alpinashop-secretos
  namespace: tienda
spec:
  provider: gcp
  parameters:
    secrets: |
      - resourceName: "projects/alpinashop-prod/secrets/db-password-catalogo/versions/latest"
        path: "db-password"
# En el Deployment: se monta como fichero en /var/secretos/db-password
      volumes:
        - name: secretos
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: "alpinashop-secretos"

Esto cierra por fin la advertencia que dejamos en 02-05: un Secret de Kubernetes está codificado en base64, que no es cifrado. Cualquiera con permiso de lectura en el namespace lo descodifica en un segundo. Con el controlador CSI, el valor no existe como objeto de Kubernetes: lo trae Secret Manager, autenticado con Workload Identity, y solo aparece en el sistema de ficheros del pod.

Cloud Build (06-01) permite usar secretos en el pipeline sin escribirlos en el fichero de configuración:

# cloudbuild.yaml
availableSecrets:
  secretManager:
    - versionName: projects/alpinashop-prod/secrets/api-key-pasarela-pago/versions/latest
      env: 'API_KEY_PAGO'

steps:
  - name: python:3.12
    entrypoint: bash
    secretEnv: ['API_KEY_PAGO']
    args:
      - -c
      - |
        # Disponible como $$API_KEY_PAGO. NUNCA lo imprimas:
        # los logs de Cloud Build son legibles por todo el equipo.
        pytest tests/integracion

  1. Cifrado en Google Cloud: qué ocurre por defecto

Cambiamos de tema, del secreto de aplicación al cifrado de los datos.

Lo primero, para quitar ansiedad innecesaria: todos los datos en reposo de Google Cloud están cifrados por defecto, siempre, sin que hagas nada y sin coste adicional. Cloud Storage, discos persistentes, Cloud SQL, BigQuery, Firestore: todo. No hay una casilla de "activar cifrado" porque no hay forma de desactivarlo.

Por tanto, la pregunta correcta no es "¿están cifrados mis datos?" sino "¿quién controla la clave?". Y esa pregunta rara vez la responde la seguridad técnica: la responde el cumplimiento normativo, el sector y, a veces, el contrato con un cliente grande.

  1. La jerarquía de claves: DEK y KEK

Entender esto explica de golpe por qué CMEK es barato de activar y por qué rotar una clave no reescribe petabytes.

flowchart TD
    D["Tus datos<br/>troceados en fragmentos"]
    DEK["DEK — Data Encryption Key<br/>una por fragmento, AES-256"]
    KEK["KEK — Key Encryption Key<br/>vive en Cloud KMS, nunca sale"]
    ROOT["Almacén raíz de claves de Google<br/>o tu clave CMEK"]

    D -->|cifrado con| DEK
    DEK -->|"envuelta (wrapped) por"| KEK
    KEK -->|protegida por| ROOT

El mecanismo, paso a paso:

  1. Los datos se trocean en fragmentos y cada fragmento se cifra con una DEK propia y distinta.
  2. La DEK no se guarda en claro: se cifra ("envuelve") con una KEK y se almacena envuelta junto al fragmento.
  3. La KEK vive en Cloud KMS y nunca sale de ahí. Para descifrar, el servicio envía la DEK envuelta a KMS, que la desenvuelve y la devuelve; la DEK en claro vive en memoria lo justo.

Consecuencias que responden preguntas muy frecuentes:

  • Rotar la KEK es instantáneo y no reescribe ni un byte de datos. Solo cambia la clave con la que se envuelven las DEK nuevas; las antiguas se siguen desenvolviendo con la versión antigua, que se conserva.
  • Revocar el acceso a la KEK inutiliza los datos inmediatamente. Sin KEK no hay DEK, y sin DEK no hay datos. Esto es a la vez la garantía más potente de CMEK y su mayor peligro operativo.
  • CMEK es sustituir la KEK, no cifrar tú los datos. Por eso activarlo tiene un coste computacional despreciable.

  1. Google-managed, CMEK y CSEK: tabla de decisión

Google-managed (por defecto) CMEK (claves gestionadas por el cliente) CSEK (claves proporcionadas por el cliente)
Quién crea la clave Google Tú, en Cloud KMS Tú, fuera de Google
Dónde vive Infraestructura de Google Cloud KMS, en tu proyecto En ningún sitio de Google: la envías en cada petición
Puedes rotarla No la ves Sí, manual o automática Tú te lo montas
Puedes revocarla No : inutiliza los datos al instante Dejando de enviarla
Auditoría de uso de la clave No , cada operación en los logs No
Si la pierdes No aplica Se puede recuperar mientras esté programada para destrucción Los datos se pierden para siempre
Servicios compatibles Todos La mayoría de los importantes Solo Cloud Storage y discos de Compute Engine
Coste 0 Coste de KMS: bajo 0, pero altísimo coste operativo
Complejidad Ninguna Media Alta

Cómo decidir, sin misticismo:

  • Google-managed cubre bien la inmensa mayoría de los casos. Si nadie te ha pedido lo contrario, esto es lo correcto y no es "menos seguro".
  • CMEK cuando necesitas una de estas tres cosas: poder revocar el acceso a los datos de forma unilateral, auditar cada uso de la clave, o cumplir un requisito normativo o contractual que lo exija explícitamente. Añade un modo de fallo nuevo —si la clave no está disponible, el servicio no arranca— y eso hay que asumirlo conscientemente.
  • CSEK es casi siempre la respuesta equivocada. Asumes la custodia, la rotación y la disponibilidad de la clave, y un error significa pérdida definitiva de datos. Existe para requisitos muy concretos donde la clave no puede residir en la nube bajo ninguna circunstancia.

Para AlpinaShop, la decisión razonada: CMEK en alpinashop-catalogo y en la base de datos de pedidos, porque contienen datos de clientes y la empresa quiere poder demostrar control de la clave ante una auditoría; Google-managed en alpinashop-dev, donde no hay datos reales y añadir un modo de fallo no compensa.

  1. Cloud KMS: llavero, claves y rotación

La jerarquía de KMS es: proyecto → ubicación → llavero (keyring) → clave → versiones de clave.

gcloud services enable cloudkms.googleapis.com

# El llavero agrupa claves y NO SE PUEDE BORRAR. Elige bien el nombre y la ubicacion.
gcloud kms keyrings create alpinashop-keyring --location=europe-west1

# Clave simetrica con rotacion automatica cada 90 dias
gcloud kms keys create clave-catalogo \
  --location=europe-west1 \
  --keyring=alpinashop-keyring \
  --purpose=encryption \
  --rotation-period=90d \
  --next-rotation-time=2026-11-01T03:00:00Z \
  --protection-level=software

gcloud kms keys create clave-pedidos \
  --location=europe-west1 \
  --keyring=alpinashop-keyring \
  --purpose=encryption \
  --rotation-period=90d \
  --next-rotation-time=2026-11-01T03:00:00Z

gcloud kms keys versions list --key=clave-catalogo \
  --keyring=alpinashop-keyring --location=europe-west1

Cuatro cosas que hay que saber de KMS y que no son evidentes:

  1. La ubicación de la clave debe ser compatible con la del recurso. Una clave en europe-west1 para un bucket en europe-west1: correcto. Para un bucket multirregión EU hace falta una clave en la ubicación europe. Si no coinciden, la operación falla con un error poco claro.
  2. Los llaveros y las claves no se pueden borrar. Nunca. Solo se destruyen sus versiones. Esto es deliberado: evita que un borrado accidental deje datos irrecuperables. Consecuencia práctica: piensa la nomenclatura antes de crear, porque vivirá para siempre.
  3. Destruir una versión de clave tiene un periodo de gracia, 30 días por defecto, durante el cual se puede restaurar. Es la red de seguridad que impide que un error se convierta en una catástrofe.
  4. La rotación automática no reencripta nada. Crea una versión nueva que pasa a ser la primaria para las operaciones futuras. Las versiones antiguas se conservan habilitadas para poder descifrar lo ya cifrado.

Permisos, otra vez con criterio de mínimo privilegio:

Rol Permite
roles/cloudkms.cryptoKeyEncrypterDecrypter Cifrar y descifrar con la clave. Es el que necesitan los agentes de servicio
roles/cloudkms.cryptoKeyEncrypter Solo cifrar
roles/cloudkms.viewer Ver metadatos, sin usarla
roles/cloudkms.admin Gestionar claves. Nunca a quien también accede a los datos

Esa última línea es separación de funciones aplicada al cifrado: quien administra las claves no debe ser quien accede a los datos cifrados, porque en tal caso el cifrado no protege de nada frente a esa persona.

KMS sirve además para cifrar datos pequeños directamente:

echo -n "dato sensible" | gcloud kms encrypt \
  --location=europe-west1 --keyring=alpinashop-keyring --key=clave-catalogo \
  --plaintext-file=- --ciphertext-file=cifrado.bin

gcloud kms decrypt \
  --location=europe-west1 --keyring=alpinashop-keyring --key=clave-catalogo \
  --ciphertext-file=cifrado.bin --plaintext-file=-

Aunque para contraseñas y claves de API, Secret Manager es la herramienta adecuada, no KMS. KMS es para claves; Secret Manager es para secretos. Internamente, Secret Manager ya usa KMS.

  1. HSM y External Key Manager

El nivel de protección determina dónde vive físicamente el material de la clave:

Nivel Dónde reside la clave Certificación Coste relativo Cuándo
SOFTWARE En la infraestructura de Google Base Por defecto
HSM Módulo de hardware dedicado FIPS 140-2 nivel 3 ~10-40× por versión de clave Requisito normativo explícito
EXTERNAL Fuera de Google, en tu proveedor de EKM Según el proveedor Alto + coste del proveedor Soberanía del dato, requisito contractual
gcloud kms keys create clave-pagos-hsm \
  --location=europe-west1 --keyring=alpinashop-keyring \
  --purpose=encryption --protection-level=hsm

External Key Manager (EKM) es el caso extremo: la clave vive en un gestor externo (Thales, Fortanix, Equinix y similares) y Google llama a ese sistema para cada operación criptográfica. Si cortas el acceso, Google no puede descifrar tus datos, ni siquiera si quisiera. Es la respuesta técnica a "quiero poder demostrar que ni el proveedor de nube puede leer mis datos".

El precio de esa garantía es alto y hay que decirlo claro: añades una dependencia externa en el camino crítico de acceso a tus datos. Si el EKM no responde, tus servicios no arrancan. Para una pyme como AlpinaShop, SOFTWARE con CMEK es la elección proporcionada; HSM solo si la pasarela de pago o una auditoría PCI lo exigen por escrito.

  1. Aplicar CMEK al bucket y a Cloud SQL

Aquí aparece el concepto que más gente atasca: el agente de servicio. Cuando Cloud Storage cifra un objeto con tu clave, quien llama a KMS no eres tú: es una cuenta de servicio interna del servicio, gestionada por Google, que debe tener permiso sobre tu clave.

Bucket alpinashop-catalogo:

PROYECTO_NUM=$(gcloud projects describe alpinashop-prod --format="value(projectNumber)")
CLAVE="projects/alpinashop-prod/locations/europe-west1/keyRings/alpinashop-keyring/cryptoKeys/clave-catalogo"

# 1) Obtener el agente de servicio de Cloud Storage del proyecto
AGENTE_GCS=$(gcloud storage service-agent --project=alpinashop-prod)
echo "$AGENTE_GCS"   # service-<NUM>@gs-project-accounts.iam.gserviceaccount.com

# 2) Darle permiso sobre la clave (y SOLO sobre esta clave)
gcloud kms keys add-iam-policy-binding clave-catalogo \
  --location=europe-west1 --keyring=alpinashop-keyring \
  --member="serviceAccount:$AGENTE_GCS" \
  --role="roles/cloudkms.cryptoKeyEncrypterDecrypter"

# 3) Establecer la clave por defecto del bucket
gcloud storage buckets update gs://alpinashop-catalogo \
  --default-encryption-key="$CLAVE"

gcloud storage buckets describe gs://alpinashop-catalogo \
  --format="value(default_kms_key)"

Un matiz esencial: esto solo afecta a los objetos nuevos. Los 60 GB ya subidos siguen cifrados con la clave de Google. Para migrarlos hay que reescribirlos:

# Reescribe los objetos existentes con la clave nueva.
# En 60 GB esto tarda y genera operaciones de clase A: hazlo por lotes y fuera de hora punta.
gcloud storage objects update "gs://alpinashop-catalogo/productos/**" \
  --encryption-key="$CLAVE"

Cloud SQL alpinashop-pedidos: aquí hay una restricción que cambia el plan de trabajo y que conviene saber antes de prometer nada.

CMEK en Cloud SQL solo se puede establecer al crear la instancia. No se puede añadir a una instancia existente. Migrar alpinashop-pedidos a CMEK significa crear una instancia nueva y migrar los datos, con la ventana de mantenimiento que eso implica.

AGENTE_SQL="service-${PROYECTO_NUM}@gcp-sa-cloud-sql.iam.gserviceaccount.com"

gcloud kms keys add-iam-policy-binding clave-pedidos \
  --location=europe-west1 --keyring=alpinashop-keyring \
  --member="serviceAccount:$AGENTE_SQL" \
  --role="roles/cloudkms.cryptoKeyEncrypterDecrypter"

# Instancia NUEVA con CMEK desde el primer momento
gcloud sql instances create alpinashop-pedidos-cmek \
  --database-version=POSTGRES_16 \
  --region=europe-west1 \
  --availability-type=REGIONAL \
  --disk-encryption-key="projects/alpinashop-prod/locations/europe-west1/keyRings/alpinashop-keyring/cryptoKeys/clave-pedidos"

La migración se haría con las herramientas de 02-03: copia, restauración en la instancia nueva, sincronización y cambio de la cadena de conexión. La decisión de AlpinaShop es hacerlo fuera de la campaña de otoño, no antes.

Y el aviso operativo que cierra el apartado, porque es el riesgo real de CMEK:

Si deshabilitas o destruyes la versión de la clave, el bucket deja de servir objetos y la base de datos deja de arrancar, de forma inmediata. No es un aviso teórico: es exactamente el comportamiento diseñado. Antes de tocar una clave en producción, comprueba con gcloud logging read qué la está usando, y ten un procedimiento escrito de recuperación.

  1. Cifrado en tránsito

Menos configurable, pero conviene conocer el mapa completo:

Trayecto Cómo se cifra ¿Configuras algo?
Cliente → balanceador TLS con tu certificado Sí: certificados y política SSL, en 03-07
Balanceador → backend TLS o red privada de Google Opcional: HTTPS hacia el backend
Entre servicios de Google ALTS, protocolo propio de autenticación y cifrado No
Entre centros de datos de Google Cifrado a nivel de enlace y de aplicación No
Aplicación → Cloud SQL TLS, y con el Auth Proxy además autenticación IAM (02-03) Sí: usa el Auth Proxy
Aplicación → APIs de Google TLS 1.2 o superior No

El punto que sí depende de ti es el primero, y es el tema de la lección siguiente. El segundo también merece una decisión: el tráfico entre el balanceador y el MIG viaja por la red privada de Google dentro de alpinashop-vpc, lo cual es razonable, pero para datos de pago la práctica recomendada es cifrar también ese tramo configurando el servicio de backend con protocolo HTTPS.

  1. Qué NO hacer nunca

Una lista corta, sin matices, porque cada punto ha causado una brecha real en alguna empresa:

  • Secretos en Git. Ni en un .env, ni en un config.py, ni en un terraform.tfvars, ni en un fichero de pruebas. Borrarlos no basta: quedan en el historial. Si ocurre, hay que rotar la credencial, no solo reescribir el historial. Usa git-secrets o el escaneo de secretos de tu plataforma para impedirlo de entrada.
  • Secretos en imágenes de contenedor. Un ENV DB_PASSWORD=... o un fichero copiado en el Dockerfile quedan en la capa de la imagen y son legibles por cualquiera que la descargue de europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo.
  • Secretos en metadatos o variables de instancia. El problema con el que empezó esta lección.
  • Secretos en los logs. Una excepción que imprime la cadena de conexión completa deja la contraseña en Cloud Logging, donde se conserva y a veces se exporta a BigQuery.
  • Secretos en Secrets de Kubernetes sin cifrado adicional. Base64 no es cifrado.
  • Secretos por chat o correo. Quedan indexados y sincronizados en dispositivos que no controlas.
  • La misma contraseña en desarrollo y en producción. El entorno de desarrollo tiene permisos más laxos por diseño; comprometerlo no debe comprometer producción.
  • Claves JSON de cuentas de servicio descargadas (03-04). Son secretos que no caducan.

Errores Comunes y Consejos

  • Usar echo en lugar de printf al crear una versión. Añade un salto de línea al valor y la autenticación falla con un mensaje que no ayuda.
  • Conceder secretAccessor a nivel de proyecto. Da acceso a todos los secretos, incluidos los futuros. Siempre a nivel de secreto.
  • Leer el secreto en cada petición HTTP. Latencia, coste y cuota. Cachéalo en memoria y acepta conscientemente que la rotación exige reiniciar.
  • Destruir una versión sin haberla deshabilitado antes. disable es reversible; destroy no. Nunca los dos el mismo día.
  • Creer que Secret Manager rota los secretos. Solo avisa por Pub/Sub. La rotación la programas tú.
  • Elegir la política de replicación sin pensar. Es irreversible y puede tener implicaciones de residencia del dato.
  • Registrar el valor del secreto. Un print de depuración lo lleva a Cloud Logging, que se conserva y se exporta.
  • Olvidar el agente de servicio al activar CMEK. El error es un permiso denegado sobre la clave, no sobre el bucket, y despista mucho.
  • Esperar que CMEK cifre lo que ya existe. Solo aplica a los datos nuevos; lo anterior hay que reescribirlo.
  • Prometer CMEK en una instancia de Cloud SQL existente. Solo se puede al crearla; implica migración.
  • Desactivar una versión de clave en producción "para probar". El bucket deja de servir y la base de datos deja de arrancar al instante.
  • Confundir KMS con Secret Manager. KMS gestiona claves; Secret Manager gestiona secretos. Para una contraseña, Secret Manager.
  • Poner claves en HSM por precaución. Cuesta bastante más por versión de clave y solo aporta si hay un requisito normativo concreto.
  • Consejo: separa quién administra las claves de quién accede a los datos. Sin esa separación, el cifrado no protege de nadie relevante.
  • Consejo: nombra los secretos por su función, no por su valor. db-password-catalogo, no password-prod-2026.

Ejercicios

Ejercicio 1 — Sacar la contraseña de la plantilla del MIG

Escribe el plan completo para eliminar la contraseña del startup-script de tpl-catalogo-v3 sin cortar el servicio: comandos, cambios en el código, permisos, orden de las operaciones y verificación. Incluye qué hacer con la contraseña actual, que lleva ocho meses expuesta en los metadatos y en el historial de Git.

Ejercicio 2 — Decidir el modelo de cifrado

Para cada conjunto de datos de AlpinaShop, decide entre Google-managed, CMEK o CSEK, y justifícalo en dos líneas:

  1. Las 60 GB de imágenes de producto en alpinashop-catalogo.
  2. La base de datos alpinashop-pedidos, con nombres, direcciones y últimos cuatro dígitos de tarjeta.
  3. Las exportaciones CSV para Lucía en gs://alpinashop-catalogo/exportaciones/.
  4. Los discos de las instancias del MIG, que solo contienen el código de la aplicación.
  5. Una copia de los contratos firmados con proveedores, que por contrato "no puede ser accesible por el proveedor de nube".

Ejercicio 3 — Un incidente de secreto expuesto

Un lunes por la mañana, el escaneo de secretos del repositorio avisa: la clave sk_live_... de la pasarela de pago está en un commit de hace tres semanas, en un fichero de pruebas de integración. El repositorio es privado, con acceso para las seis personas del equipo técnico y dos colaboradores externos.

Escribe el plan de respuesta ordenado por prioridad, y las medidas para que no vuelva a ocurrir.


Soluciones

Solución 1

Orden de operaciones, sin corte de servicio:

# PASO 0 — La contrasena actual esta comprometida. Se ROTA, no se reutiliza.
NUEVA=$(openssl rand -base64 32)

gcloud sql users set-password app_catalogo \
  --instance=alpinashop-pedidos --password="$NUEVA"

# PASO 1 — Crear el secreto con el valor NUEVO
gcloud secrets create db-password-catalogo \
  --replication-policy=user-managed --locations=europe-west1,europe-west4 \
  --labels=entorno=produccion,equipo=plataforma,aplicacion=catalogo
printf '%s' "$NUEVA" | gcloud secrets versions add db-password-catalogo --data-file=-
unset NUEVA

# PASO 2 — Permiso, solo a la identidad de la app y solo sobre este secreto
gcloud secrets add-iam-policy-binding db-password-catalogo \
  --member="serviceAccount:[email protected]" \
  --role="roles/secretmanager.secretAccessor"

PASO 3 — Cambio en el código: la aplicación pasa a usar el módulo secretos.py del apartado 4 y deja de leer os.environ["DB_PASSWORD"]. Es importante que el arranque falle de forma ruidosa si no puede leer el secreto, en lugar de continuar con un valor vacío.

# PASO 4 — Plantilla nueva, SIN contrasena y con la cuenta de servicio correcta
gcloud compute instance-templates create tpl-catalogo-v4 \
  --service-account=sa-catalogo-web@alpinashop-prod.iam.gserviceaccount.com \
  --scopes=https://www.googleapis.com/auth/cloud-platform \
  --metadata-from-file=startup-script=startup-sin-secretos.sh \
  --tags=web-catalogo

# PASO 5 — Actualizacion progresiva, sin corte (02-01 y 03-02)
gcloud compute instance-groups managed rolling-action start-update alpinashop-web-mig \
  --region=europe-west1 \
  --version=template=tpl-catalogo-v4 \
  --max-surge=2 --max-unavailable=0

# PASO 6 — Verificar y limpiar
gcloud compute instances describe alpinashop-web-1 --zone=europe-west1-b \
  --format="value(metadata.items[startup-script])" | grep -i password || echo "Limpio"

gcloud compute instance-templates delete tpl-catalogo-v3 --quiet

Qué hacer con la contraseña vieja. Estuvo ocho meses legible para cualquiera con compute.viewer y quedó en el historial de Git. Se considera comprometida: rotarla es obligatorio y es el paso 0, no el último. Reescribir el historial de Git es recomendable pero secundario; lo que anula el riesgo es que el valor ya no sirva para nada. Adicionalmente, conviene revisar los logs de acceso a Cloud SQL del periodo por si hubiera conexiones desde orígenes inesperados.

Solución 2

  1. Imágenes de producto → Google-managed, o CMEK si se busca coherencia. No son datos personales ni confidenciales: son fotos de un catálogo público. Google-managed es suficiente. En el caso de AlpinaShop se aplicó CMEK por homogeneidad de política y para poder demostrar control ante una auditoría, lo cual es un argumento válido siempre que se asuma el modo de fallo añadido.
  2. alpinashop-pedidos → CMEK, sin duda. Contiene datos personales sujetos al RGPD y fragmentos de datos de tarjeta. Aquí la capacidad de revocar y de auditar el uso de la clave es exactamente lo que se pide en una auditoría. Con la salvedad operativa conocida: exige crear una instancia nueva y migrar.
  3. Exportaciones CSV → CMEK, la misma clave que los pedidos. Son un extracto de los mismos datos personales. Sería incoherente proteger el origen y no la copia; de hecho, las exportaciones suelen ser el punto más débil de la cadena.
  4. Discos del MIG → Google-managed. Solo contienen código, que además está en Artifact Registry y en Git. CMEK aquí solo añadiría un modo de fallo —si la clave falla, las instancias no arrancan— sin proteger nada que no sea ya público internamente.
  5. Contratos con la cláusula de inaccesibilidad → EKM (EXTERNAL), o replantear el requisito. Es exactamente el caso para el que existe External Key Manager: la clave vive fuera de Google y sin ella Google no puede descifrar. CSEK sería técnicamente posible en Cloud Storage, pero la custodia manual de la clave es demasiado frágil para un requisito contractual. La alternativa honesta es discutir con el proveedor si el requisito se satisface con CMEK más registros de acceso, que es bastante más operable.

Solución 3

Prioridad 1 — Invalidar la credencial (minutos, no horas).

# 1) En el panel de la pasarela de pago: revocar sk_live_... AHORA.
#    Mientras exista, el repositorio, sus copias y los portatiles de ocho
#    personas la contienen. Reescribir el historial NO la invalida.

# 2) Generar la clave nueva y guardarla donde debe estar
printf '%s' "$CLAVE_NUEVA" | \
  gcloud secrets versions add api-key-pasarela-pago --data-file=-

# 3) Reinicio progresivo para que los procesos relean 'latest'
gcloud compute instance-groups managed rolling-action restart alpinashop-web-mig \
  --region=europe-west1 --max-unavailable=1

Prioridad 2 — Evaluar el impacto. Revisar en el panel de la pasarela las operaciones de las últimas tres semanas buscando cobros, reembolsos o consultas desde orígenes no habituales. Si hubo uso indebido, hay obligaciones de notificación que dependen del marco aplicable: eso lo decide el responsable de cumplimiento, no el equipo técnico.

Prioridad 3 — Limpiar. Eliminar el secreto del historial de Git (git filter-repo o equivalente), forzar la actualización de todas las copias del equipo y comprobar que no quedó en artefactos derivados: imágenes de contenedor en Artifact Registry, registros de Cloud Build, exportaciones de logs.

Prioridad 4 — Que no vuelva a pasar.

Medida Cómo
Escaneo previo al commit git-secrets o gitleaks como hook, más el escaneo de secretos del proveedor del repositorio
Escaneo en el pipeline Un paso de Cloud Build que falla si detecta patrones de credencial (06-01)
Secretos en las pruebas Las pruebas de integración leen de Secret Manager con una credencial de pruebas, nunca sk_live_
Claves separadas por entorno La pasarela ofrece claves de prueba y de producción; en desarrollo no debe existir una de producción
Revisión de accesos Reevaluar si los dos colaboradores externos necesitan acceso al repositorio completo
Rotación periódica --rotation-period=90d con aviso por Pub/Sub, para que rotar sea rutina y no una emergencia
Formación El fichero de pruebas se subió sin mala intención. La medida más eficaz suele ser explicar al equipo por qué importa

Conclusión

La contraseña de app_catalogo ha salido del startup-script. Sabes por qué ese sitio era inaceptable —legible con compute.viewer, presente en el historial de Git, irrotable sin desplegar, duplicada por todas partes y sin ninguna trazabilidad— y has construido la alternativa completa: los secretos db-password-catalogo y api-key-pasarela-pago en Secret Manager, con su modelo de secreto contenedor y versiones inmutables, el alias latest, y los tres estados de una versión.

Dominas el acceso desde la aplicación Flask con la identidad sa-catalogo-web y sin una sola credencial en el código, con caché en memoria y la conciencia de lo que esa caché implica para la rotación. Has concedido roles/secretmanager.secretAccessor a nivel de secreto y nunca de proyecto, y sabes que el registro de quién lee cada secreto existe pero hay que activarlo. Conoces el procedimiento de rotación en cinco pasos, con la regla que evita las catástrofes: deshabilitar siempre antes de destruir, y nunca el mismo día; y sabes que Secret Manager no rota nada, solo avisa por Pub/Sub y la automatización la escribes tú. Has elegido entre replicación automática y regional entendiendo que es irreversible y que la residencia del dato es una pregunta de cumplimiento. Y has visto las integraciones que hacen esto todavía más limpio: Cloud Run con --set-secrets, con fichero montado mejor que variable de entorno; el controlador CSI en GKE, que cierra la advertencia de 02-05 sobre el base64 de los Secrets de Kubernetes; y availableSecrets en Cloud Build.

En cifrado, has entendido lo primero que hay que entender: todo está cifrado en reposo por defecto, así que la pregunta real es quién controla la clave. Conoces la jerarquía DEK/KEK, que explica por qué rotar es instantáneo, por qué revocar inutiliza los datos al momento y por qué CMEK es sustituir la KEK y no cifrar tú nada. Tienes la tabla de decisión entre Google-managed, CMEK y CSEK, con criterios y no con dogmas: CMEK cuando necesites revocar, auditar o cumplir; CSEK casi nunca. Has creado el llavero alpinashop-keyring y las claves clave-catalogo y clave-pedidos con rotación a 90 días, sabiendo que llaveros y claves no se borran jamás y que la ubicación debe ser compatible con la del recurso. Has aplicado CMEK al bucket concediendo permiso al agente de servicio —el paso que todo el mundo olvida—, has descubierto que solo afecta a los objetos nuevos, y has aprendido la restricción que cambia los planes: CMEK en Cloud SQL solo se puede establecer al crear la instancia. Y tienes claros los niveles HSM y EXTERNAL, con el precio real de la soberanía: una dependencia externa en el camino crítico de tus datos.

Solo falta un trayecto por proteger, y es el más visible de todos: el que va del navegador del cliente a alpinashop-lb-ip. Hoy AlpinaShop se sirve por una dirección IP numérica y sin certificado; nadie compra una mochila de 200 euros en un sitio con un candado tachado. En la siguiente lección, 03-07, Cloud DNS, certificados TLS y publicación segura de servicios, cerramos el módulo: crearemos la zona pública de alpinashop.example, apuntaremos el dominio a la IP global, aprovisionaremos un certificado gestionado por Google —con sus requisitos de DNS y esa espera en PROVISIONING que desespera a todo el mundo—, ajustaremos la política SSL y las cabeceras de seguridad de la aplicación, y repasaremos con una lista de comprobación todo lo que hemos construido en el módulo 3.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados