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 ungcloud 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
.envdel 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
- El problema concreto: la contraseña de
app_catalogo - Secret Manager: modelo de secretos y versiones
- Crear los secretos de AlpinaShop
- Acceder desde la aplicación Flask
- Permisos por secreto: el mínimo privilegio aplicado
- Rotación: desactivar en lugar de destruir
- Replicación automática frente a regional: residencia del dato
- Integración con Cloud Run, GKE y Cloud Build
- Cifrado en Google Cloud: qué ocurre por defecto
- La jerarquía de claves: DEK y KEK
- Google-managed, CMEK y CSEK: tabla de decisión
- Cloud KMS: llavero, claves y rotación
- HSM y External Key Manager
- Aplicar CMEK al bucket y a Cloud SQL
- Cifrado en tránsito
- Qué NO hacer nunca
- El problema concreto: la contraseña de
app_catalogo
app_catalogoAsí 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
- 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.
latestes 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.
- 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=-conprintfy sin salto de línea final. Si usasecho, añades un\nal 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=ficheroy borra el fichero después conshred -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_historyy 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
- 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:
- Se lee en el arranque, no en cada petición. El coste y la latencia de una llamada por petición son inaceptables.
- 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ón3— es más determinista pero obliga a desplegar código para rotar. Para AlpinaShop,latestes la elección correcta; en un sistema donde un secreto equivocado sea catastrófico, la versión fija tiene su argumento. - 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.
- 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=yamlLa 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.
- 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-catalogoEl 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:
- 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-west4Existe 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.
- 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 |
Sí: 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
- 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.
- 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:
- Los datos se trocean en fragmentos y cada fragmento se cifra con una DEK propia y distinta.
- La DEK no se guarda en claro: se cifra ("envuelve") con una KEK y se almacena envuelta junto al fragmento.
- 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.
- 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 | 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 | Sí: inutiliza los datos al instante | Dejando de enviarla |
| Auditoría de uso de la clave | No | Sí, 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.
- 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-west1Cuatro cosas que hay que saber de KMS y que no son evidentes:
- La ubicación de la clave debe ser compatible con la del recurso. Una clave en
europe-west1para un bucket eneurope-west1: correcto. Para un bucket multirregiónEUhace falta una clave en la ubicacióneurope. Si no coinciden, la operación falla con un error poco claro. - 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.
- 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.
- 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.
- 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=hsmExternal 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.
- 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-pedidosa 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 readqué la está usando, y ten un procedimiento escrito de recuperación.
- 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.
- 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 unconfig.py, ni en unterraform.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. Usagit-secretso 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 elDockerfilequedan en la capa de la imagen y son legibles por cualquiera que la descargue deeurope-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
echoen lugar deprintfal 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
secretAccessora 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.
disablees reversible;destroyno. 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
printde 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, nopassword-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:
- Las 60 GB de imágenes de producto en
alpinashop-catalogo. - La base de datos
alpinashop-pedidos, con nombres, direcciones y últimos cuatro dígitos de tarjeta. - Las exportaciones CSV para Lucía en
gs://alpinashop-catalogo/exportaciones/. - Los discos de las instancias del MIG, que solo contienen el código de la aplicación.
- 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 --quietQué 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
- 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.
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.- 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.
- 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.
- 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=1Prioridad 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
- ¿Qué es Google Cloud Platform?
- Configuración de tu cuenta de GCP
- Descripción general de la consola de GCP
- Proyectos, jerarquía de recursos y facturación
- Regiones, zonas y modelo de responsabilidad compartida
- Cloud Shell y la CLI de gcloud
Módulo 2: Servicios principales de GCP
- Compute Engine: máquinas virtuales en Google Cloud
- Cloud Storage: almacenamiento de objetos
- Cloud SQL: bases de datos relacionales gestionadas
- App Engine: plataforma como servicio
- Google Kubernetes Engine (GKE)
- Bases de datos NoSQL: Firestore, Bigtable y Spanner
- Cómo elegir el servicio de cómputo adecuado
Módulo 3: Redes y seguridad
- Redes VPC
- Balanceo de carga en la nube
- Cloud CDN
- Gestión de identidad y acceso (IAM)
- Cloud Armor
- Secretos y cifrado: Secret Manager y Cloud KMS
- Cloud DNS, certificados TLS y publicación segura de servicios
Módulo 4: Datos y análisis
- BigQuery: el almacén de datos analítico
- Cloud Dataflow: procesamiento de datos por lotes y en streaming
- Cloud Dataproc: Spark y Hadoop gestionados
- Cloud Pub/Sub: mensajería asíncrona
- Cloud Data Fusion: integración de datos sin código
- Orquestación de pipelines con Cloud Composer y Workflows
- Gobierno del dato y cuadros de mando con Dataplex y Looker Studio
Módulo 5: Aprendizaje automático e IA
- Vertex AI: la plataforma de machine learning de GCP
- AutoML: modelos a medida sin escribir código
- TensorFlow en GCP: entrenamiento y servicio de modelos
- API de lenguaje natural
- API de visión
- IA generativa en Vertex AI: modelos Gemini y embeddings
- MLOps: del modelo al producto con Vertex AI Pipelines
Módulo 6: DevOps y monitoreo
- Cloud Build: integración continua en GCP
- Cloud Source Repositories y gestión del código fuente
- Cloud Functions: funciones sin servidor
- Cloud Monitoring (antes Stackdriver): métricas, paneles y alertas
- Cloud Deployment Manager e infraestructura como código nativa
- Cloud Logging y Cloud Trace: logs, trazas y diagnóstico
- Terraform en GCP: infraestructura como código en la práctica
Módulo 7: Temas avanzados de GCP
- Híbrido y multinube con Anthos
- Computación sin servidor con Cloud Run
- Redes avanzadas: VPC compartida, peering y conectividad híbrida
- Mejores prácticas de seguridad
- Gestión y optimización de costos
- Fiabilidad: SLO, alta disponibilidad y recuperación ante desastres
- Gobierno a escala: organización, políticas y auditoría
