Lab práctico · Semana 7: Seguridad en Google Cloud: identidad, perímetros, cifrado y cadena de suministro

Cloud KMS con CMEK en un bucket y Secret Manager

⏱ 60-75 minDificultad: mediaApartados: 3.1

Qué vas a construir

Un llavero y una llave de Cloud KMS en Madrid, un bucket de Cloud Storage cifrado por defecto con esa llave (CMEK), una rotación de la llave y un secreto en Secret Manager que solo puede leer una cuenta de servicio dedicada, a la que accederás por impersonación y desde Cloud Run.

flowchart LR
  KMS["Cloud KMS: kr-lab14 / llave-bucket"] -->|"cifra DEK"| GCS[("Bucket CMEK")]
  SA["Cuenta de servicio app-lab14"] -->|"secretAccessor"| SM["Secret Manager: db-password"]
  CR["Cloud Run (corre como app-lab14)"] -->|"variable de entorno"| SM
  TU["Tú (impersonación)"] -.->|"token de corta duración"| SA

Antes de empezar

  • Haber hecho lab-01-cuenta-y-presupuesto (proyecto con facturación y alerta de presupuesto).
  • Abre Cloud Shell y usa un proyecto dedicado a este lab.
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
export REGION=europe-southwest1
export KEYRING=kr-lab14
export KEY=llave-bucket
export BUCKET=gs://${PROJECT_ID}-cmek-lab14
export SA=app-lab14@${PROJECT_ID}.iam.gserviceaccount.com

gcloud services enable cloudkms.googleapis.com secretmanager.googleapis.com \
  storage.googleapis.com run.googleapis.com iamcredentials.googleapis.com

iamcredentials.googleapis.com es la API que emite los tokens de corta duración cuando impersonas una cuenta de servicio.

Paso 1: crear el llavero y la llave

El key ring fija la ubicación; las llaves que contiene no pueden cambiar de región. Creamos una llave simétrica de cifrado con rotación automática cada 90 días.

gcloud kms keyrings create $KEYRING --location=$REGION

gcloud kms keys create $KEY \
  --keyring=$KEYRING --location=$REGION \
  --purpose=encryption \
  --protection-level=software \
  --rotation-period=90d \
  --next-rotation-time=$(date -u -d "+90 days" +%Y-%m-%dT%H:%M:%SZ)

export KEY_ID=projects/$PROJECT_ID/locations/$REGION/keyRings/$KEYRING/cryptoKeys/$KEY
gcloud kms keys versions list --key=$KEY --keyring=$KEYRING --location=$REGION

En la consola: Security → Key Management → Create key ring. Fíjate en que no hay botón para borrar llaveros ni llaves: solo se destruyen versiones.

Paso 2: autorizar a Cloud Storage y crear el bucket CMEK

Quien cifra y descifra no eres tú, sino el service agent de Cloud Storage de tu proyecto. Hay que darle roles/cloudkms.cryptoKeyEncrypterDecrypter sobre la llave. El comando gcloud storage service-agent --authorize-cmek lo hace por ti:

gcloud storage service-agent --project=$PROJECT_ID --authorize-cmek=$KEY_ID

gcloud storage buckets create $BUCKET \
  --location=$REGION \
  --uniform-bucket-level-access \
  --default-encryption-key=$KEY_ID

echo "dato confidencial de prueba" > secreto.txt
gcloud storage cp secreto.txt $BUCKET/
gcloud storage objects describe $BUCKET/secreto.txt --format="default(kms_key)"

La última orden debe devolver la ruta de la llave con su versión (.../cryptoKeyVersions/1). El objeto está cifrado con una DEK, y esa DEK con tu llave.

Paso 3: rotar la llave y comprobar el efecto

Creamos una versión nueva y la hacemos primaria. Lo nuevo se cifrará con ella; lo antiguo sigue usando la versión 1.

gcloud kms keys versions create --key=$KEY --keyring=$KEYRING --location=$REGION --primary

echo "dato posterior a la rotación" > nuevo.txt
gcloud storage cp nuevo.txt $BUCKET/
gcloud storage objects describe $BUCKET/secreto.txt --format="default(kms_key)"
gcloud storage objects describe $BUCKET/nuevo.txt --format="default(kms_key)"

Verás cryptoKeyVersions/1 en el primer objeto y cryptoKeyVersions/2 en el segundo: la rotación no recifra lo existente. Para recifrar un objeto antiguo con la versión primaria, reescríbelo:

gcloud storage objects update $BUCKET/secreto.txt --encryption-key=$KEY_ID
gcloud storage objects describe $BUCKET/secreto.txt --format="default(kms_key)"

Ahora prueba el efecto de desactivar la versión primaria (reversible):

gcloud kms keys versions disable 2 --key=$KEY --keyring=$KEYRING --location=$REGION
gcloud storage cat $BUCKET/nuevo.txt   # falla: la versión que protege la DEK está desactivada
gcloud kms keys versions enable 2 --key=$KEY --keyring=$KEYRING --location=$REGION
gcloud storage cat $BUCKET/nuevo.txt   # vuelve a funcionar (puede tardar unos segundos)

Esto es el crypto-shredding: quien controla la llave controla el acceso a los datos, aunque tenga permisos de Cloud Storage.

Paso 4: crear un secreto en Secret Manager

Replicación gestionada por el usuario en Madrid (residencia del dato). El valor se lee de la entrada estándar para que no quede en el historial del shell.

