Todo lo que AlpinaShop ha construido en siete módulos funciona porque tres personas se acuerdan. Marta se acuerda de no crear VM con IP pública. Dani se acuerda de etiquetar los recursos. Lucía se acuerda de no exportar datos a un bucket externo. Los SLO viven en un panel que alguien mantiene, los runbooks se actualizan porque alguien los revisa, y la política de presupuesto de error se respeta porque el equipo la acordó una tarde.
Nada de eso está mal. Todo eso es frágil.
Porque hoy, técnicamente, nada impide que mañana alguien cree una máquina con IP pública en Iowa, desactive un log de auditoría, comparta un bucket con allUsers, o descargue una clave JSON de una cuenta de servicio. Las buenas prácticas del curso son acuerdos, no controles. Y un acuerdo se rompe el día que entra una persona nueva, el día que alguien tiene prisa, o el día que la empresa pasa de tres personas técnicas a ocho.
Gobierno es exactamente eso: convertir los acuerdos en controles que se aplican solos, y tener registro fiable de todo lo que ocurre. Es la diferencia entre "confiamos en que nadie lo haga" y "no se puede hacer".
Y hay un malentendido que conviene deshacer desde la primera línea: el gobierno no es algo que se implanta cuando se es grande. Se implanta antes, porque retrofitear políticas sobre una organización con cincuenta proyectos y mil recursos es una obra de meses, mientras que aplicarlas sobre cinco proyectos es una tarde. AlpinaShop llega tarde, pero no demasiado tarde. Esa es la única razón por la que esta lección todavía es factible en un día de trabajo.
Al terminar sabrás estructurar una organización y entender las consecuencias de cada criterio, aplicar políticas de organización que se heredan y bloquean de verdad, inventariar todo lo que existe y monitorizar los cambios en tiempo real, activar y leer los logs de auditoría que importan, montar retención inmutable en un proyecto al que ni los administradores puedan escribir, y desplegar todo eso de una vez como una zona de aterrizaje con Terraform.
Y cerrarás el módulo 7 y, con él, el recorrido completo de AlpinaShop.
Contenido
- Qué es el gobierno en la nube y por qué se necesita antes de lo que parece
- La estructura de la organización: criterios y consecuencias
- Políticas de organización: qué son y cómo se heredan
- Las políticas que AlpinaShop aplica, una a una
- Modo de prueba y el peligro de bloquear al equipo
- Restricciones personalizadas y su relación con Policy Controller
- Cuotas y límites a nivel de organización
- Cloud Asset Inventory: saber qué existe
- Feeds: monitorizar cambios en tiempo real
- Cloud Audit Logs de verdad
- Las consultas de auditoría que hay que tener preparadas
- Retención inmutable en un proyecto separado
- Zonas de aterrizaje y el orden que importa
- Gestión del cambio: revisiones, ciclo de vida y excepciones
- La madurez de AlpinaShop, y lo que le queda
- Qué es el gobierno en la nube y por qué se necesita antes de lo que parece
Gobierno es el conjunto de estructuras, políticas y procesos que aseguran que el uso de la nube es coherente con lo que la organización quiere. Cubre cuatro preguntas:
| Pregunta | Mecanismo |
|---|---|
| ¿Qué se puede crear, dónde y cómo? | Políticas de organización |
| ¿Qué existe ahora mismo? | Cloud Asset Inventory |
| ¿Quién hizo qué y cuándo? | Cloud Audit Logs |
| ¿Quién paga qué? | Estructura de proyectos y etiquetas |
Y la razón por la que se necesita antes de "ser grande", que es puramente aritmética:
| Momento | Proyectos | Recursos | Coste de aplicar políticas |
|---|---|---|---|
| Ahora (AlpinaShop) | 5 | ~120 | Un día |
| En dos años | 15 | ~600 | Una semana, con negociaciones |
| En cinco años | 60 | ~5.000 | Meses, y probablemente no se hará |
El coste no crece con el número de recursos: crece con el número de excepciones que hay que negociar. Aplicar hoy la política "prohibido crear VM con IP pública" afecta a cero recursos existentes. Aplicarla dentro de cinco años significa encontrar las cuarenta VM que la incumplen, averiguar por qué, hablar con seis equipos y aceptar veinte excepciones permanentes que vacían la política de contenido.
La regla: las políticas se ponen cuando no molestan a nadie. Después ya no se ponen.
- La estructura de la organización: criterios y consecuencias
La jerarquía de 01-04, revisada ahora con todo el contexto:
flowchart TB
ORG["Organizacion<br/>alpinashop.example"]
ORG --> F1["Carpeta produccion"]
ORG --> F2["Carpeta desarrollo"]
ORG --> F3["Carpeta compartido"]
ORG --> F4["Carpeta seguridad"]
F1 --> P1["alpinashop-prod"]
F2 --> P2["alpinashop-dev"]
F3 --> P3["alpinashop-datos"]
F3 --> P4["alpinashop-cicd"]
F3 --> P5["alpinashop-red"]
F4 --> P6["alpinashop-auditoria"]
style F4 fill:#e6f4ea,stroke:#34a853
style P6 fill:#e6f4ea,stroke:#34a853
Dos cambios respecto a 01-04: el proyecto alpinashop-red de 07-03 vive en compartido, y aparece una carpeta nueva, seguridad, con alpinashop-auditoria — el proyecto de logs inmutables del apartado 12. Está separado a propósito: es el único al que ni Marta puede escribir.
Los tres criterios para estructurar
Rara vez se usa uno solo; lo habitual es combinar dos niveles.
| Criterio | Estructura | Ventaja | Inconveniente |
|---|---|---|---|
| Por entorno | produccion / desarrollo / pruebas |
Políticas muy distintas por entorno; separación de permisos limpia | Un equipo con varias aplicaciones las tiene repartidas |
| Por equipo o unidad | tienda / analitica / corporativo |
Facturación por equipo directa; autonomía | Producción y desarrollo mezclados: mismas políticas para ambos |
| Por aplicación | Una carpeta por producto | Aislamiento máximo | Explosión de carpetas; duplicación |
Consecuencias de la elección, que es lo que casi nunca se piensa antes de decidir:
| Aspecto | Efecto de la estructura |
|---|---|
| Permisos | Se heredan hacia abajo. Un rol en la carpeta produccion aplica a todos sus proyectos |
| Políticas de organización | Se heredan igual. Por eso separar entornos permite políticas más duras en producción |
| Facturación | Se agrega por carpeta y proyecto. La estructura es el desglose de costes |
| Cuotas | Son del proyecto, no de la carpeta. Un proyecto por aplicación aísla el agotamiento de cuota |
| Radio de explosión | El proyecto es la frontera de borrado. Borrar un proyecto se lleva todo lo que hay dentro |
La elección de AlpinaShop es por entorno en el primer nivel, y es la correcta para su tamaño por un motivo concreto: la diferencia de exigencia entre producción y desarrollo es mucho mayor que la diferencia entre equipos. Nadie es owner de producción, pero en desarrollo se puede ser editor sin drama. Con una estructura por equipos, esa distinción sería imposible de expresar.
Cuando AlpinaShop tenga cuatro productos, la evolución natural es un segundo nivel: produccion/tienda, produccion/alpinapro, desarrollo/tienda… La estructura de carpetas se puede cambiar; mover proyectos entre carpetas es un comando. Lo que no se puede cambiar es el projectId.
- Políticas de organización: qué son y cómo se heredan
El Organization Policy Service es lo que convierte los acuerdos en controles.
| IAM | Políticas de organización | |
|---|---|---|
| Responde a | ¿Quién puede hacer qué? | ¿Qué se puede hacer, con independencia de quién? |
| Sujeto | Identidades | Recursos y configuraciones |
| Ejemplo | "Marta puede crear VM" | "Nadie puede crear VM con IP pública" |
| Se salta con | Un rol más alto | Nada. Ni un owner de organización |
La última fila es la esencia: una política de organización no es un permiso. Aunque tengas roles/owner en el proyecto, si la política prohíbe IP públicas, no puedes crear una. Solo puede levantarla quien tenga roles/orgpolicy.policyAdmin en el nivel donde está definida.
Tipos de restricción
| Tipo | Qué hace | Ejemplo |
|---|---|---|
| Booleana | Activa o desactiva un comportamiento | compute.requireOsLogin, storage.uniformBucketLevelAccess |
| De lista | Permite o deniega valores concretos | gcp.resourceLocations con la lista de regiones |
| Personalizada | CEL propio sobre atributos del recurso | "Las VM deben ser de tipo e2 o n2" |
La herencia, que es donde está la sutileza
Las políticas se heredan de organización → carpeta → proyecto, y en cada nivel se puede:
| Acción | Efecto |
|---|---|
| Heredar (por defecto) | Se aplica lo del padre |
Combinar (inheritFromParent: true) |
Lo del padre más lo propio |
Sustituir (inheritFromParent: false) |
Solo lo propio; se ignora al padre |
| Restaurar el valor por defecto | Elimina la política en ese nivel |
flowchart TB
O["Organizacion<br/>regiones: europe-west1, europe-southwest1"] --> F1["Carpeta produccion<br/>hereda"]
O --> F2["Carpeta desarrollo<br/>hereda + anade europe-west4"]
F1 --> P1["alpinashop-prod<br/>solo europe-west1, europe-southwest1"]
F2 --> P2["alpinashop-dev<br/>3 regiones"]
style F1 fill:#e6f4ea,stroke:#34a853
style F2 fill:#fef7e0,stroke:#fbbc04
Y la regla que sorprende a todo el mundo: para las restricciones de lista, una denegación en cualquier nivel gana siempre. Un hijo no puede permitir lo que el padre deniega. Es lo que hace que el modelo sea seguro: no se puede escapar de una política hacia abajo.
Consecuencia práctica: las políticas se definen en el nivel más alto donde tengan sentido, y se relajan hacia abajo solo con
allowadicionales sobre restricciones que el padre no denegó explícitamente. Si te encuentras necesitando "des-denegar" algo, la política estaba mal puesta.
- Las políticas que AlpinaShop aplica, una a una
Estas son las nueve, con su motivo, su nivel y su riesgo.
4.1 Prohibir IP públicas en las VM
gcloud resource-manager org-policies enable-enforce \
constraints/compute.vmExternalIpAccess \
--organization=ORG_IDCon la excepción declarada, si alguna VM legítima la necesitara:
# politica-ip-externa.yaml
constraint: constraints/compute.vmExternalIpAccess
listPolicy:
deniedValues:
- "under:organizations/ORG_ID"
# Excepcion explicita y documentada, si hiciera falta:
# allowedValues:
# - "projects/alpinashop-dev/zones/europe-west1-b/instances/bastion-pruebas"Motivo: en AlpinaShop no queda ninguna VM en la ruta de servicio (07-02), y las de desarrollo salen por Cloud NAT. Una IP pública es superficie de ataque directa desde internet. Nivel: organización. Riesgo de bloqueo: bajo.
4.2 Restringir las regiones a la UE
constraint: constraints/gcp.resourceLocations
listPolicy:
allowedValues:
- in:europe-west1-locations
- in:europe-southwest1-locations
- in:eu-locations # multirregion EU: BigQuery, bucketsMotivo: residencia del dato en la UE (07-01, 07-04) y control de costes de tráfico intercontinental (07-05).
Nivel: organización. Riesgo: medio-alto, y es la política que más disgustos da. Muchos servicios tienen componentes globales o multirregión, y in:eu-locations es imprescindible para que BigQuery multirregión y algunos buckets sigan funcionando. Esta se prueba en dry-run sí o sí.
4.3 Desactivar la creación de claves de cuentas de servicio
gcloud resource-manager org-policies enable-enforce \
constraints/iam.disableServiceAccountKeyCreation \
--organization=ORG_IDMotivo: es la deuda #1 de 07-04. Con federación de identidad e impersonación ya en uso, no hay ningún caso legítimo que necesite una clave descargable. Nivel: organización. Riesgo: medio. Puede romper integraciones antiguas — por eso primero el inventario del apartado 8, después la política.
Y su compañera, que impide que una clave existente valga eternamente:
constraint: constraints/iam.serviceAccountKeyExpiryHours
listPolicy:
allowedValues:
- "24h" # si excepcionalmente se permite una, caduca en 24 horas4.4 Exigir OS Login
gcloud resource-manager org-policies enable-enforce \
constraints/compute.requireOsLogin \
--organization=ORG_IDMotivo: OS Login vincula el acceso SSH a la identidad de Google en lugar de a claves SSH sueltas en los metadatos. Eso significa que dar de baja a una persona en el directorio le quita el acceso a todas las máquinas, que es exactamente lo que no ocurre con las claves en metadatos. Nivel: organización. Riesgo: bajo (apenas quedan VM).
4.5 Prohibir buckets públicos
gcloud resource-manager org-policies enable-enforce \
constraints/storage.publicAccessPrevention \
--organization=ORG_IDMotivo: un bucket público es la fuga de datos más común de la nube, en todos los proveedores.
Nivel: organización. Riesgo: bajo… con una trampa que hay que resolver antes: el bucket alpinashop-catalogo sirve imágenes a la tienda. Si esas imágenes se sirven a través del balanceador con bb-catalogo-imagenes (03-02), el bucket no necesita ser público y la política no rompe nada. Si alguien las estuviera sirviendo por URL directa de Cloud Storage, sí. Comprobarlo es parte del trabajo previo.
4.6 Acceso uniforme a nivel de bucket
gcloud resource-manager org-policies enable-enforce \
constraints/storage.uniformBucketLevelAccess \
--organization=ORG_IDMotivo: desactiva las ACL por objeto, que son un sistema de permisos paralelo a IAM, invisible en las auditorías y responsable de innumerables fugas. Con acceso uniforme, IAM es la única verdad.
4.7 Requerir CMEK y limitar los proyectos de clave
# Exigir clave gestionada por el cliente en los servicios que lo admiten
constraint: constraints/gcp.restrictNonCmekServices
listPolicy:
deniedValues:
- bigquery.googleapis.com
- sqladmin.googleapis.com
- storage.googleapis.com# Y que la clave venga SOLO de nuestro keyring
constraint: constraints/gcp.restrictCmekCryptoKeyProjects
listPolicy:
allowedValues:
- projects/alpinashop-prod # donde vive alpinashop-keyringMotivo: cierra el control de 03-06. La segunda política es la que casi nadie pone y evita que alguien cifre datos con una clave de un proyecto que no controlas.
Nivel: carpeta produccion. Riesgo: alto. Exigir CMEK rompe la creación de recursos si la clave no tiene los permisos correctos concedidos al agente de servicio correspondiente. dry-run obligatorio.
4.8 Impedir concesiones automáticas a las cuentas por defecto
gcloud resource-manager org-policies enable-enforce \
constraints/iam.automaticIamGrantsForDefaultServiceAccounts \
--organization=ORG_IDMotivo: es la deuda #2 de 07-04. Impide que los proyectos nuevos nazcan con la cuenta por defecto de Compute con rol Editor. No arregla los proyectos existentes, que hay que corregir a mano.
4.9 Restringir el uso compartido por dominio
constraint: constraints/iam.allowedPolicyMemberDomains
listPolicy:
allowedValues:
- "C03xxxxxx" # ID de cliente de Cloud Identity de alpinashop.exampleMotivo: impide conceder permisos a cuentas de Google ajenas a la organización. Es la política que evita el error de dar acceso a [email protected] "temporalmente".
Riesgo: alto. Rompe cualquier colaboración legítima con externos, y rompe algunos agentes de servicio de Google. Requiere excepciones cuidadosas y dry-run largo.
Resumen
| # | Restricción | Nivel | Riesgo | Deuda que cierra |
|---|---|---|---|---|
| 1 | compute.vmExternalIpAccess |
Organización | Bajo | — |
| 2 | gcp.resourceLocations |
Organización | Medio-alto | Residencia UE |
| 3 | iam.disableServiceAccountKeyCreation |
Organización | Medio | 07-04 #1 |
| 4 | compute.requireOsLogin |
Organización | Bajo | — |
| 5 | storage.publicAccessPrevention |
Organización | Bajo | — |
| 6 | storage.uniformBucketLevelAccess |
Organización | Bajo | — |
| 7 | gcp.restrictNonCmekServices + proyectos de clave |
Carpeta produccion |
Alto | Cierra 03-06 |
| 8 | iam.automaticIamGrants... |
Organización | Bajo | 07-04 #2 |
| 9 | iam.allowedPolicyMemberDomains |
Organización | Alto | — |
- Modo de prueba y el peligro de bloquear al equipo
Las políticas de organización admiten modo de prueba (dry run), igual que Cloud Armor (03-05), Policy Controller (07-01) y VPC Service Controls (07-03). Es el mismo patrón por cuarta vez en el curso, y no es casualidad: todo control que deniega debe medirse antes de aplicarse.
# Aplicar SOLO en modo de prueba
gcloud org-policies set-policy politica-regiones.yaml --update-mask=dryRunSpec# politica-regiones.yaml
name: organizations/ORG_ID/policies/gcp.resourceLocations
dryRunSpec:
rules:
- values:
allowedValues:
- in:europe-west1-locations
- in:europe-southwest1-locations
- in:eu-locationsY la consulta de lo que habría bloqueado:
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS quien,
protopayload_auditlog.methodName AS operacion,
protopayload_auditlog.resourceName AS recurso,
protopayload_auditlog.metadata.dryRun AS en_pruebas,
protopayload_auditlog.metadata.constraint AS restriccion
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_policy`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 14 DAY)
AND protopayload_auditlog.metadata.dryRun = TRUE
ORDER BY timestamp DESCEl procedimiento seguro, y el orden importa
| Paso | Qué | Duración |
|---|---|---|
| 1 | Inventariar lo que ya incumple (apartado 8) | 1 h |
| 2 | Corregir o documentar cada incumplimiento | Variable |
| 3 | Aplicar en dryRunSpec |
10 min |
| 4 | Esperar 14 días, cubriendo procesos semanales y mensuales | 14 días |
| 5 | Revisar violaciones y decidir excepciones | 1 h |
| 6 | Aplicar de verdad, empezando por alpinashop-dev |
10 min |
| 7 | Una semana en desarrollo sin incidencias | 7 días |
| 8 | Aplicar en producción | 10 min |
El paso 1 es el que se salta todo el mundo y el que evita el desastre. Aplicar dry-run sobre políticas que ya se incumplen genera cientos de violaciones que nadie va a analizar, y el resultado práctico es que se ignoran todas.
Las cinco formas de bloquearse a uno mismo
Reales, todas vistas en producción:
| Error | Consecuencia | Prevención |
|---|---|---|
allowedPolicyMemberDomains sin excepciones para los agentes de servicio de Google |
Servicios internos dejan de funcionar de formas inexplicables | dry-run largo; lista de agentes |
resourceLocations sin in:eu-locations |
BigQuery multirregión y algunos buckets dejan de crearse | Incluir siempre las multirregión que uses |
disableServiceAccountKeyCreation con integraciones antiguas vivas |
Un sistema externo deja de autenticarse sin aviso previo | Inventariar claves primero |
| CMEK exigido sin permisos al agente de servicio sobre la clave | Los recursos nuevos no se pueden crear | Conceder cloudkms.cryptoKeyEncrypterDecrypter antes |
| Política aplicada un viernes | El fin de semana entero sin poder desplegar | Aplicar martes o miércoles por la mañana |
Y la salvaguarda que hay que preparar antes de todo esto: que al menos dos personas tengan
roles/orgpolicy.policyAdminen la organización, con acceso verificado. Si la única persona que puede levantar una política se bloquea a sí misma, la salida pasa por soporte de Google y por un mal día.
- Restricciones personalizadas y su relación con Policy Controller
Cuando ninguna restricción predefinida sirve, se escribe una restricción personalizada con CEL (Common Expression Language):
# restriccion-tipos-maquina.yaml
name: organizations/ORG_ID/customConstraints/custom.tiposMaquinaPermitidos
resourceTypes:
- compute.googleapis.com/Instance
methodTypes:
- CREATE
- UPDATE
condition: "resource.machineType.contains('e2-') || resource.machineType.contains('n2-')"
actionType: ALLOW
displayName: "Solo familias de maquina e2 y n2"
description: "Evita familias caras o especializadas sin justificacion (07-05)."gcloud org-policies set-custom-constraint restriccion-tipos-maquina.yaml
gcloud resource-manager org-policies enable-enforce \
custom.tiposMaquinaPermitidos --organization=ORG_IDOtro ejemplo, esta vez para las etiquetas obligatorias de 07-05:
name: organizations/ORG_ID/customConstraints/custom.etiquetasObligatorias
resourceTypes:
- compute.googleapis.com/Instance
- storage.googleapis.com/Bucket
methodTypes: [CREATE]
condition: "has(resource.labels['centro-coste']) && has(resource.labels['entorno'])"
actionType: ALLOW
displayName: "Etiquetas centro-coste y entorno obligatorias"Políticas de organización frente a Policy Controller
Dos sistemas que hacen cosas parecidas en ámbitos distintos. La confusión es habitual:
| Políticas de organización | Policy Controller (07-01) | |
|---|---|---|
| Ámbito | Recursos de Google Cloud | Recursos de Kubernetes |
| Dónde actúa | API de Google Cloud | Webhook de admisión del clúster |
| Lenguaje | CEL | Rego (OPA/Gatekeeper) |
| Alcance | Toda la organización | Los clústeres de la flota |
| Ejemplo | "Ninguna VM con IP pública" | "Ningún pod privilegiado" |
| Coste | Incluido | Nivel base de GKE / GKE Enterprise |
No compiten: se complementan. Una política de organización no puede impedir que un pod se ejecute como root, y Policy Controller no puede impedir que se cree un bucket público. Una organización con Kubernetes necesita las dos.
Para AlpinaShop, que retiró las cargas de producción de GKE en 07-02, las políticas de organización son las que importan. Policy Controller queda como control del clúster de pruebas, que es coherente con lo decidido en DA-004.
- Cuotas y límites a nivel de organización
Las cuotas se gestionan por proyecto, pero se pueden gobernar desde arriba mediante políticas y automatización.
| Palanca | Qué hace |
|---|---|
| Ajuste de cuotas por proyecto | Bajar la cuota de vCPU en desarrollo (07-05) |
compute.quotaOverrides |
Restringir familias de máquinas caras |
| Cuotas de coste de BigQuery | Bytes por usuario y día |
| Cuotas de tasa de API | Contener bucles descontrolados |
# Bajar la cuota de CPU en desarrollo: freno DURO al gasto
gcloud alpha services quota update \
--service=compute.googleapis.com \
--consumer=projects/alpinashop-dev \
--metric=compute.googleapis.com/cpus \
--unit=1/{project}/{region} \
--dimensions=region=europe-west1 \
--value=8La distinción esencial, ya vista en 07-05 y que aquí se coloca en su sitio: los presupuestos avisan, las cuotas impiden. En una organización gobernada hacen falta los dos, y las cuotas son el único mecanismo que no depende de que alguien reaccione.
- Cloud Asset Inventory: saber qué existe
No se puede gobernar lo que no se sabe que existe. Cloud Asset Inventory es el catálogo de todos los recursos, políticas IAM y configuraciones de la organización, con su historial.
Buscar recursos
# Todo lo que existe en la organizacion
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--format="table(name, assetType, location, project)"
# VM con IP publica: el inventario PREVIO a la politica 4.1
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--asset-types=compute.googleapis.com/Instance \
--query="natIP:*" \
--format="table(name, project, location)"
# Recursos fuera de Europa: el inventario previo a la politica 4.2
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--query="NOT location:europe AND NOT location:eu AND NOT location:global" \
--format="table(name, assetType, location, project)"
# Recursos SIN etiqueta de centro de coste (07-05)
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--query="NOT labels.centro-coste:*" \
--format="table(name, assetType, project)"Buscar políticas IAM
# Quien tiene owner en cualquier sitio de la organizacion
gcloud asset search-all-iam-policies --scope=organizations/ORG_ID \
--query="policy:roles/owner" \
--format="table(resource, policy.bindings.members)"
# Cualquier cosa concedida a una cuenta externa
gcloud asset search-all-iam-policies --scope=organizations/ORG_ID \
--query="policy:gmail.com" \
--format="table(resource, policy.bindings.role, policy.bindings.members)"Estas dos consultas, ejecutadas antes de aplicar las políticas 4.1, 4.2 y 4.9, son exactamente el paso 1 del procedimiento del apartado 5. Sin ellas, se aplica a ciegas.
Exportar a BigQuery
gcloud asset export --organization=ORG_ID \
--content-type=resource \
--bigquery-table=projects/alpinashop-auditoria/datasets/inventario/tables/recursos \
--output-bigquery-force
gcloud asset export --organization=ORG_ID \
--content-type=iam-policy \
--bigquery-table=projects/alpinashop-auditoria/datasets/inventario/tables/politicas_iam \
--output-bigquery-forceCon export diario programado (Cloud Scheduler + Workflows, 04-06), se obtiene algo muy valioso: el histórico del inventario, que permite responder a "¿qué había el 3 de junio?" — una pregunta que aparece en toda investigación y en toda auditoría.
-- Recursos creados en los ultimos 7 dias, por tipo y proyecto
SELECT
asset_type,
SPLIT(name, '/')[SAFE_OFFSET(4)] AS proyecto,
COUNT(*) AS nuevos
FROM `alpinashop-auditoria.inventario.recursos`
WHERE DATE(update_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
GROUP BY asset_type, proyecto
ORDER BY nuevos DESC
- Feeds: monitorizar cambios en tiempo real
El inventario dice qué hay. Los feeds avisan cuando algo cambia, en el momento.
# 1. Topic al que llegaran las notificaciones
gcloud pubsub topics create cambios-criticos --project=alpinashop-auditoria
# 2. Feed sobre cambios de politicas IAM en TODA la organizacion
gcloud asset feeds create feed-iam \
--organization=ORG_ID \
--content-type=iam-policy \
--asset-types="cloudresourcemanager.googleapis.com/Project,cloudresourcemanager.googleapis.com/Folder" \
--pubsub-topic=projects/alpinashop-auditoria/topics/cambios-criticos
# 3. Feed sobre creacion o cambio de recursos sensibles
gcloud asset feeds create feed-recursos-sensibles \
--organization=ORG_ID \
--content-type=resource \
--asset-types="compute.googleapis.com/Firewall,storage.googleapis.com/Bucket,iam.googleapis.com/ServiceAccountKey" \
--pubsub-topic=projects/alpinashop-auditoria/topics/cambios-criticosY la función que decide qué merece un aviso inmediato (Cloud Functions 2ª gen, 06-03):
# main.py — evaluar-cambio-critico
import base64, json, logging
CRITICOS = [
("roles/owner", "Se ha concedido OWNER"),
("roles/editor", "Se ha concedido EDITOR"),
("allUsers", "ACCESO PUBLICO concedido"),
("allAuthenticatedUsers", "ACCESO SEMIPUBLICO concedido"),
("roles/iam.securityAdmin", "Se ha concedido SECURITY ADMIN"),
]
def evaluar_cambio(evento, contexto):
datos = json.loads(base64.b64decode(evento["data"]).decode("utf-8"))
activo = datos.get("asset", {})
nombre = activo.get("name", "")
tipo = activo.get("assetType", "")
# 1. Creacion de una clave de cuenta de servicio: SIEMPRE excepcional (07-04)
if tipo == "iam.googleapis.com/ServiceAccountKey":
alertar("CRITICAL", "Clave de cuenta de servicio creada", nombre, datos)
return
# 2. Concesiones peligrosas
texto = json.dumps(activo.get("iamPolicy", {}))
for patron, mensaje in CRITICOS:
if patron in texto:
alertar("CRITICAL", mensaje, nombre, datos)
return
# 3. Regla de firewall abierta a todo internet en un puerto de administracion
if tipo == "compute.googleapis.com/Firewall":
r = activo.get("resource", {}).get("data", {})
rangos = r.get("sourceRanges", [])
if "0.0.0.0/0" in rangos and r.get("direction") == "INGRESS":
for permitido in r.get("allowed", []):
puertos = permitido.get("ports", [])
if any(p in ("22", "3389", "3306", "5432") for p in puertos):
alertar("CRITICAL", "Firewall abierto a internet en puerto sensible",
nombre, datos)
return
def alertar(gravedad, mensaje, recurso, datos):
logging.log_struct({
"severity": gravedad,
"message": mensaje,
"recurso": recurso,
"momento": datos.get("window", {}).get("startTime"),
"alerta_gobierno": True,
})Con una alerta de Cloud Monitoring sobre jsonPayload.alerta_gobierno=true y gravedad CRITICAL, esos eventos llegan al canal de seguridad en segundos.
Esto cierra la deuda #4 de 07-04 —alertas de cambios de IAM y creación de claves— y es la diferencia entre enterarse de un cambio peligroso en el momento o descubrirlo en la revisión trimestral, seis semanas después.
- Cloud Audit Logs de verdad
Se han mencionado en 03-04, 06-06 y 07-04. Aquí, completos.
| Tipo | Qué registra | ¿Por defecto? | Coste | Se puede desactivar |
|---|---|---|---|---|
| Actividad de administrador | Cambios de configuración y permisos | Sí | Gratis | No |
| Acceso a datos | Lecturas y escrituras de datos | No (salvo BigQuery) | De pago, y puede ser mucho | Sí |
| Eventos del sistema | Acciones de Google sobre tus recursos | Sí | Gratis | No |
| Denegaciones de política | Peticiones rechazadas por políticas | Sí | Gratis | No |
Los dos datos que hay que retener:
- La actividad de administrador es gratis y no se puede desactivar. Ni siquiera un atacante con permisos de organización puede borrar la evidencia de sus cambios de configuración. Es el cimiento de toda investigación.
- El acceso a datos está desactivado por defecto. Y sin él, como demostró el ejercicio de 07-04, no se puede saber quién leyó los datos de los clientes — lo que convierte una credencial filtrada en una brecha de alcance indeterminable.
Activar el acceso a datos donde importa
Se hace con criterio, porque el volumen puede ser enorme:
# politica-auditoria.yaml — aplicada a la CARPETA produccion
auditConfigs:
# BigQuery: lecturas y escrituras de datos de clientes
- service: bigquery.googleapis.com
auditLogConfigs:
- logType: DATA_READ
- logType: DATA_WRITE
# Cloud Storage: solo escrituras y administracion.
# DATA_READ en un bucket que sirve imagenes generaria millones de entradas.
- service: storage.googleapis.com
auditLogConfigs:
- logType: DATA_WRITE
- logType: ADMIN_READ
# Secret Manager: TODO. Cada acceso a un secreto es relevante
- service: secretmanager.googleapis.com
auditLogConfigs:
- logType: DATA_READ
- logType: DATA_WRITE
# Cloud KMS: cada uso de clave
- service: cloudkms.googleapis.com
auditLogConfigs:
- logType: DATA_READ
- logType: DATA_WRITE
# IAM: cambios de identidad
- service: iam.googleapis.com
auditLogConfigs:
- logType: DATA_WRITELas tres decisiones que hacen esto asequible, y son las que convierten un coste inaceptable en unos 35 € al mes (07-05):
- Cloud Storage sin
DATA_READ: el bucket del catálogo sirve millones de imágenes; registrar cada lectura costaría más que toda la plataforma. Se registran escrituras y lecturas administrativas, que es donde está el riesgo. - Secret Manager con todo: son pocos accesos y cada uno importa.
- Solo en la carpeta
produccion: en desarrollo no hay datos reales.
Y una exclusión imprescindible para que el volumen no se dispare:
gcloud logging sinks update _Default \
--add-exclusion=name=ruido-lecturas-repetitivas,\
filter='protoPayload.methodName="google.storage.objects.get"
AND protoPayload.authenticationInfo.principalEmail=~"^service-.*gserviceaccount.com$"'Se excluyen las lecturas que hacen los propios agentes de servicio de Google, que son ruido puro y de altísimo volumen.
Cómo se lee una entrada
{
"protoPayload": {
"@type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo": {
"principalEmail": "[email protected]",
"serviceAccountDelegationInfo": [
{"firstPartyPrincipal": {"principalEmail": "sa-deploy-prod@..."}}
]
},
"requestMetadata": {
"callerIp": "88.20.145.33",
"callerSuppliedUserAgent": "google-cloud-sdk gcloud/latest"
},
"serviceName": "cloudresourcemanager.googleapis.com",
"methodName": "SetIamPolicy",
"authorizationInfo": [
{"resource": "projects/alpinashop-prod",
"permission": "resourcemanager.projects.setIamPolicy",
"granted": true}
],
"resourceName": "projects/alpinashop-prod",
"serviceData": {"policyDelta": {"bindingDeltas": [
{"action": "ADD", "role": "roles/owner", "member": "user:[email protected]"}
]}}
},
"severity": "NOTICE",
"timestamp": "2026-08-04T18:23:11.004Z"
}Los campos que hay que saber leer, en orden de importancia:
| Campo | Qué dice |
|---|---|
authenticationInfo.principalEmail |
Quién |
serviceAccountDelegationInfo |
Quién de verdad, si hubo impersonación (03-04) |
methodName |
Qué |
resourceName |
Sobre qué |
requestMetadata.callerIp |
Desde dónde |
authorizationInfo.granted |
Si se permitió o se denegó |
serviceData.policyDelta |
El cambio exacto, en los cambios de IAM |
timestamp |
Cuándo |
serviceAccountDelegationInfo es el campo que casi nadie mira y el que más importa en una organización que usa impersonación: sin él, un cambio hecho por Marta impersonando sa-deploy-prod aparece atribuido a la cuenta de servicio, y se pierde el rastro de la persona.
- Las consultas de auditoría que hay que tener preparadas
Escritas y guardadas como vistas antes de necesitarlas. Un incidente no es momento de aprender SQL.
1. ¿Quién cambió una política IAM?
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS quien,
protopayload_auditlog.resourceName AS recurso,
delta.action AS accion,
delta.role AS rol,
delta.member AS miembro,
protopayload_auditlog.requestMetadata.callerIp AS ip
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`,
UNNEST(JSON_QUERY_ARRAY(protopayload_auditlog.servicedata_v1_iam.policyDelta.bindingDeltas)) AS d,
UNNEST([STRUCT(
JSON_VALUE(d, '$.action') AS action,
JSON_VALUE(d, '$.role') AS role,
JSON_VALUE(d, '$.member') AS member)]) AS delta
WHERE protopayload_auditlog.methodName LIKE '%SetIamPolicy%'
AND DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
ORDER BY timestamp DESC2. ¿Quién accedió a datos de clientes?
SELECT
DATE(timestamp) AS dia,
protopayload_auditlog.authenticationInfo.principalEmail AS quien,
protopayload_auditlog.resourceName AS tabla,
COUNT(*) AS accesos
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_data_access`
WHERE protopayload_auditlog.serviceName = 'bigquery.googleapis.com'
AND protopayload_auditlog.resourceName LIKE '%pedidos%'
AND DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY dia, quien, tabla
ORDER BY dia DESC, accesos DESC3. ¿Quién apagó o modificó un log?
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS quien,
protopayload_auditlog.methodName AS operacion,
protopayload_auditlog.resourceName AS recurso
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE (protopayload_auditlog.methodName LIKE '%UpdateSink%'
OR protopayload_auditlog.methodName LIKE '%DeleteSink%'
OR protopayload_auditlog.methodName LIKE '%UpdateBucket%'
OR protopayload_auditlog.methodName LIKE '%SetIamPolicy%'
AND protopayload_auditlog.serviceName = 'logging.googleapis.com'
OR protopayload_auditlog.methodName LIKE '%UpdateExclusion%')
AND DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)
ORDER BY timestamp DESCEsta tercera es la más importante de las tres, y la que menos gente tiene. Manipular el registro es lo primero que hace un atacante competente y lo primero que hace alguien que quiere ocultar un error. Debe tener alerta permanente.
4. Actividad fuera de horario.
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS quien,
protopayload_auditlog.methodName AS operacion,
protopayload_auditlog.requestMetadata.callerIp AS ip
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
AND NOT REGEXP_CONTAINS(
protopayload_auditlog.authenticationInfo.principalEmail, r'^(service-|.*gserviceaccount)')
AND (EXTRACT(HOUR FROM timestamp AT TIME ZONE 'Europe/Madrid') NOT BETWEEN 7 AND 21
OR EXTRACT(DAYOFWEEK FROM timestamp AT TIME ZONE 'Europe/Madrid') IN (1, 7))
ORDER BY timestamp DESCEl filtro que excluye cuentas de servicio es esencial: el software trabaja de noche, y sin ese filtro la consulta devuelve miles de líneas irrelevantes.
5. Denegaciones de política: lo que alguien intentó y no pudo.
SELECT
DATE(timestamp) AS dia,
protopayload_auditlog.authenticationInfo.principalEmail AS quien,
protopayload_auditlog.methodName AS operacion,
protopayload_auditlog.status.message AS motivo,
COUNT(*) AS intentos
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_policy`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY dia, quien, operacion, motivo
ORDER BY intentos DESCEsta consulta tiene un doble uso que conviene entender: detecta intentos maliciosos, y también detecta políticas mal calibradas que están estorbando al equipo. Si Dani aparece veinte veces intentando algo legítimo, el problema es la política, no Dani.
- Retención inmutable en un proyecto separado
Aquí está la pieza que hace que la auditoría sea creíble.
Un log que el administrador puede borrar no sirve como evidencia.
Si Marta tiene permisos para modificar los logs de auditoría, entonces ante cualquier investigación —interna, de un cliente o de la AEPD— la respuesta a "¿podría alguien haber alterado esto?" es "sí". Y eso vacía de valor todo el registro.
La solución es un proyecto separado con permisos que ni siquiera los administradores tienen.
flowchart LR
subgraph ORG["Organizacion"]
P1["alpinashop-prod"]
P2["alpinashop-dev"]
P3["alpinashop-datos"]
P4["alpinashop-red"]
end
SINK["Sumidero agregado<br/>a nivel de ORGANIZACION"]
P1 & P2 & P3 & P4 --> SINK
SINK --> B["Bucket de logs<br/>alpinashop-auditoria<br/>retencion BLOQUEADA 400 dias"]
SINK --> BQ["BigQuery<br/>consultas de auditoria"]
SINK --> GCS["Bucket GCS<br/>bloqueo de retencion 7 anos"]
style B fill:#e6f4ea,stroke:#34a853
style GCS fill:#e6f4ea,stroke:#34a853
Paso 1: el proyecto y sus permisos
gcloud projects create alpinashop-auditoria --folder=FOLDER_SEGURIDAD_ID
gcloud services enable logging.googleapis.com bigquery.googleapis.com storage.googleapis.com \
--project=alpinashop-auditoriaY el reparto de permisos, que es la clave de todo:
| Identidad | Rol | Puede |
|---|---|---|
gcp-seguridad@ |
roles/logging.viewer, roles/bigquery.dataViewer |
Leer, no escribir ni borrar |
gcp-infra@ (Marta) |
roles/logging.viewer |
Solo leer |
| Auditor externo | roles/logging.viewer acotado, temporal |
Leer durante la auditoría |
| Nadie | roles/logging.admin sobre los sumideros |
— |
Marta, que administra toda la plataforma, solo puede leer aquí. Es la separación de funciones de 07-04 llevada a su conclusión: quien administra no custodia la evidencia de lo que administra.
Paso 2: el sumidero agregado a nivel de organización
gcloud logging sinks create sumidero-auditoria-org \
logging.googleapis.com/projects/alpinashop-auditoria/locations/eu/buckets/auditoria \
--organization=ORG_ID \
--include-children \
--log-filter='logName:"cloudaudit.googleapis.com"'--include-children es lo que lo hace agregado: recoge los logs de todos los proyectos y carpetas de la organización, incluidos los que se creen en el futuro. Sin esa bandera, cada proyecto nuevo quedaría fuera y nadie se daría cuenta.
Después hay que conceder al escritor del sumidero permiso sobre el destino:
ESCRITOR=$(gcloud logging sinks describe sumidero-auditoria-org \
--organization=ORG_ID --format='value(writerIdentity)')
gcloud projects add-iam-policy-binding alpinashop-auditoria \
--member="$ESCRITOR" --role=roles/logging.bucketWriterEste paso se olvida constantemente y el síntoma es desconcertante: el sumidero existe, aparece en la consola, y no llega nada. Sin errores.
Paso 3: retención bloqueada
# Bucket de logs con retencion de 400 dias y BLOQUEO
gcloud logging buckets create auditoria \
--location=eu --retention-days=400 \
--project=alpinashop-auditoria
# EL PASO IRREVERSIBLE
gcloud logging buckets update auditoria \
--location=eu --locked \
--project=alpinashop-auditoria⚠️
--lockedes irreversible. A partir de ese momento nadie, ni Google, ni el owner de la organización, ni el soporte, puede reducir la retención ni borrar el bucket antes de que expire. Ese es exactamente el punto: la garantía es que ni tú puedes.
Y para conservación a largo plazo, con bloqueo de retención de Cloud Storage:
gcloud storage buckets create gs://alpinashop-auditoria-archivo \
--location=EU --uniform-bucket-level-access \
--project=alpinashop-auditoria
gcloud storage buckets update gs://alpinashop-auditoria-archivo \
--retention-period=7y
# BLOQUEO DEFINITIVO: ningun objeto se podra borrar antes de 7 anos
gcloud storage buckets update gs://alpinashop-auditoria-archivo --lock-retention-periodEsto es lo que se conoce como almacenamiento WORM (Write Once, Read Many), y es lo que exigen muchos marcos de cumplimiento. También es, dicho sea de paso, la mejor defensa contra ransomware que existe: un atacante con permisos totales no puede cifrar ni borrar lo que está bajo bloqueo de retención.
Con una contrapartida que hay que asumir conscientemente: si se bloquean 7 años, se pagan 7 años de almacenamiento, sin excepción. Antes de bloquear, se calcula el volumen y se comprueba que el número tiene sentido.
- Zonas de aterrizaje y el orden que importa
Una zona de aterrizaje (landing zone) es la organización preparada antes de que llegue la primera carga de trabajo: jerarquía, políticas, red, identidad, auditoría y facturación, todo desplegado con código.
AlpinaShop lo ha hecho al revés, como casi todo el mundo: primero las cargas, después el gobierno. Ha funcionado porque es pequeña. La forma correcta, y la que hay que conocer para el próximo proyecto, es esta.
El orden de despliegue, y por qué importa
flowchart TB
A["1. Organizacion y Cloud Identity<br/>grupos, MFA, dominio"] --> B["2. Jerarquia de carpetas"]
B --> C["3. Facturacion<br/>cuenta, exportacion, presupuestos"]
C --> D["4. Politicas de organizacion<br/>en DRY-RUN"]
D --> E["5. Proyecto de auditoria<br/>sumidero agregado + retencion"]
E --> F["6. Proyecto de red<br/>VPC compartida"]
F --> G["7. IAM: grupos y roles"]
G --> H["8. Proyectos de carga"]
H --> I["9. Politicas en modo APLICADO"]
I --> J["10. Cargas de trabajo"]
style E fill:#e6f4ea,stroke:#34a853
style D fill:#fef7e0,stroke:#fbbc04
style I fill:#fef7e0,stroke:#fbbc04
Las cuatro razones por las que este orden concreto y no otro:
- La auditoría (5) va antes que la red y las cargas. Si el sumidero se crea después, los logs de todo lo que se hizo mientras tanto no están capturados. La construcción de la plataforma es precisamente el periodo con más cambios de permisos, y es el que más interesa tener registrado.
- Las políticas en
dry-run(4) van antes que los proyectos de carga (8). Así se miden sobre recursos que se van creando, y las violaciones se corrigen sobre la marcha en lugar de acumularse. - La red (6) antes que los proyectos de servicio (8). Un proyecto de servicio necesita que exista el host.
- Las políticas aplicadas (9) antes que las cargas (10). Es el único momento en que aplicar una política no rompe nada, porque no hay nada que romper.
En Terraform
# modules/landing-zone/main.tf — estructura del modulo
module "carpetas" {
source = "./modulos/carpetas"
organizacion = var.org_id
carpetas = ["produccion", "desarrollo", "compartido", "seguridad"]
}
module "auditoria" {
source = "./modulos/auditoria"
depends_on = [module.carpetas]
org_id = var.org_id
carpeta_seguridad = module.carpetas.ids["seguridad"]
retencion_dias = 400
bloquear_retencion = true # IRREVERSIBLE: se pone a mano la primera vez
}
module "politicas" {
source = "./modulos/politicas-organizacion"
depends_on = [module.carpetas]
org_id = var.org_id
dry_run = var.politicas_en_pruebas # true al principio, false tras medir
regiones_permitidas = ["in:europe-west1-locations",
"in:europe-southwest1-locations",
"in:eu-locations"]
}
module "red" {
source = "./modulos/vpc-compartida"
depends_on = [module.politicas]
carpeta_compartido = module.carpetas.ids["compartido"]
proyecto_host = "alpinashop-red"
subredes = var.subredes
}
module "proyectos" {
source = "./modulos/proyectos"
depends_on = [module.red]
for_each = var.proyectos
# ...
}depends_on explícito en cada módulo, aunque no haya referencias entre ellos. Es el caso legítimo de depends_on que 06-07 señalaba como excepción: el orden aquí no viene de dependencias de datos, sino de una secuencia lógica que Terraform no puede inferir.
Google publica los blueprints de fundación (Cloud Foundation Fabric, Terraform Example Foundation), que implementan todo esto con criterio. Merecen una advertencia honesta: están pensados para organizaciones grandes y traen mucha más estructura de la que necesita una pyme. Úsalos como referencia y como fuente de decisiones bien pensadas, no como plantilla a copiar.
Lo que costaría hacerlo hoy en AlpinaShop
| Fase | Trabajo | Duración |
|---|---|---|
| Inventariar incumplimientos | Consultas del apartado 8 | 2 h |
Crear alpinashop-auditoria y el sumidero |
Terraform | 4 h |
Políticas en dry-run |
Terraform | 3 h |
| Espera y análisis | — | 14 días |
| Corregir incumplimientos y excepciones | Variable | 1-2 días |
| Aplicar en desarrollo | — | 1 h |
| Espera | — | 7 días |
| Aplicar en producción | — | 1 h |
| Feeds y alertas | Terraform + función | 4 h |
| Total de trabajo efectivo | ~4 días | |
| Total de calendario | ~4 semanas |
Cuatro días de trabajo repartidos en cuatro semanas. Ese es el precio de gobernar la plataforma, y explica por qué la aritmética del apartado 1 importa: dentro de dos años serían cuatro semanas de trabajo y seis meses de calendario.
- Gestión del cambio: revisiones, ciclo de vida y excepciones
Las políticas técnicas no bastan. Hacen falta procesos, y para un equipo de tres personas tienen que ser ligeros o no se harán.
Revisión periódica de accesos
| Periodicidad | Qué se revisa | Quién | Duración |
|---|---|---|---|
| Mensual | Cuentas de servicio nuevas; claves creadas; roles básicos | Marta | 30 min |
| Trimestral | Todos los permisos frente a las funciones reales; recomendaciones del Recommender | Marta + Dani | 2 h |
| Semestral | Cuentas externas; excepciones de política; accesos de terceros | Marta + dirección | 1 h |
| Al cambiar alguien de puesto o irse | Todos sus accesos | Inmediato | — |
#!/usr/bin/env bash
# revision-trimestral.sh — genera el informe para la reunion
echo "=== 1. Roles basicos en la organizacion ==="
gcloud asset search-all-iam-policies --scope=organizations/$ORG_ID \
--query="policy:(roles/owner OR roles/editor)" \
--format="table(resource, policy.bindings.role, policy.bindings.members)"
echo "=== 2. Cuentas externas ==="
gcloud asset search-all-iam-policies --scope=organizations/$ORG_ID \
--query="policy:(gmail.com OR hotmail.com OR outlook.com)" \
--format="table(resource, policy.bindings.members)"
echo "=== 3. Permisos concedidos y no usados en 90 dias ==="
for P in $PROYECTOS; do
gcloud recommender recommendations list --project="$P" --location=global \
--recommender=google.iam.policy.Recommender \
--format="table(content.overview.member, content.overview.removedRole)"
done
echo "=== 4. Claves de cuentas de servicio ==="
gcloud asset search-all-resources --scope=organizations/$ORG_ID \
--asset-types=iam.googleapis.com/ServiceAccountKey \
--format="table(name, createTime)"
echo "=== 5. Excepciones de politica vigentes ==="
gcloud org-policies list --organization=$ORG_ID --format="table(name, spec.rules)"Ciclo de vida de los proyectos
| Fase | Qué se exige |
|---|---|
| Solicitud | Propósito, responsable, entorno, centro de coste, fecha de revisión |
| Creación | Siempre por Terraform, nunca desde la consola. Etiquetas obligatorias, presupuesto, carpeta correcta |
| Operación | Revisión trimestral de coste y permisos |
| Caducidad | Todo proyecto de pruebas tiene fecha de caducidad |
| Retirada | Exportar datos → desvincular facturación → 30 días de espera → borrar |
La fecha de caducidad obligatoria en los proyectos de prueba es la medida que más gasto fantasma evita (07-05), porque ataca la causa en lugar del síntoma. Se implementa con una etiqueta caduca=2026-12-31 y un trabajo semanal que avisa de los vencidos.
Y los 30 días de espera antes de borrar no son burocracia: borrar un proyecto es irreversible pasado el periodo de gracia, y siempre aparece alguien que necesitaba algo de ahí.
El proceso de excepciones
Toda política acabará teniendo excepciones. Sin proceso, se conceden a voz y se convierten en permanentes.
| Elemento | Regla |
|---|---|
| Solicitud | Por escrito, con motivo técnico y alternativas descartadas |
| Aprobación | Marta + una persona más. Nunca una sola |
| Alcance | El mínimo posible: un recurso, no un proyecto |
| Caducidad | Obligatoria. Máximo 90 días, renovable con justificación |
| Registro | En alpinashop-infra/EXCEPCIONES.md, con Terraform |
| Revisión | Semestral: ¿sigue haciendo falta? |
# EXCEPCIONES.md
## EXC-001 — IP pública en `bastion-migracion`
- **Política:** `constraints/compute.vmExternalIpAccess`
- **Alcance:** `projects/alpinashop-dev/zones/europe-west1-b/instances/bastion-migracion`
- **Motivo:** el proveedor del ERP exige conexión entrante desde su IP para la
migración de datos; no admite conexión saliente ni PSC.
- **Alternativas descartadas:** IAP (el proveedor no puede usar el túnel),
VPN (fuera de plazo del proyecto).
- **Mitigación:** firewall restringido a la IP del proveedor; la VM se apaga
fuera de las sesiones de trabajo; log de conexiones activado.
- **Solicitada por:** Dani · **Aprobada por:** Marta + dirección
- **Concedida:** 2026-08-05 · **CADUCA: 2026-11-03**
- **Revisión:** al terminar la migración, se elimina la VM y la excepciónLa fecha de caducidad es lo que separa una excepción de una derogación. Sin ella, la política queda vacía de contenido para siempre y nadie recuerda por qué.
- La madurez de AlpinaShop, y lo que le queda
Dónde está
| Dominio | Estado | Comentario |
|---|---|---|
| Estructura organizativa | 🟢 Buena | Jerarquía por entorno, proyectos con propósito claro, carpeta de seguridad separada |
| Identidad | 🟢 Buena | Grupos, sin roles básicos, sin claves, federación, nadie owner de producción |
| Red | 🟢 Buena | VPC compartida, permisos por subred, híbrido resuelto, perímetro de datos |
| Cómputo | 🟢 Buena | Sin servidores que mantener, escalado a cero, canario y reversión en segundos |
| Datos | 🟡 Aceptable | Clasificados, cifrados, con retención; falta probar el derecho de supresión |
| CI/CD | 🟢 Buena | Trunk-based, sin claves, aprobación manual, canario verificado |
| Observabilidad | 🟢 Buena | Métricas, logs, trazas, SLO, presupuesto de error, alertas por consumo |
| Fiabilidad | 🟡 Aceptable | HA regional, DR con RTO/RPO, restauración probada; falta multirregión |
| Coste | 🟢 Buena | Visibilidad, atribución, presupuestos, rutina mensual, −60 % |
| Seguridad | 🟡 Aceptable | Defensa en profundidad; deuda prioritaria pagada; falta detección activa |
| Gobierno | 🟡 En marcha | Lo de esta lección: políticas, inventario, auditoría inmutable |
| Cumplimiento | 🟠 Mejorable | Documentación desactualizada; falta validación profesional |
Lo que le queda, con honestidad
A corto plazo (este trimestre):
- Terminar de aplicar las políticas de organización. Están en
dry-run; hay que atravesar las 4 semanas del apartado 13. - Documentación de cumplimiento actualizada, con asesoría profesional. Es la casilla más atrasada y la única con consecuencias legales.
- Probar el procedimiento de derecho de supresión de extremo a extremo, con un cliente de prueba.
A medio plazo (este año):
- Multirregión para Cloud SQL, que es lo único que impide la alta disponibilidad regional completa (07-06).
- Detección activa de amenazas, cuando haya alguien que pueda atenderla (07-04).
- Elevación temporal de privilegios con PAM.
- Revisión externa de seguridad sobre la arquitectura ya endurecida.
Lo que NO va a hacer, y está bien:
| No hará | Por qué |
|---|---|
| GKE Enterprise / Google Distributed Cloud | DA-004: no hay cargas fuera de GCP que gestionar |
| Malla de servicios | Un servicio. No hay nada que mallar |
| Multinube | No hay motivo de negocio |
| Activo-activo | El coste no se justifica con 1.200 pedidos/mes (07-06) |
| Equipo de seguridad dedicado | Cuarenta personas |
| Network Connectivity Center | Una VPC y un enlace |
Esa última tabla es la más valiosa de la lección, porque una organización madura no es la que ha adoptado más tecnología: es la que sabe justificar lo que ha decidido no adoptar. Las seis filas tienen su decisión escrita, su motivo, y —en el caso de DA-004— sus condiciones de revisión.
El recorrido completo
Vale la pena mirar atrás una vez.
AlpinaShop empezó el módulo 1 sin cuenta de Google Cloud, con una tienda que funcionaba en algún sitio y tres personas que no sabían qué era una región. Terminó el módulo 2 con la aplicación migrada y una decisión pendiente. El módulo 3 le dio red, balanceo, identidad y secretos. El 4, una plataforma de datos. El 5, modelos que predicen. El 6, entrega continua y observabilidad. Y el 7 ha resuelto lo que quedaba: el híbrido, el cómputo definitivo, la red avanzada, la seguridad revisada, la factura entendida, la fiabilidad medida y el gobierno aplicado.
Hoy AlpinaShop tiene una plataforma que una persona puede operar, cualquiera puede entender leyendo un repositorio, y se reconstruye entera con terraform apply. No es perfecta —tiene una lista de deudas escrita y priorizada, que es exactamente lo que debe tener— pero es defendible ante un cliente, ante un auditor y ante la persona que entre a trabajar el mes que viene.
Errores Comunes y Consejos
- Dejar el gobierno para "cuando seamos grandes". El coste crece con las excepciones que hay que negociar, no con el número de recursos. Se hace ahora o no se hace.
- Aplicar políticas sin inventariar antes lo que las incumple. Se generan cientos de violaciones que nadie analiza y el
dry-rundeja de servir. - Olvidar
in:eu-locationsenresourceLocations. BigQuery multirregión y algunos buckets dejan de crearse. allowedPolicyMemberDomainssin excepciones para los agentes de servicio de Google. Servicios internos fallan de formas inexplicables.- Exigir CMEK sin dar permisos al agente de servicio sobre la clave. Los recursos nuevos no se pueden crear.
- Aplicar políticas un viernes. Fin de semana sin poder desplegar.
- Que solo una persona tenga
orgpolicy.policyAdmin. Si se bloquea a sí misma, la salida es soporte de Google. - Sumidero sin
--include-children. Los proyectos nuevos quedan fuera y nadie se entera. - Olvidar dar
logging.bucketWritera lawriterIdentitydel sumidero. El sumidero existe y no llega nada, sin errores. - Crear el proyecto de auditoría al final. Los logs de la fase de construcción, que es la de más cambios, se pierden.
- Que el administrador pueda escribir en los logs de auditoría. Entonces no valen como evidencia.
- Bloquear la retención sin calcular el volumen.
--lockedes irreversible y pagas todos los años que pusiste. - Activar
DATA_READen un bucket que sirve imágenes. Millones de entradas y una factura absurda. - No mirar
serviceAccountDelegationInfo. En una organización con impersonación, sin ese campo se pierde el rastro de la persona. - Excepciones sin fecha de caducidad. Dejan de ser excepciones y vacían la política.
- Copiar un blueprint de fundación tal cual. Están hechos para organizaciones grandes y traen estructura que no necesitas.
- Consejo: la consulta de "quién tocó un log" merece alerta permanente. Es lo primero que manipula quien quiere ocultar algo.
- Consejo: pon fecha de caducidad obligatoria a los proyectos de prueba. Ataca la causa del gasto fantasma, no el síntoma.
- Consejo: usa las denegaciones de política como termómetro. Si alguien del equipo aparece veinte veces, la política está mal calibrada.
- Consejo: escribe lo que decides NO adoptar. Es lo que distingue una organización madura de una que simplemente no llegó.
Ejercicios
Ejercicio 1 — Diseñar el conjunto de políticas de una empresa nueva
AlpinaTech, una consultora de 25 personas, arranca en Google Cloud desde cero. Datos:
- Tres equipos: desarrollo (12), datos (5), infraestructura (3). El resto no es técnico.
- Trabajan para clientes externos, y algunos exigen que sus datos no salgan de la UE.
- Dos consultores freelance con cuenta de Gmail colaboran en proyectos concretos.
- Un cliente exige certificación ISO 27001 en 18 meses.
- Presupuesto: 2.500 €/mes de nube.
Diseña: la jerarquía de carpetas y proyectos con su criterio justificado, el conjunto completo de políticas de organización con su nivel y su riesgo, cómo resuelves el caso de los freelance sin desactivar allowedPolicyMemberDomains, y el orden de despliegue de la zona de aterrizaje.
Ejercicio 2 — Investigar un cambio sospechoso
Un lunes por la mañana, la consulta de auditoría mensual revela esto:
timestamp quien operacion recurso 2026-08-01T23:47:12Z [email protected]... SetIamPolicy projects/alpinashop-prod 2026-08-01T23:47:44Z [email protected]... CreateServiceAccount projects/alpinashop-prod 2026-08-01T23:48:03Z [email protected]... CreateServiceAccountKey projects/alpinashop-prod 2026-08-01T23:51:20Z [email protected]... storage.buckets.update alpinashop-datalake
Era sábado por la noche. No había ningún despliegue programado. La cuenta sa-mantenimiento no aparece en el Terraform de AlpinaShop.
Escribe la investigación completa: qué consultas ejecutas y en qué orden, qué buscas en cada una, cómo determinas si fue un ataque o una automatización legítima no documentada, qué contienes y en qué orden, y qué controles de gobierno habrían detectado o impedido esto antes.
Ejercicio 3 — Justificar el gobierno ante dirección
La dirección de AlpinaShop pregunta por qué hay que dedicar cuatro días a "poner reglas" cuando el sistema funciona perfectamente y no ha habido ningún incidente.
Escribe la respuesta de Marta: qué riesgo concreto cubre cada bloque de trabajo, qué pasaría sin él con ejemplos reales del curso, qué coste tiene hacerlo ahora frente a dentro de dos años, y qué le pides además del tiempo. Máximo una página, en lenguaje no técnico.
Soluciones
Solución 1 — Diseñar las políticas de AlpinaTech
Jerarquía, y el criterio que la decide.
El factor determinante no es el tamaño ni los equipos: es que trabajan para clientes externos con requisitos distintos. Eso hace que el aislamiento por cliente sea más importante que el aislamiento por entorno.
Organizacion alpinatech.example
├── carpeta: clientes
│ ├── carpeta: cliente-acme
│ │ ├── alpinatech-acme-prod
│ │ └── alpinatech-acme-dev
│ └── carpeta: cliente-beta
│ ├── alpinatech-beta-prod
│ └── alpinatech-beta-dev
├── carpeta: interno
│ ├── alpinatech-web-corporativa
│ └── alpinatech-herramientas
├── carpeta: compartido
│ ├── alpinatech-red
│ └── alpinatech-cicd
└── carpeta: seguridad
└── alpinatech-auditoriaPor qué carpeta por cliente y entorno dentro:
| Ventaja | Detalle |
|---|---|
| Aislamiento contractual | Los datos de Acme no pueden mezclarse con los de Beta ni por accidente |
| Políticas por cliente | Si Acme exige solo UE y Beta no, se aplica en la carpeta de cada uno |
| Facturación directa | El coste por carpeta es lo que se factura al cliente |
| Retirada limpia | Cuando acaba el contrato, se borra la carpeta entera |
| Permisos por proyecto | Solo el equipo asignado a Acme tiene acceso a Acme |
La última fila es la que más importa con freelance de por medio, y enlaza con el punto siguiente.
Conjunto de políticas.
| # | Restricción | Nivel | Riesgo | Motivo |
|---|---|---|---|---|
| 1 | iam.allowedPolicyMemberDomains |
Organización | Alto | Trabajan con externos: es la más importante y la más delicada |
| 2 | gcp.resourceLocations = UE |
Carpeta clientes |
Medio-alto | Requisito contractual; en interno puede ser más laxo |
| 3 | iam.disableServiceAccountKeyCreation |
Organización | Medio | Base de la ISO 27001 |
| 4 | storage.publicAccessPrevention |
Organización | Bajo | Datos de clientes |
| 5 | storage.uniformBucketLevelAccess |
Organización | Bajo | IAM como única verdad |
| 6 | compute.vmExternalIpAccess |
Organización | Bajo | — |
| 7 | compute.requireOsLogin |
Organización | Bajo | Baja de un freelance = pierde acceso a todo |
| 8 | iam.automaticIamGrantsForDefaultServiceAccounts |
Organización | Bajo | Desde el día 1: no habrá que corregirlo nunca |
| 9 | sql.restrictPublicIp |
Organización | Bajo | — |
| 10 | compute.disableSerialPortAccess |
Organización | Bajo | Evita un camino de acceso sin auditar |
| 11 | Personalizada: etiquetas cliente y centro-coste obligatorias |
Organización | Medio | La facturación al cliente depende de ello |
| 12 | gcp.restrictNonCmekServices |
Carpeta clientes |
Alto | Si algún cliente lo exige |
Nótese la política 2 a nivel de carpeta clientes y no de organización. Aplicarla en la organización obligaría a la web corporativa y a las herramientas internas a estar en la UE, lo cual probablemente esté bien pero es una restricción que no se necesita. Las políticas se ponen en el nivel más alto donde tengan sentido, no en el más alto posible.
El caso de los freelance — la parte interesante del ejercicio.
Desactivar allowedPolicyMemberDomains para dar acceso a dos cuentas de Gmail sería tirar por la borda la política más valiosa del conjunto. Hay cuatro opciones, y la última es la correcta:
| Opción | Valoración |
|---|---|
| No aplicar la política | ❌ Cualquiera puede dar acceso a cualquier cuenta de Google del mundo |
| Excepción permanente para sus dos correos de Gmail | ❌ Cuentas personales sin control de la empresa: sin MFA obligatorio, sin baja centralizada, sin poder revocar el correo |
| Excepción en la carpeta del cliente concreto | ⚠️ Mejor, pero sigue apoyándose en cuentas personales |
| Cuentas de Cloud Identity propias para los freelance | ✅ La correcta |
La solución: [email protected] gestionada por la empresa, con MFA obligatorio, condición de IAM con fecha de caducidad (03-04) coincidente con el fin del contrato, y permisos únicamente sobre el proyecto en el que trabajan.
resource "google_project_iam_member" "freelance_acme" {
project = "alpinatech-acme-dev"
role = "roles/editor"
member = "user:[email protected]"
condition {
title = "Contrato Acme hasta 2026-12-31"
expression = "request.time < timestamp('2027-01-01T00:00:00Z')"
}
}El acceso caduca solo. No depende de que nadie se acuerde de revocarlo el 31 de diciembre, que es exactamente el tipo de tarea que se olvida. Y el coste de una licencia de Cloud Identity es ridículo comparado con el riesgo de que un consultor mantenga acceso a datos de un cliente durante dos años después de terminar.
Orden de despliegue.
- Cloud Identity, dominio, grupos (
at-desarrollo@,at-datos@,at-infra@,at-seguridad@), MFA obligatorio desde el minuto uno. - Organización y carpetas.
- Facturación: cuenta, exportación a BigQuery, presupuesto global de 2.500 € y uno por carpeta de cliente.
alpinatech-auditoriacon sumidero agregado y retención bloqueada. Antes que nada más, para capturar toda la construcción.- Políticas 1-11 en
dry-run. alpinatech-redcon VPC compartida.alpinatech-cicdcon Workload Identity Federation.- Primer proyecto de cliente como plantilla, con módulo de Terraform reutilizable.
- Políticas en modo aplicado.
- Cargas de trabajo.
Ventaja decisiva de AlpinaTech sobre AlpinaShop: empieza en el paso 1 con la casilla vacía. No tendrá que negociar ninguna excepción, porque cuando aplique las políticas no habrá nada que las incumpla. Es literalmente el mejor momento posible, y solo ocurre una vez.
Sobre la ISO 27001 a 18 meses: prácticamente todo lo de esta lección es evidencia directa para la certificación —control de accesos, registro de auditoría inmutable, gestión de cambios, revisión periódica de permisos, clasificación de datos—. Hacerlo ahora significa llegar a la auditoría con 18 meses de registros; hacerlo dentro de un año significa llegar con seis. El auditor mira el histórico, no la foto del día.
Solución 2 — Investigar el cambio sospechoso
Lectura inicial de los indicios. Cuatro señales, y ninguna es concluyente por separado pero juntas dibujan un patrón muy reconocible:
| Indicio | Por qué preocupa |
|---|---|
| Sábado 23:47 | Fuera de todo horario y sin despliegue programado |
SetIamPolicy → CreateServiceAccount → CreateServiceAccountKey en 51 segundos |
Es la secuencia canónica de establecimiento de persistencia |
CreateServiceAccountKey |
La política 4.3 debería impedirlo. Existe una clave descargable nueva |
sa-mantenimiento no está en Terraform |
Recurso creado fuera del proceso: o es shadow IT o es un atacante |
Paso 1 — ¿Quién actuó de verdad? (5 minutos)
sa-deploy-prod es una cuenta de servicio; no actúa sola. La pregunta es quién la usó:
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS identidad,
JSON_VALUE(protopayload_auditlog.authenticationInfo.serviceAccountDelegationInfo)
AS delegacion,
protopayload_auditlog.requestMetadata.callerIp AS ip,
protopayload_auditlog.requestMetadata.callerSuppliedUserAgent AS agente,
protopayload_auditlog.methodName AS operacion
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.authenticationInfo.principalEmail LIKE 'sa-deploy-prod%'
AND timestamp BETWEEN TIMESTAMP('2026-08-01 22:00:00')
AND TIMESTAMP('2026-08-02 02:00:00')
ORDER BY timestampLos tres campos que deciden el caso:
| Campo | Si es… | Significa |
|---|---|---|
delegacion |
Vacío | La cuenta se usó directamente, con una credencial. ¿De dónde salió? |
delegacion |
Un correo de persona | Alguien la impersonó. ¿Quién y por qué un sábado? |
agente |
Google-Cloud-Build |
Fue el pipeline: hay que buscar qué compilación |
agente |
gcloud/... |
Alguien desde un terminal |
agente |
Una biblioteca genérica o vacío | Muy sospechoso |
ip |
Rango de Google (35.x, 34.x) |
Compatible con Cloud Build |
ip |
IP de oficina | Alguien del equipo |
ip |
Cualquier otra | 🚨 Incidente confirmado |
Paso 2 — ¿Qué se concedió exactamente? (5 minutos)
SELECT
timestamp,
protopayload_auditlog.resourceName AS recurso,
JSON_VALUE(d, '$.action') AS accion,
JSON_VALUE(d, '$.role') AS rol,
JSON_VALUE(d, '$.member') AS miembro
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`,
UNNEST(JSON_QUERY_ARRAY(
protopayload_auditlog.servicedata_v1_iam.policyDelta.bindingDeltas)) AS d
WHERE protopayload_auditlog.methodName LIKE '%SetIamPolicy%'
AND DATE(timestamp) = '2026-08-01'Si el resultado incluye ADD roles/owner o ADD roles/editor a sa-mantenimiento, o cualquier concesión a una identidad externa, es un incidente y se activa el guion de 07-04.
Paso 3 — ¿Se usó la clave creada? (10 minutos)
Esta es la pregunta que determina el alcance:
SELECT
timestamp,
protopayload_auditlog.methodName AS operacion,
protopayload_auditlog.resourceName AS recurso,
protopayload_auditlog.requestMetadata.callerIp AS ip
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.authenticationInfo.principalEmail LIKE 'sa-mantenimiento%'
ORDER BY timestampY el cambio sobre el bucket del data lake, que es el objetivo evidente:
SELECT timestamp, protopayload_auditlog.methodName, protopayload_auditlog.request
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.resourceName LIKE '%alpinashop-datalake%'
AND DATE(timestamp) BETWEEN '2026-08-01' AND CURRENT_DATE()
ORDER BY timestampSi buckets.update añadió allUsers, el data lake estuvo público. Eso convierte el caso en una brecha de datos personales con notificación a la AEPD en 72 horas.
Paso 4 — ¿Ataque o automatización no documentada? (15 minutos)
| Señal | Automatización legítima | Ataque |
|---|---|---|
| IP | Rango de Google o de oficina | Externa, o de un proveedor de nube ajeno |
| Agente de usuario | Google-Cloud-Build, terraform |
Genérico, vacío, python-requests |
| Delegación | Una persona del equipo | Vacío o desconocido |
| Compilación asociada | Existe en Cloud Build | No existe |
| Rol concedido | Acotado y coherente | owner, editor |
| Clave creada | Ninguna: el pipeline no las necesita | Sí |
| Nombre del recurso | Coherente con la convención | Genérico y plausible: sa-mantenimiento |
| Actividad posterior | Coherente con la tarea | Enumeración, lecturas masivas |
El indicio que más pesa es la creación de la clave. El pipeline de AlpinaShop usa Workload Identity Federation desde 06-02 y no necesita ni ha creado nunca una clave JSON. Una clave nueva en producción, un sábado a las 23:48, es casi con certeza persistencia de un atacante. Y el nombre sa-mantenimiento es exactamente el tipo de nombre plausible que se elige para pasar desapercibido en una lista.
Paso 5 — Contención, en este orden.
# 1. BORRAR la clave: es la credencial exfiltrada
gcloud iam service-accounts keys delete KEY_ID \
--iam-account=sa-mantenimiento@alpinashop-prod.iam.gserviceaccount.com
# 2. DESHABILITAR (no borrar) la cuenta: se conserva la evidencia
gcloud iam service-accounts disable \
[email protected]
# 3. Revertir las concesiones IAM indebidas
gcloud projects remove-iam-policy-binding alpinashop-prod \
--member="serviceAccount:[email protected]" \
--role="roles/owner"
# 4. Cerrar el bucket si quedó abierto
gcloud storage buckets update gs://alpinashop-datalake --no-public-access-prevention=false
# 5. Rotar las credenciales de sa-deploy-prod y revisar el pipeline
# (si el compromiso vino por ahi, todo lo que toca esta en duda)
# 6. Congelar evidencia fuera del proyecto afectado
gcloud logging read 'timestamp>="2026-07-01T00:00:00Z"' --project=alpinashop-prod \
--format=json > /tmp/evidencia.json
gcloud storage cp /tmp/evidencia.json gs://alpinashop-evidencia-forense/
# 7. Buscar MAS persistencia: nunca hay solo una
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--query="createTime>2026-07-25" --format="table(name, assetType, createTime)"El orden importa: primero la credencial (paso 1), porque mientras exista el atacante sigue dentro; después la cuenta; después los permisos. Y el paso 7 nunca se omite: quien establece persistencia rara vez lo hace en un solo sitio.
Qué controles de gobierno lo habrían detectado o impedido.
| Control | Efecto | Apartado |
|---|---|---|
iam.disableServiceAccountKeyCreation |
Lo habría IMPEDIDO. Sin clave, no hay persistencia | 4.3 |
Feed de Cloud Asset Inventory sobre ServiceAccountKey |
Alerta en segundos, no 36 horas después | 9 |
Feed sobre concesiones de owner |
Alerta inmediata al SetIamPolicy |
9 |
storage.publicAccessPrevention |
Habría impedido abrir el bucket | 4.5 |
| Consulta de actividad fuera de horario | Detección al día siguiente, no en la revisión mensual | 11 |
| Terraform como única vía de creación + detección de deriva | Un plan habría mostrado la cuenta no declarada |
06-07 |
| Retención inmutable de logs | Garantiza que la evidencia no fue alterada | 12 |
Y la conclusión que da sentido a la lección entera: de las siete medidas, la primera habría impedido el ataque por completo y la segunda lo habría detectado en segundos. Ambas son de esta lección, ambas cuestan menos de una hora de configuración, y ninguna estaba puesta el sábado por la noche.
Un control preventivo que cuesta una hora vale más que la investigación más brillante.
Solución 3 — Justificar el gobierno ante dirección
Por qué dedicamos cuatro días a poner reglas en la nube Marta Ruiz, responsable de infraestructura
Es verdad que el sistema funciona y que no hemos tenido ningún incidente grave. También es verdad que funciona porque las tres personas del equipo nos acordamos de hacer las cosas bien, no porque nada lo impida. Hoy, cualquiera de nosotros —o cualquiera que entre a trabajar el mes que viene— puede, sin querer y sin que salte ninguna alarma, dejar un almacén de datos abierto a internet, crear una contraseña permanente que nadie sepa que existe, o poner datos de clientes en un servidor fuera de Europa.
No es una hipótesis. Este año hemos tenido dos casos. Encontramos una contraseña de acceso a datos que llevaba catorce meses perdida y seguía funcionando. Y descubrimos dos servicios encendidos que nadie usaba y que nos habían costado 1.500 €. Los dos se resolvieron, pero los dos los encontramos por casualidad, meses después.
Qué cubre cada bloque de trabajo:
Reglas automáticas (día 1). Impiden técnicamente lo que hoy solo evitamos por costumbre: nada de servidores expuestos a internet, nada de datos fuera de la UE, y —la más importante— nadie puede crear contraseñas permanentes. Sin esto, el caso de la contraseña perdida puede repetirse mañana.
Registro protegido (día 2). Guardamos en un sitio aparte, y con un candado que ni yo puedo abrir, el registro de todo lo que se hace en la plataforma. Sirve para tres cosas: si algo pasa, sabemos exactamente qué y quién; si un cliente o la Agencia de Protección de Datos nos lo pide, podemos demostrarlo; y como ni siquiera el administrador puede modificarlo, vale como prueba. Un registro que yo pudiera borrar no serviría de nada.
Avisos en el momento (día 3). Hoy, si alguien se concede permisos de administrador un sábado por la noche, nos enteraríamos en la revisión del mes siguiente. Con esto nos enteramos en segundos.
Inventario y revisiones (día 4). Saber en todo momento qué existe, quién tiene acceso a qué, y qué está costando dinero sin dar servicio.
Por qué ahora y no más adelante. Es una cuestión de aritmética, y es el argumento más importante de esta nota. Hoy tenemos cinco proyectos y unos ciento veinte recursos: poner las reglas afecta a cero cosas existentes, porque ya lo hacemos todo bien. Dentro de dos años, con quince proyectos y seiscientos recursos, poner esas mismas reglas significará encontrar todo lo que no las cumple, hablar con cada equipo y aceptar excepciones permanentes que dejarían las reglas casi vacías. Cuatro días ahora son cuatro semanas dentro de dos años, y probablemente ya no se haga.
Y hay un motivo comercial que también conviene tener presente: cuando un cliente grande nos pida un cuestionario de seguridad —y nos lo van a pedir— la diferencia entre responder "sí, y aquí está el registro de los últimos dieciocho meses" y responder "confiamos en nuestro equipo" puede ser la diferencia entre firmar el contrato o no.
Lo que pido además del tiempo:
- Que una segunda persona tenga las llaves maestras. Hoy soy la única que puede levantar estas reglas. Si me equivoco o no estoy disponible, el equipo se queda bloqueado.
- Media hora al trimestre para revisar juntos quién tiene acceso a qué. Es rápido y evita que los permisos se acumulen solos, que es lo que siempre acaba pasando.
- Aceptar que durante dos semanas iremos algo más despacio, mientras medimos el impacto de las reglas antes de activarlas. No vamos a encender nada sin haberlo probado primero: bloquear al equipo por prisa sería el peor resultado posible.
Las cuatro decisiones de comunicación:
- Reconocer que tienen razón antes de rebatir. "Es verdad que funciona" desarma la objeción en lugar de enfrentarse a ella. Lo que se corrige es el porqué funciona, no el hecho.
- Dos incidentes reales en lugar de riesgos hipotéticos. "Podría pasar" se descarta; "nos pasó en marzo y lo encontramos en julio" no. Y ambos casos se resolvieron, lo que permite contarlos sin parecer alarmista ni incompetente.
- El argumento aritmético como eje. Cuatro días ahora frente a cuatro semanas dentro de dos años es un razonamiento que un director financiero entiende inmediatamente y con el que no se puede discutir, porque no depende de valorar el riesgo: depende de contar.
- El ángulo comercial al final. Convierte el gobierno de coste en habilitador de ingresos. Es el argumento que más peso tiene en un consejo y por eso va después de los demás, no antes: puesto primero parecería una excusa; puesto al final, remata.
Y una quinta, que se ve en las peticiones: se pide explícitamente ir más despacio dos semanas. Anunciar el coste antes de que se note evita que la primera fricción se interprete como un error de planificación.
Conclusión
El módulo 7 termina donde tenía que terminar: convirtiendo en controles lo que hasta ahora eran acuerdos.
Sabes qué es el gobierno y respondes a sus cuatro preguntas con mecanismos concretos: qué se puede crear (políticas de organización), qué existe (Asset Inventory), quién hizo qué (Audit Logs) y quién paga (estructura y etiquetas). Y tienes el argumento que decide cuándo implantarlo: el coste no crece con los recursos, crece con las excepciones que hay que negociar. Las políticas se ponen cuando no molestan a nadie; después ya no se ponen.
Sabes estructurar una organización con los tres criterios —entorno, equipo, aplicación— y, más importante, con sus consecuencias en permisos, políticas heredadas, facturación, cuotas y radio de explosión. Entiendes por qué AlpinaShop estructura por entorno y por qué AlpinaTech, con clientes externos, debe hacerlo por cliente.
Dominas las políticas de organización: qué las distingue de IAM —no son permisos, y no las salta ni un owner—, sus tres tipos, y la herencia con su regla clave: una denegación en cualquier nivel gana siempre. Tienes las nueve políticas de AlpinaShop con su motivo, su nivel y su riesgo, incluidas las dos que cierran deudas críticas de 07-04.
Sabes aplicarlas sin bloquear al equipo: el patrón dry-run por cuarta vez en el curso, el procedimiento de ocho pasos que empieza por inventariar lo que ya incumple, las cinco formas reales de bloquearse a uno mismo, y la salvaguarda que hay que preparar antes de todo: dos personas con orgpolicy.policyAdmin.
Sabes escribir restricciones personalizadas con CEL y distinguirlas de Policy Controller, que actúa sobre Kubernetes y no compite sino que complementa. Y sabes que las cuotas impiden donde los presupuestos solo avisan.
Manejas Cloud Asset Inventory para saber qué existe, buscar recursos y políticas IAM, exportar a BigQuery y construir el histórico que responde a "¿qué había el 3 de junio?". Y montas feeds que avisan en segundos de una concesión de owner, un bucket público o —el más importante— la creación de una clave de cuenta de servicio.
Conoces los Cloud Audit Logs de verdad: los cuatro tipos, con los dos datos que hay que retener —la actividad de administrador es gratis y no se puede desactivar; el acceso a datos está apagado por defecto y sin él no se puede acotar una brecha— y las tres decisiones que hacen su coste asumible. Sabes leer una entrada campo a campo, incluido serviceAccountDelegationInfo, que casi nadie mira y sin el cual se pierde el rastro de la persona detrás de una impersonación.
Tienes las cinco consultas de auditoría guardadas antes de necesitarlas, con la de "quién tocó un log" señalada como la más importante y la menos frecuente, y con las denegaciones de política usadas como termómetro de si tus reglas están bien calibradas.
Y has montado la pieza que hace creíble todo lo demás: retención inmutable en un proyecto separado, con sumidero agregado a nivel de organización e --include-children para que ningún proyecto futuro quede fuera, con logging.bucketWriter concedido a la writerIdentity —el paso que se olvida siempre—, y con --locked irreversible. Donde Marta, que administra toda la plataforma, solo puede leer. Porque un log que el administrador puede borrar no sirve como evidencia.
Sabes qué es una zona de aterrizaje y —lo que importa— por qué el orden es el que es: la auditoría antes que la red y las cargas, para no perder los logs del periodo con más cambios de toda la vida de la plataforma. Con los depends_on explícitos de Terraform como caso legítimo, y con los blueprints de Google como referencia y no como plantilla.
Y tienes los procesos de gestión del cambio dimensionados para tres personas: revisiones con su periodicidad y su script, ciclo de vida de proyectos con fecha de caducidad obligatoria en los de prueba, y un proceso de excepciones donde la fecha de caducidad es lo único que separa una excepción de una derogación permanente.
Y aquí termina el módulo 7. Mira dónde está AlpinaShop.
Empezó este módulo con una aplicación que se construía, se desplegaba y se observaba sola, y con una lista de conversaciones aplazadas. Hoy la lista está vacía. DA-004 decidió no adoptar la plataforma híbrida y conectar Sabadell con un túnel, con sus condiciones de revisión escritas. DA-001, prometida en el módulo 2 y arrastrada durante cinco, está cumplida: el catálogo corre en Cloud Run, el MIG está apagado, y no queda una sola máquina virtual en la ruta de servicio. La red es una VPC compartida con permisos por subred, conectividad híbrida real y un perímetro que impide que los datos salgan. La seguridad se ha revisado entera capa por capa, con un checklist de 43 controles y una deuda priorizada de la que ya está pagada la parte crítica. La factura ha bajado un 60 % y —más importante— se entiende, se atribuye y se sigue por coste unitario. La fiabilidad tiene números: cuatro SLI, cuatro SLO, presupuesto de error con política, plan de recuperación con RTO y RPO, y una restauración probada que encontró cinco problemas que nadie sospechaba. Y el gobierno convierte todo eso en controles que no dependen de que nadie se acuerde.
Queda deuda, escrita y priorizada. Y queda una lista de cosas que AlpinaShop ha decidido no hacer, con su motivo — que es lo que de verdad distingue a una organización madura de una que simplemente no llegó.
Has recorrido siete módulos siguiendo a Marta, a Dani y a Lucía. Has visto crear una cuenta y una jerarquía, migrar una aplicación, elegir entre cinco servicios de cómputo, montar redes, identidades y secretos, construir una plataforma de datos, entrenar y servir modelos, automatizar la entrega, observar el sistema, resolver un incidente de extremo a extremo, escribir la infraestructura como código, y ahora asegurarla, medirla, abaratarla y gobernarla. Has visto tomar decisiones y también deshacerlas: App Engine se descartó, GKE se retiró, Anthos se rechazó, un endpoint se apagó. Has visto cuatro decisiones de arquitectura documentadas, revisadas y —algunas— cumplidas años después de escribirse.
Lo que no has hecho todavía es decidir tú.
El módulo 8 es tu proyecto. Ya no hay una AlpinaShop a la que seguir: hay una empresa, un problema y un conjunto de restricciones que tendrás que resolver de principio a fin. Vas a recoger requisitos y traducirlos a decisiones (08-01), diseñar la arquitectura completa justificando cada elección igual que se justificó DA-001 (08-02), implementarla con infraestructura como código y entrega continua (08-03), probarla y desplegarla con canario, SLO y plan de recuperación (08-04), y presentarla y defenderla ante quien te pregunte por qué elegiste lo que elegiste (08-05). Y terminarás mirando hacia adelante, hacia las certificaciones de Google Cloud y hacia lo que viene después de este curso (08-06).
Todo lo que necesitas está en los siete módulos que acabas de recorrer. Ahora te toca a ti.
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