printf "S3cr3t0-de-prueba" | gcloud secrets create db-password \
  --replication-policy=user-managed --locations=$REGION --data-file=-

gcloud secrets versions list db-password
gcloud secrets versions access latest --secret=db-password

Tú puedes leerlo porque eres Owner del proyecto. Ahora vamos a hacerlo con mínimo privilegio.

Paso 5: cuenta de servicio con acceso solo a ese secreto

gcloud iam service-accounts create app-lab14 --display-name="App del lab 14"

# Solo puede leer ESTE secreto (binding a nivel de secreto, no de proyecto)
gcloud secrets add-iam-policy-binding db-password \
  --member="serviceAccount:$SA" --role="roles/secretmanager.secretAccessor"

# Tú puedes impersonarla (tokens de corta duración, sin claves)
gcloud iam service-accounts add-iam-policy-binding $SA \
  --member="user:$(gcloud config get-value account)" \
  --role="roles/iam.serviceAccountTokenCreator"

Accede al secreto como la cuenta de servicio (la propagación de IAM puede tardar uno o dos minutos):

gcloud secrets versions access latest --secret=db-password --impersonate-service-account=$SA
gcloud storage ls $BUCKET --impersonate-service-account=$SA   # debe fallar con 403

La segunda orden falla: la cuenta solo tiene permiso sobre el secreto. Mínimo privilegio comprobado.

Paso 6 (opcional): consumir el secreto desde Cloud Run

Cloud Run puede inyectar el secreto como variable de entorno y resolverlo en el arranque usando la identidad del servicio. Desplegamos la imagen pública de ejemplo corriendo como app-lab14:

gcloud run deploy demo-secretos \
  --image=us-docker.pkg.dev/cloudrun/container/hello \
  --region=$REGION \
  --service-account=$SA \
  --set-secrets=DB_PASSWORD=db-password:latest \
  --no-allow-unauthenticated

gcloud run services describe demo-secretos --region=$REGION \
  --format="yaml(spec.template.spec.containers[0].env)"

El despliegue solo termina bien si la cuenta de servicio puede leer el secreto (prueba a quitarle el rol y redesplegar: fallará). En el YAML verás que la variable apunta al secreto (secretKeyRef), no contiene el valor. Para producción, fija una versión concreta en lugar de latest o usa el montaje como volumen, que sí refleja versiones nuevas sin redesplegar.

Comprueba que funciona

  • gcloud storage objects describe muestra kms_key con tu llave en ambos objetos.
  • Con la versión 2 desactivada, gcloud storage cat falla aunque seas Owner.
  • La cuenta app-lab14 lee el secreto, pero no el bucket.
  • En Logging → Logs Explorer, filtra por protoPayload.serviceName="secretmanager.googleapis.com": verás los accesos (los de lectura de datos aparecen solo si activas los Data Access logs de Secret Manager).

Limpieza

Los llaveros y las llaves no se pueden borrar, pero las versiones destruidas no se facturan. Programa su destrucción y borra el resto:

gcloud run services delete demo-secretos --region=$REGION --quiet
gcloud storage rm --recursive $BUCKET
gcloud secrets delete db-password --quiet
gcloud iam service-accounts delete $SA --quiet

for v in $(gcloud kms keys versions list --key=$KEY --keyring=$KEYRING --location=$REGION \
    --filter="state=ENABLED OR state=DISABLED" --format="value(name.basename())"); do
  gcloud kms keys versions destroy $v --key=$KEY --keyring=$KEYRING --location=$REGION --quiet
done
rm -f secreto.txt nuevo.txt

Las versiones quedan 30 días en scheduled for destruction. Si el proyecto era solo para este lab, también puedes borrarlo entero con gcloud projects delete $PROJECT_ID.

Preguntas para pensar como arquitecto

¿Por qué en una empresa real la llave debería estar en otro proyecto distinto del bucket?

Por separación de funciones: el equipo de seguridad administra las llaves (roles/cloudkms.admin) en un proyecto de llaves y los equipos de aplicación solo tienen cryptoKeyEncrypterDecrypter a través de sus service agents. Así nadie puede a la vez leer los datos y destruir o cambiar la llave, y la auditoría de uso de llaves queda centralizada.

Un auditor exige que los datos de hace tres años queden cifrados con la versión actual de la llave. ¿Basta con rotar?

No. La rotación solo afecta a lo que se cifre a partir de entonces. Hay que reescribir (recifrar) los datos existentes, por ejemplo con gcloud storage objects update --encryption-key o un proceso por lotes, y después, si procede, destruir las versiones antiguas cuando ya nada dependa de ellas.

¿Qué diferencia hay entre desactivar una versión y destruirla?

Desactivar es inmediato y reversible: los datos quedan inaccesibles hasta que la reactives. Destruir se programa (30 días por defecto) y, una vez ejecutado, es irreversible: los datos cifrados con esa versión se pierden para siempre. Por eso se recomienda desactivar primero y observar antes de destruir.

¿Por qué no guardar la contraseña de la base de datos en Cloud KMS?

Cloud KMS no devuelve material secreto: cifra y descifra con llaves que nunca salen del servicio. Una contraseña hay que recuperarla para usarla, y eso es lo que hace Secret Manager, con versiones, IAM por secreto, replicación controlada y auditoría. Cloud KMS puede, eso sí, cifrar el secreto de Secret Manager mediante CMEK.


Volver al módulo